Skip to content

Implement JSString to reduce bridging overhead #63

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 2 commits into from
Sep 18, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ func expectNumber(_ value: JSValue, file: StaticString = #file, line: UInt = #li

func expectString(_ value: JSValue, file: StaticString = #file, line: UInt = #line, column: UInt = #column) throws -> String {
switch value {
case let .string(string): return string
case let .string(string): return String(string)
default:
throw MessageError("Type of \(value) should be \"string\"", file: file, line: line, column: column)
}
Expand Down
3 changes: 2 additions & 1 deletion IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import JavaScriptKit


try test("Literal Conversion") {
let global = JSObject.global
let inputs: [JSValue] = [
Expand All @@ -16,7 +17,7 @@ try test("Literal Conversion") {
.undefined,
]
for (index, input) in inputs.enumerated() {
let prop = "prop_\(index)"
let prop = JSString("prop_\(index)")
setJSValue(this: global, name: prop, value: input)
let got = getJSValue(this: global, name: prop)
switch (got, input) {
Expand Down
34 changes: 22 additions & 12 deletions Runtime/src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -117,7 +117,7 @@ class SwiftRuntimeHeap {
export class SwiftRuntime {
private instance: WebAssembly.Instance | null;
private heap: SwiftRuntimeHeap
private version: number = 610
private version: number = 611

constructor() {
this.instance = null;
Expand Down Expand Up @@ -163,9 +163,8 @@ export class SwiftRuntime {
const textDecoder = new TextDecoder('utf-8');
const textEncoder = new TextEncoder(); // Only support utf-8

const readString = (ptr: pointer, len: number) => {
const uint8Memory = new Uint8Array(memory().buffer);
return textDecoder.decode(uint8Memory.subarray(ptr, ptr + len));
const readString = (ref: ref) => {
return this.heap.referenceHeap(ref);
}

const writeString = (ptr: pointer, bytes: Uint8Array) => {
Expand Down Expand Up @@ -217,7 +216,7 @@ export class SwiftRuntime {
return payload3;
}
case JavaScriptValueKind.String: {
return readString(payload1, payload2)
return readString(payload1);
}
case JavaScriptValueKind.Object: {
return this.heap.referenceHeap(payload1)
Expand Down Expand Up @@ -261,10 +260,9 @@ export class SwiftRuntime {
break;
}
case "string": {
const bytes = textEncoder.encode(value);
writeUint32(kind_ptr, JavaScriptValueKind.String);
writeUint32(payload1_ptr, this.heap.retain(bytes));
writeUint32(payload2_ptr, bytes.length);
writeUint32(payload1_ptr, this.heap.retain(value));
writeUint32(payload2_ptr, 0);
break;
}
case "undefined": {
Expand Down Expand Up @@ -308,20 +306,20 @@ export class SwiftRuntime {

return {
swjs_set_prop: (
ref: ref, name: pointer, length: number,
ref: ref, name: ref,
kind: JavaScriptValueKind,
payload1: number, payload2: number, payload3: number
) => {
const obj = this.heap.referenceHeap(ref);
Reflect.set(obj, readString(name, length), decodeValue(kind, payload1, payload2, payload3))
Reflect.set(obj, readString(name), decodeValue(kind, payload1, payload2, payload3))
},
swjs_get_prop: (
ref: ref, name: pointer, length: number,
ref: ref, name: ref,
kind_ptr: pointer,
payload1_ptr: pointer, payload2_ptr: pointer, payload3_ptr: number
) => {
const obj = this.heap.referenceHeap(ref);
const result = Reflect.get(obj, readString(name, length));
const result = Reflect.get(obj, readString(name));
writeValue(result, kind_ptr, payload1_ptr, payload2_ptr, payload3_ptr);
},
swjs_set_subscript: (
Expand All @@ -341,6 +339,18 @@ export class SwiftRuntime {
const result = Reflect.get(obj, index);
writeValue(result, kind_ptr, payload1_ptr, payload2_ptr, payload3_ptr);
},
swjs_encode_string: (ref: ref, bytes_ptr_result: pointer) => {
const bytes = textEncoder.encode(this.heap.referenceHeap(ref));
const bytes_ptr = this.heap.retain(bytes);
writeUint32(bytes_ptr_result, bytes_ptr);
return bytes.length;
},
swjs_decode_string: (bytes_ptr: pointer, length: number) => {
const uint8Memory = new Uint8Array(memory().buffer);
const bytes = uint8Memory.subarray(bytes_ptr, bytes_ptr + length);
const string = textDecoder.decode(bytes);
return this.heap.retain(string);
},
swjs_load_string: (ref: ref, buffer: pointer) => {
const bytes = this.heap.referenceHeap(ref);
writeString(buffer, bytes);
Expand Down
2 changes: 1 addition & 1 deletion Sources/JavaScriptKit/Compatibility.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,5 +3,5 @@
/// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`.
@_cdecl("swjs_library_version")
func _library_version() -> Double {
return 610
return 611
}
8 changes: 8 additions & 0 deletions Sources/JavaScriptKit/FundamentalObjects/JSObject.swift
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,14 @@ public class JSObject: Equatable {
/// - Parameter name: The name of this object's member to access.
/// - Returns: The value of the `name` member of this object.
public subscript(_ name: String) -> JSValue {
get { getJSValue(this: self, name: JSString(name)) }
set { setJSValue(this: self, name: JSString(name), value: newValue) }
}

/// Access the `name` member dynamically through JavaScript and Swift runtime bridge library.
/// - Parameter name: The name of this object's member to access.
/// - Returns: The value of the `name` member of this object.
public subscript(_ name: JSString) -> JSValue {
get { getJSValue(this: self, name: name) }
set { setJSValue(this: self, name: name, value: newValue) }
}
Expand Down
104 changes: 104 additions & 0 deletions Sources/JavaScriptKit/FundamentalObjects/JSString.swift
Original file line number Diff line number Diff line change
@@ -0,0 +1,104 @@
import _CJavaScriptKit

/// `JSString` represents a string in JavaScript and supports bridging string between JavaScript and Swift.
///
/// Conversion between `Swift.String` and `JSString` can be:
///
/// ```swift
/// // Convert `Swift.String` to `JSString`
/// let jsString: JSString = ...
/// let swiftString: String = String(jsString)
///
/// // Convert `JSString` to `Swift.String`
/// let swiftString: String = ...
/// let jsString: JSString = JSString(swiftString)
/// ```
///
public struct JSString: LosslessStringConvertible, Equatable {
/// The internal representation of JS compatible string
/// The initializers of this type must initialize `jsRef` or `buffer`.
/// And the uninitialized one will be lazily initialized
class Guts {
var shouldDealocateRef: Bool = false
lazy var jsRef: JavaScriptObjectRef = {
self.shouldDealocateRef = true
return buffer.withUTF8 { bufferPtr in
return _decode_string(bufferPtr.baseAddress!, Int32(bufferPtr.count))
}
}()

lazy var buffer: String = {
var bytesRef: JavaScriptObjectRef = 0
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
var bytesRef: JavaScriptObjectRef = 0
var bytesRef = JavaScriptObjectRef()

let bytesLength = Int(_encode_string(jsRef, &bytesRef))
// +1 for null terminator
let buffer = malloc(Int(bytesLength + 1))!.assumingMemoryBound(to: UInt8.self)
defer {
free(buffer)
_release(bytesRef)
}
_load_string(bytesRef, buffer)
buffer[bytesLength] = 0
return String(decodingCString: UnsafePointer(buffer), as: UTF8.self)
}()

init(from stringValue: String) {
self.buffer = stringValue
}

init(from jsRef: JavaScriptObjectRef) {
self.jsRef = jsRef
self.shouldDealocateRef = true
}

deinit {
guard shouldDealocateRef else { return }
_release(jsRef)
}
}

let guts: Guts

internal init(jsRef: JavaScriptObjectRef) {
self.guts = Guts(from: jsRef)
}

/// Instantiate a new `JSString` with given Swift.String.
public init(_ stringValue: String) {
self.guts = Guts(from: stringValue)
}

/// A Swift representation of this `JSString`.
/// Note that this accessor may copy the JS string value into Swift side memory.
public var description: String { guts.buffer }

/// Returns a Boolean value indicating whether two strings are equal values.
///
/// - Parameters:
/// - lhs: A string to compare.
/// - rhs: Another string to compare.
public static func == (lhs: JSString, rhs: JSString) -> Bool {
return lhs.guts.buffer == rhs.guts.buffer
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could there be a way to run this comparison on the JS side, without potentially triggering copies?

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a trade-off point. If we compare them in JS side, it may trigger copies and re-encode Swift string to JS

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Does it make sense to add a flag to Guts that reports whether the value is cached in Swift? That way it would be possible to do the check in JS if neither string is cached in Swift.

}
}

extension JSString: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self.init(value)
}
}


// MARK: - Internal Helpers
extension JSString {

func asInternalJSRef() -> JavaScriptObjectRef {
guts.jsRef
}

func withRawJSValue<T>(_ body: (RawJSValue) -> T) -> T {
let rawValue = RawJSValue(
kind: .string, payload1: guts.jsRef, payload2: 0, payload3: 0
)
return body(rawValue)
}
}
29 changes: 22 additions & 7 deletions Sources/JavaScriptKit/JSValue.swift
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ import _CJavaScriptKit
/// `JSValue` represents a value in JavaScript.
public enum JSValue: Equatable {
case boolean(Bool)
case string(String)
case string(JSString)
case number(Double)
case object(JSObject)
case null
Expand All @@ -21,7 +21,18 @@ public enum JSValue: Equatable {

/// Returns the `String` value of this JS value if the type is string.
/// If not, returns `nil`.
///
/// Note that this accessor may copy the JS string value into Swift side memory.
///
/// To avoid the copying, please consider the `jsString` instead.
public var string: String? {
jsString.map(String.init)
}

/// Returns the `JSString` value of this JS value if the type is string.
/// If not, returns `nil`.
///
public var jsString: JSString? {
switch self {
case let .string(string): return string
default: return nil
Expand Down Expand Up @@ -76,6 +87,10 @@ extension JSValue {

extension JSValue {

public static func string(_ value: String) -> JSValue {
.string(JSString(value))
}

/// Deprecated: Please create `JSClosure` directly and manage its lifetime manually.
///
/// Migrate this usage
Expand Down Expand Up @@ -108,7 +123,7 @@ extension JSValue {

extension JSValue: ExpressibleByStringLiteral {
public init(stringLiteral value: String) {
self = .string(value)
self = .string(JSString(value))
}
}

Expand All @@ -130,17 +145,17 @@ extension JSValue: ExpressibleByNilLiteral {
}
}

public func getJSValue(this: JSObject, name: String) -> JSValue {
public func getJSValue(this: JSObject, name: JSString) -> JSValue {
var rawValue = RawJSValue()
_get_prop(this.id, name, Int32(name.count),
_get_prop(this.id, name.asInternalJSRef(),
&rawValue.kind,
&rawValue.payload1, &rawValue.payload2, &rawValue.payload3)
return rawValue.jsValue()
}

public func setJSValue(this: JSObject, name: String, value: JSValue) {
public func setJSValue(this: JSObject, name: JSString, value: JSValue) {
value.withRawJSValue { rawValue in
_set_prop(this.id, name, Int32(name.count), rawValue.kind, rawValue.payload1, rawValue.payload2, rawValue.payload3)
_set_prop(this.id, name.asInternalJSRef(), rawValue.kind, rawValue.payload1, rawValue.payload2, rawValue.payload3)
}
}

Expand Down Expand Up @@ -183,7 +198,7 @@ extension JSValue: CustomStringConvertible {
case let .boolean(boolean):
return boolean.description
case .string(let string):
return string
return string.description
case .number(let number):
return number.description
case .object(let object), .function(let object as JSObject):
Expand Down
6 changes: 6 additions & 0 deletions Sources/JavaScriptKit/JSValueConstructible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -91,3 +91,9 @@ extension UInt64: JSValueConstructible {
value.number.map(Self.init)
}
}

extension JSString: JSValueConstructible {
public static func construct(from value: JSValue) -> JSString? {
value.jsString
}
}
23 changes: 8 additions & 15 deletions Sources/JavaScriptKit/JSValueConvertible.swift
Original file line number Diff line number Diff line change
Expand Up @@ -37,7 +37,7 @@ extension Double: JSValueConvertible {
}

extension String: JSValueConvertible {
public func jsValue() -> JSValue { .string(self) }
public func jsValue() -> JSValue { .string(JSString(self)) }
}

extension UInt8: JSValueConvertible {
Expand Down Expand Up @@ -72,6 +72,10 @@ extension Int64: JSValueConvertible {
public func jsValue() -> JSValue { .number(Double(self)) }
}

extension JSString: JSValueConvertible {
public func jsValue() -> JSValue { .string(self) }
}

extension JSObject: JSValueCodable {
// `JSObject.jsValue` is defined in JSObject.swift to be able to overridden
// from `JSFunction`
Expand Down Expand Up @@ -181,13 +185,7 @@ extension RawJSValue: JSValueConvertible {
case .number:
return .number(payload3)
case .string:
// +1 for null terminator
let buffer = malloc(Int(payload2 + 1))!.assumingMemoryBound(to: UInt8.self)
defer { free(buffer) }
_load_string(JavaScriptObjectRef(payload1), buffer)
buffer[Int(payload2)] = 0
let string = String(decodingCString: UnsafePointer(buffer), as: UTF8.self)
return .string(string)
return .string(JSString(jsRef: payload1))
case .object:
return .object(JSObject(id: UInt32(payload1)))
case .null:
Expand Down Expand Up @@ -218,13 +216,8 @@ extension JSValue {
payload1 = 0
payload2 = 0
payload3 = numberValue
case var .string(stringValue):
kind = .string
return stringValue.withUTF8 { bufferPtr in
let ptrValue = UInt32(UInt(bitPattern: bufferPtr.baseAddress!))
let rawValue = RawJSValue(kind: kind, payload1: JavaScriptPayload1(ptrValue), payload2: JavaScriptPayload2(bufferPtr.count), payload3: 0)
return body(rawValue)
}
case let .string(string):
return string.withRawJSValue(body)
case let .object(ref):
kind = .object
payload1 = JavaScriptPayload1(ref.id)
Expand Down
Loading