diff --git a/Examples/Embedded/Package.swift b/Examples/Embedded/Package.swift index 5ae19adc6..aae080022 100644 --- a/Examples/Embedded/Package.swift +++ b/Examples/Embedded/Package.swift @@ -1,11 +1,11 @@ -// swift-tools-version:6.0 +// swift-tools-version:6.1 import PackageDescription let package = Package( name: "Embedded", dependencies: [ - .package(name: "JavaScriptKit", path: "../../"), + .package(name: "JavaScriptKit", path: "../../", traits: ["Embedded"]), .package(url: "https://github.com/swiftwasm/swift-dlmalloc", branch: "0.1.0"), ], targets: [ diff --git a/Examples/Embedded/Sources/EmbeddedApp/main.swift b/Examples/Embedded/Sources/EmbeddedApp/main.swift index 3f8c18ca6..37b2334ba 100644 --- a/Examples/Embedded/Sources/EmbeddedApp/main.swift +++ b/Examples/Embedded/Sources/EmbeddedApp/main.swift @@ -11,18 +11,49 @@ var divElement = document.createElement("div") divElement.innerText = .string("Count \(count)") _ = document.body.appendChild(divElement) -var buttonElement = document.createElement("button") -buttonElement.innerText = "Click me" -buttonElement.onclick = JSValue.object( +var clickMeElement = document.createElement("button") +clickMeElement.innerText = "Click me" +clickMeElement.onclick = JSValue.object( JSClosure { _ in count += 1 divElement.innerText = .string("Count \(count)") return .undefined } ) +_ = document.body.appendChild(clickMeElement) -_ = document.body.appendChild(buttonElement) +var encodeResultElement = document.createElement("pre") +var textInputElement = document.createElement("input") +textInputElement.type = "text" +textInputElement.placeholder = "Enter text to encode to UTF-8" +textInputElement.oninput = JSValue.object( + JSClosure { _ in + let textEncoder = JSObject.global.TextEncoder.function!.new() + let encode = textEncoder.encode.function! + let encodedData = JSTypedArray( + unsafelyWrapping: encode(this: textEncoder, textInputElement.value).object! + ) + encodeResultElement.innerText = .string( + encodedData.withUnsafeBytes { bytes in + bytes.map { hex($0) }.joined(separator: " ") + } + ) + return .undefined + } +) +let encoderContainer = document.createElement("div") +_ = encoderContainer.appendChild(textInputElement) +_ = encoderContainer.appendChild(encodeResultElement) +_ = document.body.appendChild(encoderContainer) func print(_ message: String) { _ = JSObject.global.console.log(message) } + +func hex(_ value: UInt8) -> String { + var result = "0x" + let hexChars: [Character] = ["0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "A", "B", "C", "D", "E", "F"] + result.append(hexChars[Int(value / 16)]) + result.append(hexChars[Int(value % 16)]) + return result +} diff --git a/Examples/Embedded/build.sh b/Examples/Embedded/build.sh index f807cdbf5..81840e76f 100755 --- a/Examples/Embedded/build.sh +++ b/Examples/Embedded/build.sh @@ -1,5 +1,4 @@ #!/bin/bash package_dir="$(cd "$(dirname "$0")" && pwd)" -JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM=true \ - swift package --package-path "$package_dir" \ +swift package --package-path "$package_dir" \ -c release --triple wasm32-unknown-none-wasm js diff --git a/Package.swift b/Package.swift index 85a9a616d..cf88b3db9 100644 --- a/Package.swift +++ b/Package.swift @@ -1,9 +1,8 @@ -// swift-tools-version:6.0 +// swift-tools-version:6.1 import PackageDescription // NOTE: needed for embedded customizations, ideally this will not be necessary at all in the future, or can be replaced with traits -let shouldBuildForEmbedded = Context.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false let useLegacyResourceBundling = Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false @@ -16,22 +15,23 @@ let package = Package( .library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]), .plugin(name: "PackageToJS", targets: ["PackageToJS"]), ], + traits: [ + "Embedded" + ], targets: [ .target( name: "JavaScriptKit", dependencies: ["_CJavaScriptKit"], exclude: useLegacyResourceBundling ? [] : ["Runtime"], resources: useLegacyResourceBundling ? [.copy("Runtime")] : [], - cSettings: shouldBuildForEmbedded - ? [ - .unsafeFlags(["-fdeclspec"]) - ] : nil, - swiftSettings: shouldBuildForEmbedded - ? [ - .enableExperimentalFeature("Embedded"), - .enableExperimentalFeature("Extern"), - .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]), - ] : nil + cSettings: [ + .unsafeFlags(["-fdeclspec"], .when(traits: ["Embedded"])) + ], + swiftSettings: [ + .enableExperimentalFeature("Embedded", .when(traits: ["Embedded"])), + .enableExperimentalFeature("Extern", .when(traits: ["Embedded"])), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"], .when(traits: ["Embedded"])), + ] ), .target(name: "_CJavaScriptKit"), .testTarget( @@ -44,7 +44,11 @@ let package = Package( .target( name: "JavaScriptBigIntSupport", - dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"] + dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"], + swiftSettings: [ + .enableExperimentalFeature("Embedded", .when(traits: ["Embedded"])), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"], .when(traits: ["Embedded"])), + ] ), .target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]), .testTarget( @@ -54,7 +58,11 @@ let package = Package( .target( name: "JavaScriptEventLoop", - dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"] + dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"], + swiftSettings: [ + .enableExperimentalFeature("Embedded", .when(traits: ["Embedded"])), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"], .when(traits: ["Embedded"])), + ] ), .target(name: "_CJavaScriptEventLoop"), .testTarget( diff --git a/Package@swift-6.0.swift b/Package@swift-6.0.swift new file mode 100644 index 000000000..fcf40524a --- /dev/null +++ b/Package@swift-6.0.swift @@ -0,0 +1,104 @@ +// swift-tools-version:6.0 + +import PackageDescription + +// NOTE: needed for embedded customizations, ideally this will not be necessary at all in the future, or can be replaced with traits +let shouldBuildForEmbedded = Context.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false +let useLegacyResourceBundling = + Context.environment["JAVASCRIPTKIT_USE_LEGACY_RESOURCE_BUNDLING"].flatMap(Bool.init) ?? false + +let package = Package( + name: "JavaScriptKit", + products: [ + .library(name: "JavaScriptKit", targets: ["JavaScriptKit"]), + .library(name: "JavaScriptEventLoop", targets: ["JavaScriptEventLoop"]), + .library(name: "JavaScriptBigIntSupport", targets: ["JavaScriptBigIntSupport"]), + .library(name: "JavaScriptEventLoopTestSupport", targets: ["JavaScriptEventLoopTestSupport"]), + .plugin(name: "PackageToJS", targets: ["PackageToJS"]), + ], + targets: [ + .target( + name: "JavaScriptKit", + dependencies: ["_CJavaScriptKit"], + exclude: useLegacyResourceBundling ? [] : ["Runtime"], + resources: useLegacyResourceBundling ? [.copy("Runtime")] : [], + cSettings: shouldBuildForEmbedded + ? [ + .unsafeFlags(["-fdeclspec"]) + ] : nil, + swiftSettings: shouldBuildForEmbedded + ? [ + .enableExperimentalFeature("Embedded"), + .enableExperimentalFeature("Extern"), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]), + ] : nil + ), + .target(name: "_CJavaScriptKit"), + .testTarget( + name: "JavaScriptKitTests", + dependencies: ["JavaScriptKit"], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ] + ), + + .target( + name: "JavaScriptBigIntSupport", + dependencies: ["_CJavaScriptBigIntSupport", "JavaScriptKit"], + swiftSettings: shouldBuildForEmbedded + ? [ + .enableExperimentalFeature("Embedded"), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]), + ] : [] + ), + .target(name: "_CJavaScriptBigIntSupport", dependencies: ["_CJavaScriptKit"]), + .testTarget( + name: "JavaScriptBigIntSupportTests", + dependencies: ["JavaScriptBigIntSupport", "JavaScriptKit"] + ), + + .target( + name: "JavaScriptEventLoop", + dependencies: ["JavaScriptKit", "_CJavaScriptEventLoop"], + swiftSettings: shouldBuildForEmbedded + ? [ + .enableExperimentalFeature("Embedded"), + .unsafeFlags(["-Xfrontend", "-emit-empty-object-file"]), + ] : [] + ), + .target(name: "_CJavaScriptEventLoop"), + .testTarget( + name: "JavaScriptEventLoopTests", + dependencies: [ + "JavaScriptEventLoop", + "JavaScriptKit", + "JavaScriptEventLoopTestSupport", + ], + swiftSettings: [ + .enableExperimentalFeature("Extern") + ] + ), + .target( + name: "JavaScriptEventLoopTestSupport", + dependencies: [ + "_CJavaScriptEventLoopTestSupport", + "JavaScriptEventLoop", + ] + ), + .target(name: "_CJavaScriptEventLoopTestSupport"), + .testTarget( + name: "JavaScriptEventLoopTestSupportTests", + dependencies: [ + "JavaScriptKit", + "JavaScriptEventLoopTestSupport", + ] + ), + .plugin( + name: "PackageToJS", + capability: .command( + intent: .custom(verb: "js", description: "Convert a Swift package to a JavaScript package") + ), + sources: ["Sources"] + ), + ] +) diff --git a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift index 9d0b2c19d..559022c2c 100644 --- a/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift +++ b/Plugins/PackageToJS/Sources/PackageToJSPlugin.swift @@ -161,9 +161,11 @@ struct PackageToJSPlugin: CommandPlugin { } // Build products + let selfPackage = try findSelfPackage(in: context.package) let productName = try buildOptions.product ?? deriveDefaultProduct(package: context.package) let build = try buildWasm( productName: productName, + selfPackage: selfPackage, context: context, options: buildOptions.packageOptions ) @@ -178,14 +180,6 @@ struct PackageToJSPlugin: CommandPlugin { } else { context.pluginWorkDirectoryURL.appending(path: "Package") } - guard - let selfPackage = findPackageInDependencies( - package: context.package, - id: Self.JAVASCRIPTKIT_PACKAGE_ID - ) - else { - throw PackageToJSError("Failed to find JavaScriptKit in dependencies!?") - } var make = MiniMake( explain: buildOptions.packageOptions.explain, printProgress: self.printProgress @@ -226,9 +220,11 @@ struct PackageToJSPlugin: CommandPlugin { exit(1) } + let selfPackage = try findSelfPackage(in: context.package) let productName = "\(context.package.displayName)PackageTests" let build = try buildWasm( productName: productName, + selfPackage: selfPackage, context: context, options: testOptions.packageOptions ) @@ -264,14 +260,6 @@ struct PackageToJSPlugin: CommandPlugin { } else { context.pluginWorkDirectoryURL.appending(path: "PackageTests") } - guard - let selfPackage = findPackageInDependencies( - package: context.package, - id: Self.JAVASCRIPTKIT_PACKAGE_ID - ) - else { - throw PackageToJSError("Failed to find JavaScriptKit in dependencies!?") - } var make = MiniMake( explain: testOptions.packageOptions.explain, printProgress: self.printProgress @@ -311,6 +299,7 @@ struct PackageToJSPlugin: CommandPlugin { private func buildWasm( productName: String, + selfPackage: Package, context: PluginContext, options: PackageToJS.PackageOptions ) throws @@ -331,11 +320,7 @@ struct PackageToJSPlugin: CommandPlugin { ) parameters.echoLogs = true parameters.otherSwiftcFlags = ["-color-diagnostics"] - let buildingForEmbedded = - ProcessInfo.processInfo.environment["JAVASCRIPTKIT_EXPERIMENTAL_EMBEDDED_WASM"].flatMap( - Bool.init - ) ?? false - if !buildingForEmbedded { + if !isBuildingForEmbedded(selfPackage: selfPackage) { // NOTE: We only support static linking for now, and the new SwiftDriver // does not infer `-static-stdlib` for WebAssembly targets intentionally // for future dynamic linking support. @@ -355,6 +340,31 @@ struct PackageToJSPlugin: CommandPlugin { return try self.packageManager.build(.product(productName), parameters: parameters) } + /// Check if the build is for embedded WebAssembly + private func isBuildingForEmbedded(selfPackage: Package) -> Bool { + let coreTarget = selfPackage.targets.first { $0.name == "JavaScriptKit" } + guard let swiftTarget = coreTarget as? SwiftSourceModuleTarget else { + return false + } + // SwiftPM defines "Embedded" compilation condition when `Embedded` experimental + // feature is enabled. + // TODO: This should be replaced with a proper trait-based solution in the future. + return swiftTarget.compilationConditions.contains("Embedded") + } + + /// Find JavaScriptKit package in the dependencies of the given package recursively + private func findSelfPackage(in package: Package) throws -> Package { + guard + let selfPackage = findPackageInDependencies( + package: package, + id: Self.JAVASCRIPTKIT_PACKAGE_ID + ) + else { + throw PackageToJSError("Failed to find JavaScriptKit in dependencies!?") + } + return selfPackage + } + /// Clean if the build graph of the packaging process has changed /// /// This is especially important to detect user changes debug/release diff --git a/Runtime/src/index.ts b/Runtime/src/index.ts index 3f23ed753..83d588ad6 100644 --- a/Runtime/src/index.ts +++ b/Runtime/src/index.ts @@ -479,6 +479,13 @@ export class SwiftRuntime { return obj instanceof constructor; }, + swjs_value_equals: (lhs_ref: ref, rhs_ref: ref) => { + const memory = this.memory; + const lhs = memory.getObject(lhs_ref); + const rhs = memory.getObject(rhs_ref); + return lhs == rhs; + }, + swjs_create_function: ( host_func_id: number, line: number, @@ -517,6 +524,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..bb20bd95f 100644 --- a/Runtime/src/types.ts +++ b/Runtime/src/types.ts @@ -96,12 +96,14 @@ export interface ImportedFunctions { exception_payload2_ptr: pointer ): number; swjs_instanceof(obj_ref: ref, constructor_ref: ref): boolean; + swjs_value_equals(lhs_ref: ref, rhs_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/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 2d6fc33b8..47919b17d 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -1,11 +1,11 @@ // // Created by Manuel Burghard. Licensed unter MIT. // -#if !hasFeature(Embedded) import _CJavaScriptKit /// A protocol that allows a Swift numeric type to be mapped to the JavaScript TypedArray that holds integers of its type -public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValue { +public protocol TypedArrayElement { + associatedtype Element: ConvertibleToJSValue, ConstructibleFromJSValue = Self /// The constructor function for the TypedArray class for this particular kind of number static var typedArrayClass: JSFunction { get } } @@ -13,8 +13,9 @@ public protocol TypedArrayElement: ConvertibleToJSValue, ConstructibleFromJSValu /// A wrapper around all [JavaScript `TypedArray` /// classes](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/TypedArray) /// that exposes their properties in a type-safe way. -public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral where Element: TypedArrayElement { - public class var constructor: JSFunction? { Element.typedArrayClass } +public final class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral where Traits: TypedArrayElement { + public typealias Element = Traits.Element + public class var constructor: JSFunction? { Traits.typedArrayClass } public var jsObject: JSObject public subscript(_ index: Int) -> Element { @@ -139,33 +140,28 @@ public class JSTypedArray: JSBridgedClass, ExpressibleByArrayLiteral wh } } -// MARK: - Int and UInt support - -// FIXME: Should be updated to support wasm64 when that becomes available. -func valueForBitWidth(typeName: String, bitWidth: Int, when32: T) -> T { - if bitWidth == 32 { - return when32 - } else if bitWidth == 64 { - fatalError("64-bit \(typeName)s are not yet supported in JSTypedArray") - } else { - fatalError( - "Unsupported bit width for type \(typeName): \(bitWidth) (hint: stick to fixed-size \(typeName)s to avoid this issue)" - ) - } -} - extension Int: TypedArrayElement { - public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue } - private static let _typedArrayClass = LazyThreadLocal(initialize: { - valueForBitWidth(typeName: "Int", bitWidth: Int.bitWidth, when32: JSObject.global.Int32Array).function! - }) + public static var typedArrayClass: JSFunction { + #if _pointerBitWidth(_32) + return JSObject.global.Int32Array.function! + #elseif _pointerBitWidth(_64) + return JSObject.global.Int64Array.function! + #else + #error("Unsupported pointer width") + #endif + } } extension UInt: TypedArrayElement { - public static var typedArrayClass: JSFunction { _typedArrayClass.wrappedValue } - private static let _typedArrayClass = LazyThreadLocal(initialize: { - valueForBitWidth(typeName: "UInt", bitWidth: Int.bitWidth, when32: JSObject.global.Uint32Array).function! - }) + public static var typedArrayClass: JSFunction { + #if _pointerBitWidth(_32) + return JSObject.global.Uint32Array.function! + #elseif _pointerBitWidth(_64) + return JSObject.global.Uint64Array.function! + #else + #error("Unsupported pointer width") + #endif + } } extension Int8: TypedArrayElement { @@ -176,13 +172,6 @@ extension UInt8: TypedArrayElement { public static var typedArrayClass: JSFunction { JSObject.global.Uint8Array.function! } } -/// A wrapper around [the JavaScript `Uint8ClampedArray` -/// class](https://developer.mozilla.org/docs/Web/JavaScript/Reference/Global_Objects/Array) -/// that exposes its properties in a type-safe and Swifty way. -public class JSUInt8ClampedArray: JSTypedArray { - override public class var constructor: JSFunction? { JSObject.global.Uint8ClampedArray.function! } -} - extension Int16: TypedArrayElement { public static var typedArrayClass: JSFunction { JSObject.global.Int16Array.function! } } @@ -206,4 +195,10 @@ extension Float32: TypedArrayElement { extension Float64: TypedArrayElement { public static var typedArrayClass: JSFunction { JSObject.global.Float64Array.function! } } -#endif + +public enum JSUInt8Clamped: TypedArrayElement { + public typealias Element = UInt8 + public static var typedArrayClass: JSFunction { JSObject.global.Uint8ClampedArray.function! } +} + +public typealias JSUInt8ClampedArray = JSTypedArray 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/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift index cd88a5302..f084ffc81 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -77,7 +77,11 @@ public struct JSString: LosslessStringConvertible, Equatable { /// - 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 + withExtendedLifetime(lhs.guts) { lhsGuts in + withExtendedLifetime(rhs.guts) { rhsGuts in + return swjs_value_equals(lhsGuts.jsRef, rhsGuts.jsRef) + } + } } } 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..aff6d1c8f 100644 --- a/Sources/JavaScriptKit/Runtime/index.d.ts +++ b/Sources/JavaScriptKit/Runtime/index.d.ts @@ -50,8 +50,10 @@ interface ImportedFunctions { swjs_call_new(ref: number, argv: pointer, argc: number): number; swjs_call_throwing_new(ref: number, argv: pointer, argc: number, exception_kind_ptr: pointer, exception_payload1_ptr: pointer, exception_payload2_ptr: pointer): number; swjs_instanceof(obj_ref: ref, constructor_ref: ref): boolean; + swjs_value_equals(lhs_ref: ref, rhs_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..1e45e9b08 100644 --- a/Sources/JavaScriptKit/Runtime/index.js +++ b/Sources/JavaScriptKit/Runtime/index.js @@ -604,6 +604,12 @@ const constructor = memory.getObject(constructor_ref); return obj instanceof constructor; }, + swjs_value_equals: (lhs_ref, rhs_ref) => { + const memory = this.memory; + const lhs = memory.getObject(lhs_ref); + const rhs = memory.getObject(rhs_ref); + return lhs == rhs; + }, swjs_create_function: (host_func_id, line, file) => { var _a; const fileString = this.memory.getObject(file); @@ -627,6 +633,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..ef1f57e74 100644 --- a/Sources/JavaScriptKit/Runtime/index.mjs +++ b/Sources/JavaScriptKit/Runtime/index.mjs @@ -598,6 +598,12 @@ class SwiftRuntime { const constructor = memory.getObject(constructor_ref); return obj instanceof constructor; }, + swjs_value_equals: (lhs_ref, rhs_ref) => { + const memory = this.memory; + const lhs = memory.getObject(lhs_ref); + const rhs = memory.getObject(rhs_ref); + return lhs == rhs; + }, swjs_create_function: (host_func_id, line, file) => { var _a; const fileString = this.memory.getObject(file); @@ -621,6 +627,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..931b48f7a 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -257,6 +257,16 @@ IMPORT_JS_FUNCTION(swjs_call_throwing_new, JavaScriptObjectRef, (const JavaScrip IMPORT_JS_FUNCTION(swjs_instanceof, bool, (const JavaScriptObjectRef obj, const JavaScriptObjectRef constructor)) +/// Acts like JavaScript `==` operator. +/// Performs "==" comparison, a.k.a the "Abstract Equality Comparison" +/// algorithm defined in the ECMAScript. +/// https://262.ecma-international.org/11.0/#sec-abstract-equality-comparison +/// +/// @param lhs The left-hand side value to compare. +/// @param rhs The right-hand side value to compare. +/// @result Return `true` if `lhs` is `==` to `rhs`. Return `false` if not. +IMPORT_JS_FUNCTION(swjs_value_equals, bool, (const JavaScriptObjectRef lhs, const JavaScriptObjectRef rhs)) + /// Creates a JavaScript thunk function that calls Swift side closure. /// See also comments on JSFunction.swift /// @@ -314,6 +324,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") + } +} diff --git a/Tests/JavaScriptKitTests/JSStringTests.swift b/Tests/JavaScriptKitTests/JSStringTests.swift new file mode 100644 index 000000000..456c24147 --- /dev/null +++ b/Tests/JavaScriptKitTests/JSStringTests.swift @@ -0,0 +1,13 @@ +import JavaScriptKit +import XCTest + +final class JSStringTests: XCTestCase { + func testEquatable() { + let string1 = JSString("Hello, world!") + let string2 = JSString("Hello, world!") + let string3 = JSString("Hello, world") + XCTAssertEqual(string1, string1) + XCTAssertEqual(string1, string2) + XCTAssertNotEqual(string1, string3) + } +} diff --git a/Tests/JavaScriptKitTests/JSTypedArrayTests.swift b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift index 0465b1e43..a4649879e 100644 --- a/Tests/JavaScriptKitTests/JSTypedArrayTests.swift +++ b/Tests/JavaScriptKitTests/JSTypedArrayTests.swift @@ -17,8 +17,8 @@ final class JSTypedArrayTests: XCTestCase { } func testTypedArray() { - func checkArray(_ array: [T]) where T: TypedArrayElement & Equatable { - XCTAssertEqual(toString(JSTypedArray(array).jsValue.object!), jsStringify(array)) + func checkArray(_ array: [T]) where T: TypedArrayElement & Equatable, T.Element == T { + XCTAssertEqual(toString(JSTypedArray(array).jsValue.object!), jsStringify(array)) checkArrayUnsafeBytes(array) } @@ -30,20 +30,20 @@ final class JSTypedArrayTests: XCTestCase { array.map({ String(describing: $0) }).joined(separator: ",") } - func checkArrayUnsafeBytes(_ array: [T]) where T: TypedArrayElement & Equatable { - let copyOfArray: [T] = JSTypedArray(array).withUnsafeBytes { buffer in + func checkArrayUnsafeBytes(_ array: [T]) where T: TypedArrayElement & Equatable, T.Element == T { + let copyOfArray: [T] = JSTypedArray(array).withUnsafeBytes { buffer in Array(buffer) } XCTAssertEqual(copyOfArray, array) } let numbers = [UInt8](0...255) - let typedArray = JSTypedArray(numbers) + let typedArray = JSTypedArray(numbers) XCTAssertEqual(typedArray[12], 12) XCTAssertEqual(numbers.count, typedArray.lengthInBytes) let numbersSet = Set(0...255) - let typedArrayFromSet = JSTypedArray(numbersSet) + let typedArrayFromSet = JSTypedArray(numbersSet) XCTAssertEqual(typedArrayFromSet.jsObject.length, 256) XCTAssertEqual(typedArrayFromSet.lengthInBytes, 256 * MemoryLayout.size) @@ -63,7 +63,7 @@ final class JSTypedArrayTests: XCTestCase { 0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, .leastNormalMagnitude, 42, ] - let jsFloat32Array = JSTypedArray(float32Array) + let jsFloat32Array = JSTypedArray(float32Array) for (i, num) in float32Array.enumerated() { XCTAssertEqual(num, jsFloat32Array[i]) } @@ -72,7 +72,7 @@ final class JSTypedArrayTests: XCTestCase { 0, 1, .pi, .greatestFiniteMagnitude, .infinity, .leastNonzeroMagnitude, .leastNormalMagnitude, 42, ] - let jsFloat64Array = JSTypedArray(float64Array) + let jsFloat64Array = JSTypedArray(float64Array) for (i, num) in float64Array.enumerated() { XCTAssertEqual(num, jsFloat64Array[i]) } diff --git a/Utilities/format.swift b/Utilities/format.swift index 9700e9ea3..be6e70858 100755 --- a/Utilities/format.swift +++ b/Utilities/format.swift @@ -67,7 +67,7 @@ let excluded: Set = [ URL(fileURLWithPath: #filePath).lastPathComponent, ] -/// Returns a list of directories to format. +/// Returns a list of file paths to format. func filesToFormat() -> [String] { var files: [String] = [] let fileManager = FileManager.default