From 6eb534b932fc5843492aac731eb3901cb2d9f8e8 Mon Sep 17 00:00:00 2001 From: Max Desiatov Date: Fri, 21 Mar 2025 18:10:49 +0000 Subject: [PATCH 1/4] Add `JSDictionary` type for Embedded Swift compat --- Runtime/src/index.ts | 2 ++ Runtime/src/types.ts | 1 + .../FundamentalObjects/JSDictionary.swift | 34 +++++++++++++++++++ .../_CJavaScriptKit/include/_CJavaScriptKit.h | 2 ++ 4 files changed, 39 insertions(+) create mode 100644 Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 3f23ed753..2bd4ffcef 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -517,6 +517,8 @@ export class SwiftRuntime { return this.memory.retain(array.slice()); }, + swjs_create_object: () => { return this.memory.retain({}); }, + swjs_load_typed_array: (ref: ref, buffer: pointer) => { const memory = this.memory; const typedArray = memory.getObject(ref); diff --git a/Runtime/src/types.ts b/Runtime/src/types.ts index 587b60770..5348f4c16 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -102,6 +102,7 @@ export interface ImportedFunctions { elementsPtr: pointer, length: number ): number; + swjs_create_object(): number; swjs_load_typed_array(ref: ref, buffer: pointer): void; swjs_release(ref: number): void; swjs_release_remote(tid: number, ref: number): void; diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift b/Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift new file mode 100644 index 000000000..51957b279 --- /dev/null +++ b/Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift @@ -0,0 +1,34 @@ +import _CJavaScriptKit + +public final class JSDictionary { + private let ref: JSObject + + init(ref: JSObject) { self.ref = ref } + + public init() { ref = JSObject(id: swjs_create_object()) } + + public subscript(key: String) -> JSValue { + get { ref[dynamicMember: key] } + set { ref[dynamicMember: key] = newValue } + } +} + +extension JSDictionary: ExpressibleByDictionaryLiteral { + public convenience init(dictionaryLiteral elements: (String, JSValue)...) { + self.init() + + for (key, value) in elements { self[key] = value } + } +} + +extension JSDictionary: ConvertibleToJSValue { + public var jsValue: JSValue { .object(ref) } +} + +extension JSDictionary: ConstructibleFromJSValue { + public static func construct(from value: JSValue) -> JSDictionary? { + guard let object = value.object else { return nil } + + return JSDictionary(ref: object) + } +} diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 2b96a81ea..6416379f8 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -314,6 +314,8 @@ IMPORT_JS_FUNCTION(swjs_terminate_worker_thread, void, (int tid)) IMPORT_JS_FUNCTION(swjs_get_worker_thread_id, int, (void)) +IMPORT_JS_FUNCTION(swjs_create_object, JavaScriptObjectRef, (void)) + int swjs_get_worker_thread_id_cached(void); /// Requests sending a JavaScript object to another worker thread. From 5c7e75e526fe6cf971d3653bb38e3de8494651e2 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 25 Mar 2025 15:13:37 +0000 Subject: [PATCH 2/4] ./Utilities/format.swift --- .../FundamentalObjects/JSDictionary.swift | 32 +++++++++---------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift b/Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift index 51957b279..d56cedf17 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift @@ -1,34 +1,34 @@ import _CJavaScriptKit public final class JSDictionary { - private let ref: JSObject + private let ref: JSObject - init(ref: JSObject) { self.ref = ref } + init(ref: JSObject) { self.ref = ref } - public init() { ref = JSObject(id: swjs_create_object()) } + public init() { ref = JSObject(id: swjs_create_object()) } - public subscript(key: String) -> JSValue { - get { ref[dynamicMember: key] } - set { ref[dynamicMember: key] = newValue } - } + public subscript(key: String) -> JSValue { + get { ref[dynamicMember: key] } + set { ref[dynamicMember: key] = newValue } + } } extension JSDictionary: ExpressibleByDictionaryLiteral { - public convenience init(dictionaryLiteral elements: (String, JSValue)...) { - self.init() + public convenience init(dictionaryLiteral elements: (String, JSValue)...) { + self.init() - for (key, value) in elements { self[key] = value } - } + for (key, value) in elements { self[key] = value } + } } extension JSDictionary: ConvertibleToJSValue { - public var jsValue: JSValue { .object(ref) } + public var jsValue: JSValue { .object(ref) } } extension JSDictionary: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> JSDictionary? { - guard let object = value.object else { return nil } + public static func construct(from value: JSValue) -> JSDictionary? { + guard let object = value.object else { return nil } - return JSDictionary(ref: object) - } + return JSDictionary(ref: object) + } } From 0d7d3de483bc687942f90e0d0c18ec34ff9f6319 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 25 Mar 2025 15:37:57 +0000 Subject: [PATCH 3/4] make regenerate_swiftpm_resources --- Sources/JavaScriptKit/Runtime/index.d.ts | 1 + Sources/JavaScriptKit/Runtime/index.js | 1 + Sources/JavaScriptKit/Runtime/index.mjs | 1 + 3 files changed, 3 insertions(+) diff --git a/Sources/JavaScriptKit/Runtime/index.d.ts b/Sources/JavaScriptKit/Runtime/index.d.ts index 5bfa4c242..6bffb1ba4 100644 --- a/Sources/JavaScriptKit/Runtime/index.d.ts +++ b/Sources/JavaScriptKit/Runtime/index.d.ts @@ -52,6 +52,7 @@ interface ImportedFunctions { swjs_instanceof(obj_ref: ref, constructor_ref: ref): boolean; swjs_create_function(host_func_id: number, line: number, file: ref): number; swjs_create_typed_array(constructor_ref: ref, elementsPtr: pointer, length: number): number; + swjs_create_object(): number; swjs_load_typed_array(ref: ref, buffer: pointer): void; swjs_release(ref: number): void; swjs_release_remote(tid: number, ref: number): void; diff --git a/Sources/JavaScriptKit/Runtime/index.js b/Sources/JavaScriptKit/Runtime/index.js index a3bc31397..0ef6c9b32 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -627,6 +627,7 @@ // Call `.slice()` to copy the memory return this.memory.retain(array.slice()); }, + swjs_create_object: () => { return this.memory.retain({}); }, swjs_load_typed_array: (ref, buffer) => { const memory = this.memory; const typedArray = memory.getObject(ref); diff --git a/Sources/JavaScriptKit/Runtime/index.mjs b/Sources/JavaScriptKit/Runtime/index.mjs index ba1b6beaf..8f85b2c47 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -621,6 +621,7 @@ class SwiftRuntime { // Call `.slice()` to copy the memory return this.memory.retain(array.slice()); }, + swjs_create_object: () => { return this.memory.retain({}); }, swjs_load_typed_array: (ref, buffer) => { const memory = this.memory; const typedArray = memory.getObject(ref); From ec0bbe6b5ebd1da915ef7c0a35518f3f750be583 Mon Sep 17 00:00:00 2001 From: Yuta Saito Date: Tue, 25 Mar 2025 15:39:57 +0000 Subject: [PATCH 4/4] Make JSObject conform to ExpressibleByDictionaryLiteral directly --- .../FundamentalObjects/JSClosure.swift | 10 ++++++ .../FundamentalObjects/JSDictionary.swift | 34 ------------------- .../FundamentalObjects/JSObject.swift | 15 +++++++- .../FundamentalObjects/JSSymbol.swift | 5 +++ Tests/JavaScriptKitTests/JSObjectTests.swift | 28 +++++++++++++++ 5 files changed, 57 insertions(+), 35 deletions(-) delete mode 100644 Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift create mode 100644 Tests/JavaScriptKitTests/JSObjectTests.swift diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 66ce009bf..fa713c3b9 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -35,6 +35,11 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { ) } + @available(*, unavailable, message: "JSOneshotClosure does not support dictionary literal initialization") + public required init(dictionaryLiteral elements: (String, JSValue)...) { + fatalError("JSOneshotClosure does not support dictionary literal initialization") + } + #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async(_ body: sending @escaping (sending [JSValue]) async throws -> JSValue) -> JSOneshotClosure @@ -122,6 +127,11 @@ public class JSClosure: JSFunction, JSClosureProtocol { Self.sharedClosures.wrappedValue[hostFuncRef] = (self, body) } + @available(*, unavailable, message: "JSClosure does not support dictionary literal initialization") + public required init(dictionaryLiteral elements: (String, JSValue)...) { + fatalError("JSClosure does not support dictionary literal initialization") + } + #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async(_ body: @Sendable @escaping (sending [JSValue]) async throws -> JSValue) -> JSClosure { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift b/Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift deleted file mode 100644 index d56cedf17..000000000 --- a/Sources/JavaScriptKit/FundamentalObjects/JSDictionary.swift +++ /dev/null @@ -1,34 +0,0 @@ -import _CJavaScriptKit - -public final class JSDictionary { - private let ref: JSObject - - init(ref: JSObject) { self.ref = ref } - - public init() { ref = JSObject(id: swjs_create_object()) } - - public subscript(key: String) -> JSValue { - get { ref[dynamicMember: key] } - set { ref[dynamicMember: key] = newValue } - } -} - -extension JSDictionary: ExpressibleByDictionaryLiteral { - public convenience init(dictionaryLiteral elements: (String, JSValue)...) { - self.init() - - for (key, value) in elements { self[key] = value } - } -} - -extension JSDictionary: ConvertibleToJSValue { - public var jsValue: JSValue { .object(ref) } -} - -extension JSDictionary: ConstructibleFromJSValue { - public static func construct(from value: JSValue) -> JSDictionary? { - guard let object = value.object else { return nil } - - return JSDictionary(ref: object) - } -} diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 33a20f3b5..12dbf9e02 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -15,7 +15,7 @@ import _CJavaScriptKit /// The lifetime of this object is managed by the JavaScript and Swift runtime bridge library with /// reference counting system. @dynamicMemberLookup -public class JSObject: Equatable { +public class JSObject: Equatable, ExpressibleByDictionaryLiteral { internal static var constructor: JSFunction { _constructor.wrappedValue } private static let _constructor = LazyThreadLocal(initialize: { JSObject.global.Object.function! }) @@ -38,6 +38,19 @@ public class JSObject: Equatable { #endif } + /// Creates an empty JavaScript object. + public convenience init() { + self.init(id: swjs_create_object()) + } + + /// Creates a new object with the key-value pairs in the dictionary literal. + /// + /// - Parameter elements: A variadic list of key-value pairs where all keys are strings + public convenience required init(dictionaryLiteral elements: (String, JSValue)...) { + self.init() + for (key, value) in elements { self[key] = value } + } + /// Asserts that the object is being accessed from the owner thread. /// /// - Parameter hint: A string to provide additional context for debugging. diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index 42f63e010..a9461317b 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -24,6 +24,11 @@ public class JSSymbol: JSObject { super.init(id: id) } + @available(*, unavailable, message: "JSSymbol does not support dictionary literal initialization") + public required init(dictionaryLiteral elements: (String, JSValue)...) { + fatalError("JSSymbol does not support dictionary literal initialization") + } + public static func `for`(key: JSString) -> JSSymbol { Symbol.for!(key).symbol! } diff --git a/Tests/JavaScriptKitTests/JSObjectTests.swift b/Tests/JavaScriptKitTests/JSObjectTests.swift new file mode 100644 index 000000000..e283da608 --- /dev/null +++ b/Tests/JavaScriptKitTests/JSObjectTests.swift @@ -0,0 +1,28 @@ +import JavaScriptKit +import XCTest + +final class JSObjectTests: XCTestCase { + func testEmptyObject() { + let object = JSObject() + let keys = JSObject.global.Object.function!.keys.function!(object) + XCTAssertEqual(keys.array?.count, 0) + } + + func testInitWithDictionaryLiteral() { + let object: JSObject = [ + "key1": 1, + "key2": "value2", + "key3": .boolean(true), + "key4": .object(JSObject()), + "key5": [1, 2, 3].jsValue, + "key6": ["key": "value"].jsValue, + ] + XCTAssertEqual(object.key1, .number(1)) + XCTAssertEqual(object.key2, "value2") + XCTAssertEqual(object.key3, .boolean(true)) + let getKeys = JSObject.global.Object.function!.keys.function! + XCTAssertEqual(getKeys(object.key4).array?.count, 0) + XCTAssertEqual(object.key5.array.map(Array.init), [1, 2, 3]) + XCTAssertEqual(object.key6.object?.key, "value") + } +}