diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift index 3ef17e2bb..5d915415d 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/UnitTestUtils.swift @@ -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) } diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index bcd822703..d1a6b147e 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -1,5 +1,6 @@ import JavaScriptKit + try test("Literal Conversion") { let global = JSObject.global let inputs: [JSValue] = [ @@ -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) { diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 0e641ffab..74c2f880f 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -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; @@ -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) => { @@ -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) @@ -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": { @@ -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: ( @@ -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); diff --git a/Sources/JavaScriptKit/Compatibility.swift b/Sources/JavaScriptKit/Compatibility.swift index 375f6a264..0abe8533b 100644 --- a/Sources/JavaScriptKit/Compatibility.swift +++ b/Sources/JavaScriptKit/Compatibility.swift @@ -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 } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 1342922f7..1b324805d 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -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) } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift new file mode 100644 index 000000000..ee334ed5f --- /dev/null +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -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 + 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 + } +} + +extension JSString: ExpressibleByStringLiteral { + public init(stringLiteral value: String) { + self.init(value) + } +} + + +// MARK: - Internal Helpers +extension JSString { + + func asInternalJSRef() -> JavaScriptObjectRef { + guts.jsRef + } + + func withRawJSValue(_ body: (RawJSValue) -> T) -> T { + let rawValue = RawJSValue( + kind: .string, payload1: guts.jsRef, payload2: 0, payload3: 0 + ) + return body(rawValue) + } +} diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index d35b7aaf7..4dc569373 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -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 @@ -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 @@ -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 @@ -108,7 +123,7 @@ extension JSValue { extension JSValue: ExpressibleByStringLiteral { public init(stringLiteral value: String) { - self = .string(value) + self = .string(JSString(value)) } } @@ -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) } } @@ -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): diff --git a/Sources/JavaScriptKit/JSValueConstructible.swift b/Sources/JavaScriptKit/JSValueConstructible.swift index ae380e6b3..e1bf3072e 100644 --- a/Sources/JavaScriptKit/JSValueConstructible.swift +++ b/Sources/JavaScriptKit/JSValueConstructible.swift @@ -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 + } +} diff --git a/Sources/JavaScriptKit/JSValueConvertible.swift b/Sources/JavaScriptKit/JSValueConvertible.swift index e8008b9de..bf94ed16d 100644 --- a/Sources/JavaScriptKit/JSValueConvertible.swift +++ b/Sources/JavaScriptKit/JSValueConvertible.swift @@ -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 { @@ -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` @@ -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: @@ -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) diff --git a/Sources/JavaScriptKit/XcodeSupport.swift b/Sources/JavaScriptKit/XcodeSupport.swift index d9dc3b88d..44b95cefd 100644 --- a/Sources/JavaScriptKit/XcodeSupport.swift +++ b/Sources/JavaScriptKit/XcodeSupport.swift @@ -8,7 +8,7 @@ import _CJavaScriptKit #if !arch(wasm32) func _set_prop( _: JavaScriptObjectRef, - _: UnsafePointer!, _: Int32, + _: JavaScriptObjectRef, _: JavaScriptValueKind, _: JavaScriptPayload1, _: JavaScriptPayload2, @@ -16,7 +16,7 @@ import _CJavaScriptKit ) { fatalError() } func _get_prop( _: JavaScriptObjectRef, - _: UnsafePointer!, _: Int32, + _: JavaScriptObjectRef, _: UnsafeMutablePointer!, _: UnsafeMutablePointer!, _: UnsafeMutablePointer!, @@ -38,6 +38,14 @@ import _CJavaScriptKit _: UnsafeMutablePointer!, _: UnsafeMutablePointer! ) { fatalError() } + func _encode_string( + _: JavaScriptObjectRef, + _: UnsafeMutablePointer! + ) -> Int32 { fatalError() } + func _decode_string( + _: UnsafePointer!, + _: Int32 + ) -> JavaScriptObjectRef { fatalError() } func _load_string( _: JavaScriptObjectRef, _: UnsafeMutablePointer! diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 620854ce1..7cb4e3985 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -32,13 +32,13 @@ typedef struct { #if __wasm32__ __attribute__((__import_module__("javascript_kit"), __import_name__("swjs_set_prop"))) extern void -_set_prop(const JavaScriptObjectRef _this, const char *prop, const int length, +_set_prop(const JavaScriptObjectRef _this, const JavaScriptObjectRef prop, const JavaScriptValueKind kind, const JavaScriptPayload1 payload1, const JavaScriptPayload2 payload2, const JavaScriptPayload3 payload3); __attribute__((__import_module__("javascript_kit"), __import_name__("swjs_get_prop"))) extern void -_get_prop(const JavaScriptObjectRef _this, const char *prop, const int length, +_get_prop(const JavaScriptObjectRef _this, const JavaScriptObjectRef prop, JavaScriptValueKind *kind, JavaScriptPayload1 *payload1, JavaScriptPayload2 *payload2, JavaScriptPayload3 *payload3); @@ -56,6 +56,14 @@ _get_subscript(const JavaScriptObjectRef _this, const int length, JavaScriptValueKind *kind, JavaScriptPayload1 *payload1, JavaScriptPayload2 *payload2, JavaScriptPayload3 *payload3); +__attribute__((__import_module__("javascript_kit"), + __import_name__("swjs_encode_string"))) extern int +_encode_string(const JavaScriptObjectRef str_obj, JavaScriptObjectRef *bytes_ptr_result); + +__attribute__((__import_module__("javascript_kit"), + __import_name__("swjs_decode_string"))) extern JavaScriptObjectRef +_decode_string(const unsigned char *bytes_ptr, const int length); + __attribute__((__import_module__("javascript_kit"), __import_name__("swjs_load_string"))) extern void _load_string(const JavaScriptObjectRef ref, unsigned char *buffer);