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/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/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/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); 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. 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") + } +}