diff --git a/.gitignore b/.gitignore index a24baa7da..5102946ea 100644 --- a/.gitignore +++ b/.gitignore @@ -6,3 +6,4 @@ node_modules /*.xcodeproj xcuserdata/ .swiftpm +.vscode diff --git a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift index c10dc746d..6a9ff54c1 100644 --- a/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift +++ b/IntegrationTests/TestSuites/Sources/PrimaryTests/main.swift @@ -804,6 +804,32 @@ try test("Hashable Conformance") { try expectEqual(firstHash, secondHash) } +try test("Symbols") { + let symbol1 = JSSymbol("abc") + let symbol2 = JSSymbol("abc") + try expectNotEqual(symbol1, symbol2) + try expectEqual(symbol1.name, symbol2.name) + try expectEqual(symbol1.name, "abc") + + try expectEqual(JSSymbol.iterator, JSSymbol.iterator) + + // let hasInstanceClass = { + // prop: function () {} + // }.prop + // Object.defineProperty(hasInstanceClass, Symbol.hasInstance, { value: () => true }) + let hasInstanceObject = JSObject.global.Object.function!.new() + hasInstanceObject.prop = JSClosure { _ in .undefined }.jsValue + let hasInstanceClass = hasInstanceObject.prop.function! + let propertyDescriptor = JSObject.global.Object.function!.new() + propertyDescriptor.value = JSClosure { _ in .boolean(true) }.jsValue + _ = JSObject.global.Object.function!.defineProperty!( + hasInstanceClass, JSSymbol.hasInstance, + propertyDescriptor + ) + try expectEqual(hasInstanceClass[JSSymbol.hasInstance].function!().boolean, true) + try expectEqual(JSObject.global.Object.isInstanceOf(hasInstanceClass), true) +} + struct AnimalStruct: Decodable { let name: String let age: Int diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index f2898f09e..15fc570ea 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -110,11 +110,9 @@ export class SwiftRuntime { payload2: number ) => { const obj = this.memory.getObject(ref); - Reflect.set( - obj, - this.memory.getObject(name), - JSValue.decode(kind, payload1, payload2, this.memory) - ); + const key = this.memory.getObject(name); + const value = JSValue.decode(kind, payload1, payload2, this.memory); + obj[key] = value; }, swjs_get_prop: ( @@ -125,7 +123,8 @@ export class SwiftRuntime { payload2_ptr: pointer ) => { const obj = this.memory.getObject(ref); - const result = Reflect.get(obj, this.memory.getObject(name)); + const key = this.memory.getObject(name); + const result = obj[key]; JSValue.write( result, kind_ptr, @@ -144,11 +143,8 @@ export class SwiftRuntime { payload2: number ) => { const obj = this.memory.getObject(ref); - Reflect.set( - obj, - index, - JSValue.decode(kind, payload1, payload2, this.memory) - ); + const value = JSValue.decode(kind, payload1, payload2, this.memory); + obj[index] = value; }, swjs_get_subscript: ( @@ -159,7 +155,7 @@ export class SwiftRuntime { payload2_ptr: pointer ) => { const obj = this.memory.getObject(ref); - const result = Reflect.get(obj, index); + const result = obj[index]; JSValue.write( result, kind_ptr, @@ -201,11 +197,8 @@ export class SwiftRuntime { const func = this.memory.getObject(ref); let result: any; try { - result = Reflect.apply( - func, - undefined, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + result = func(...args); } catch (error) { JSValue.write( error, @@ -237,11 +230,8 @@ export class SwiftRuntime { const func = this.memory.getObject(ref); let isException = true; try { - const result = Reflect.apply( - func, - undefined, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + const result = func(...args); JSValue.write( result, kind_ptr, @@ -278,11 +268,8 @@ export class SwiftRuntime { const func = this.memory.getObject(func_ref); let result: any; try { - result = Reflect.apply( - func, - obj, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + result = func.apply(obj, args); } catch (error) { JSValue.write( error, @@ -317,11 +304,8 @@ export class SwiftRuntime { const func = this.memory.getObject(func_ref); let isException = true; try { - const result = Reflect.apply( - func, - obj, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + const result = func.apply(obj, args); JSValue.write( result, kind_ptr, @@ -346,10 +330,8 @@ export class SwiftRuntime { }, swjs_call_new: (ref: ref, argv: pointer, argc: number) => { const constructor = this.memory.getObject(ref); - const instance = Reflect.construct( - constructor, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + const instance = new constructor(...args); return this.memory.retain(instance); }, @@ -364,10 +346,8 @@ export class SwiftRuntime { const constructor = this.memory.getObject(ref); let result: any; try { - result = Reflect.construct( - constructor, - JSValue.decodeArray(argv, argc, this.memory) - ); + const args = JSValue.decodeArray(argv, argc, this.memory); + result = new constructor(...args); } catch (error) { JSValue.write( error, diff --git a/Runtime/src/js-value.ts b/Runtime/src/js-value.ts index 61f7b486a..67ac5d46a 100644 --- a/Runtime/src/js-value.ts +++ b/Runtime/src/js-value.ts @@ -10,6 +10,7 @@ export enum Kind { Null = 4, Undefined = 5, Function = 6, + Symbol = 7, } export const decode = ( @@ -102,6 +103,11 @@ export const write = ( memory.writeUint32(payload1_ptr, memory.retain(value)); break; } + case "symbol": { + memory.writeUint32(kind_ptr, exceptionBit | Kind.Symbol); + memory.writeUint32(payload1_ptr, memory.retain(value)); + break; + } default: throw new Error(`Type "${typeof value}" is not supported yet`); } diff --git a/Runtime/src/object-heap.ts b/Runtime/src/object-heap.ts index 61f1925fe..2f9b1fdf3 100644 --- a/Runtime/src/object-heap.ts +++ b/Runtime/src/object-heap.ts @@ -22,33 +22,25 @@ export class SwiftRuntimeHeap { } retain(value: any) { - const isObject = typeof value == "object"; const entry = this._heapEntryByValue.get(value); - if (isObject && entry) { + if (entry) { entry.rc++; return entry.id; } const id = this._heapNextKey++; this._heapValueById.set(id, value); - if (isObject) { - this._heapEntryByValue.set(value, { id: id, rc: 1 }); - } + this._heapEntryByValue.set(value, { id: id, rc: 1 }); return id; } release(ref: ref) { const value = this._heapValueById.get(ref); - const isObject = typeof value == "object"; - if (isObject) { - const entry = this._heapEntryByValue.get(value)!; - entry.rc--; - if (entry.rc != 0) return; - - this._heapEntryByValue.delete(value); - this._heapValueById.delete(ref); - } else { - this._heapValueById.delete(ref); - } + const entry = this._heapEntryByValue.get(value)!; + entry.rc--; + if (entry.rc != 0) return; + + this._heapEntryByValue.delete(value); + this._heapValueById.delete(ref); } referenceHeap(ref: ref) { diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index 10d29a1a6..83680ad02 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -207,6 +207,8 @@ extension RawJSValue: ConvertibleToJSValue { return .undefined case .function: return .function(JSFunction(id: UInt32(payload1))) + case .symbol: + return .symbol(JSSymbol(id: UInt32(payload1))) } } } @@ -238,6 +240,9 @@ extension JSValue { case let .function(functionRef): kind = .function payload1 = JavaScriptPayload1(functionRef.id) + case let .symbol(symbolRef): + kind = .symbol + payload1 = JavaScriptPayload1(symbolRef.id) } let rawValue = RawJSValue(kind: kind, payload1: payload1, payload2: payload2) return body(rawValue) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 4e0019f3c..9cec5dad0 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -11,7 +11,6 @@ import _CJavaScriptKit /// ``` /// public class JSFunction: JSObject { - /// Call this function with given `arguments` and binding given `this` as context. /// - Parameters: /// - this: The value to be passed as the `this` parameter to this function. @@ -19,7 +18,7 @@ public class JSFunction: JSObject { /// - Returns: The result of this call. @discardableResult public func callAsFunction(this: JSObject? = nil, arguments: [ConvertibleToJSValue]) -> JSValue { - invokeNonThrowingJSFunction(self, arguments: arguments, this: this) + invokeNonThrowingJSFunction(self, arguments: arguments, this: this).jsValue } /// A variadic arguments version of `callAsFunction`. @@ -41,7 +40,7 @@ public class JSFunction: JSObject { public func new(arguments: [ConvertibleToJSValue]) -> JSObject { arguments.withRawJSValues { rawValues in rawValues.withUnsafeBufferPointer { bufferPointer in - return JSObject(id: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) + JSObject(id: _call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) } } } @@ -75,7 +74,7 @@ public class JSFunction: JSObject { fatalError("unavailable") } - public override class func construct(from value: JSValue) -> Self? { + override public class func construct(from value: JSValue) -> Self? { return value.function as? Self } @@ -84,9 +83,9 @@ public class JSFunction: JSObject { } } -private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> JSValue { +func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSValue], this: JSObject?) -> RawJSValue { arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer -> (JSValue) in + rawValues.withUnsafeBufferPointer { bufferPointer in let argv = bufferPointer.baseAddress let argc = bufferPointer.count var kindAndFlags = JavaScriptValueKindAndFlags() @@ -94,8 +93,8 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve var payload2 = JavaScriptPayload2() if let thisId = this?.id { _call_function_with_this_no_catch(thisId, - jsFunc.id, argv, Int32(argc), - &kindAndFlags, &payload1, &payload2) + jsFunc.id, argv, Int32(argc), + &kindAndFlags, &payload1, &payload2) } else { _call_function_no_catch( jsFunc.id, argv, Int32(argc), @@ -104,7 +103,7 @@ private func invokeNonThrowingJSFunction(_ jsFunc: JSFunction, arguments: [Conve } assert(!kindAndFlags.isException) let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result.jsValue + return result } } } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 659d9212a..427648bcc 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -95,6 +95,14 @@ public class JSObject: Equatable { set { setJSValue(this: self, index: Int32(index), value: newValue) } } + /// Access the `symbol` member dynamically through JavaScript and Swift runtime bridge library. + /// - Parameter symbol: The name of this object's member to access. + /// - Returns: The value of the `name` member of this object. + public subscript(_ name: JSSymbol) -> JSValue { + get { getJSValue(this: self, symbol: name) } + set { setJSValue(this: self, symbol: name, value: newValue) } + } + /// A modifier to call methods as throwing methods capturing `this` /// /// diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift new file mode 100644 index 000000000..3ec1b2902 --- /dev/null +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -0,0 +1,56 @@ +import _CJavaScriptKit + +private let Symbol = JSObject.global.Symbol.function! + +public class JSSymbol: JSObject { + public var name: String? { self["description"].string } + + public init(_ description: JSString) { + // can’t do `self =` so we have to get the ID manually + let result = invokeNonThrowingJSFunction(Symbol, arguments: [description], this: nil) + precondition(result.kind == .symbol) + super.init(id: UInt32(result.payload1)) + } + @_disfavoredOverload + public convenience init(_ description: String) { + self.init(JSString(description)) + } + + override init(id: JavaScriptObjectRef) { + super.init(id: id) + } + + public static func `for`(key: JSString) -> JSSymbol { + Symbol.for!(key).symbol! + } + + @_disfavoredOverload + public static func `for`(key: String) -> JSSymbol { + Symbol.for!(key).symbol! + } + + public static func key(for symbol: JSSymbol) -> JSString? { + Symbol.keyFor!(symbol).jsString + } + + @_disfavoredOverload + public static func key(for symbol: JSSymbol) -> String? { + Symbol.keyFor!(symbol).string + } +} + +extension JSSymbol { + public static let asyncIterator: JSSymbol! = Symbol.asyncIterator.symbol + public static let hasInstance: JSSymbol! = Symbol.hasInstance.symbol + public static let isConcatSpreadable: JSSymbol! = Symbol.isConcatSpreadable.symbol + public static let iterator: JSSymbol! = Symbol.iterator.symbol + public static let match: JSSymbol! = Symbol.match.symbol + public static let matchAll: JSSymbol! = Symbol.matchAll.symbol + public static let replace: JSSymbol! = Symbol.replace.symbol + public static let search: JSSymbol! = Symbol.search.symbol + public static let species: JSSymbol! = Symbol.species.symbol + public static let split: JSSymbol! = Symbol.split.symbol + public static let toPrimitive: JSSymbol! = Symbol.toPrimitive.symbol + public static let toStringTag: JSSymbol! = Symbol.toStringTag.symbol + public static let unscopables: JSSymbol! = Symbol.unscopables.symbol +} diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 03c2a81ab..b001dc7ab 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -10,6 +10,7 @@ public enum JSValue: Equatable { case null case undefined case function(JSFunction) + case symbol(JSSymbol) /// Returns the `Bool` value of this JS value if its type is boolean. /// If not, returns `nil`. @@ -67,6 +68,13 @@ public enum JSValue: Equatable { } } + public var symbol: JSSymbol? { + switch self { + case let .symbol(symbol): return symbol + default: return nil + } + } + /// Returns the `true` if this JS value is null. /// If not, returns `false`. public var isNull: Bool { @@ -80,23 +88,23 @@ public enum JSValue: Equatable { } } -extension JSValue { +public extension JSValue { /// An unsafe convenience method of `JSObject.subscript(_ name: String) -> ((ConvertibleToJSValue...) -> JSValue)?` /// - Precondition: `self` must be a JavaScript Object and specified member should be a callable object. - public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { + subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { object![dynamicMember: name]! } /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. - public subscript(dynamicMember name: String) -> JSValue { + subscript(dynamicMember name: String) -> JSValue { get { self.object![name] } set { self.object![name] = newValue } } /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. - public subscript(_ index: Int) -> JSValue { + subscript(_ index: Int) -> JSValue { get { object![index] } set { object![index] = newValue } } @@ -104,15 +112,14 @@ extension JSValue { extension JSValue: Swift.Error {} -extension JSValue { - public func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { +public extension JSValue { + func fromJSValue() -> Type? where Type: ConstructibleFromJSValue { return Type.construct(from: self) } } -extension JSValue { - - public static func string(_ value: String) -> JSValue { +public extension JSValue { + static func string(_ value: String) -> JSValue { .string(JSString(value)) } @@ -141,12 +148,12 @@ extension JSValue { /// eventListenter.release() /// ``` @available(*, deprecated, message: "Please create JSClosure directly and manage its lifetime manually.") - public static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { + static func function(_ body: @escaping ([JSValue]) -> JSValue) -> JSValue { .object(JSClosure(body)) } @available(*, deprecated, renamed: "object", message: "JSClosure is no longer a subclass of JSFunction. Use .object(closure) instead.") - public static func function(_ closure: JSClosure) -> JSValue { + static func function(_ closure: JSClosure) -> JSValue { .object(closure) } } @@ -170,7 +177,7 @@ extension JSValue: ExpressibleByFloatLiteral { } extension JSValue: ExpressibleByNilLiteral { - public init(nilLiteral: ()) { + public init(nilLiteral _: ()) { self = .null } } @@ -205,14 +212,28 @@ public func setJSValue(this: JSObject, index: Int32, value: JSValue) { } } -extension JSValue { - /// Return `true` if this value is an instance of the passed `constructor` function. - /// Returns `false` for everything except objects and functions. - /// - Parameter constructor: The constructor function to check. - /// - Returns: The result of `instanceof` in the JavaScript environment. - public func isInstanceOf(_ constructor: JSFunction) -> Bool { +public func getJSValue(this: JSObject, symbol: JSSymbol) -> JSValue { + var rawValue = RawJSValue() + _get_prop(this.id, symbol.id, + &rawValue.kind, + &rawValue.payload1, &rawValue.payload2) + return rawValue.jsValue +} + +public func setJSValue(this: JSObject, symbol: JSSymbol, value: JSValue) { + value.withRawJSValue { rawValue in + _set_prop(this.id, symbol.id, rawValue.kind, rawValue.payload1, rawValue.payload2) + } +} + +public extension JSValue { + /// Return `true` if this value is an instance of the passed `constructor` function. + /// Returns `false` for everything except objects and functions. + /// - Parameter constructor: The constructor function to check. + /// - Returns: The result of `instanceof` in the JavaScript environment. + func isInstanceOf(_ constructor: JSFunction) -> Bool { switch self { - case .boolean, .string, .number, .null, .undefined: + case .boolean, .string, .number, .null, .undefined, .symbol: return false case let .object(ref): return ref.isInstanceOf(constructor) @@ -227,11 +248,12 @@ extension JSValue: CustomStringConvertible { switch self { case let .boolean(boolean): return boolean.description - case .string(let string): + case let .string(string): return string.description - case .number(let number): + case let .number(number): return number.description - case .object(let object), .function(let object as JSObject): + case let .object(object), let .function(object as JSObject), + .symbol(let object as JSObject): return object.toString!().fromJSValue()! case .null: return "null" diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index ce0bf5862..daf405141 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -21,6 +21,7 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindNull = 4, JavaScriptValueKindUndefined = 5, JavaScriptValueKindFunction = 6, + JavaScriptValueKindSymbol = 7, } JavaScriptValueKind; typedef struct { @@ -60,6 +61,10 @@ typedef double JavaScriptPayload2; /// payload1: the target `JavaScriptHostFuncRef` /// payload2: 0 /// +/// For symbol value: +/// payload1: `JavaScriptObjectRef` +/// payload2: 0 +/// typedef struct { JavaScriptValueKind kind; JavaScriptPayload1 payload1;