From fe0b4aee0890e65533421b56827d48e6ab5a63ba Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Mon, 7 Oct 2024 23:00:59 +0200 Subject: [PATCH 1/9] getting rid of existentials, patched headers in C parts --- Package.swift | 3 +- .../BasicObjects/JSPromise.swift | 12 ++ .../JavaScriptKit/BasicObjects/JSTimer.swift | 37 +++- .../BasicObjects/JSTypedArray.swift | 3 +- .../JavaScriptKit/ConvertibleToJSValue.swift | 28 ++- .../FundamentalObjects/JSClosure.swift | 8 +- .../FundamentalObjects/JSFunction.swift | 183 ++++++++++++++---- .../FundamentalObjects/JSObject.swift | 36 +++- .../FundamentalObjects/JSString.swift | 7 +- .../FundamentalObjects/JSSymbol.swift | 4 + .../JSThrowingFunction.swift | 2 + Sources/JavaScriptKit/JSValue.swift | 2 + Sources/JavaScriptKit/JSValueDecoder.swift | 2 + Sources/_CJavaScriptKit/_CJavaScriptKit.c | 12 +- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 4 + swift-build-embedded | 34 ++++ 16 files changed, 319 insertions(+), 58 deletions(-) create mode 100755 swift-build-embedded diff --git a/Package.swift b/Package.swift index aa529c772..fb7e5b4e5 100644 --- a/Package.swift +++ b/Package.swift @@ -14,7 +14,8 @@ let package = Package( .target( name: "JavaScriptKit", dependencies: ["_CJavaScriptKit"], - resources: [.copy("Runtime")] + //LES: TODO - make this conditional + // resources: [.copy("Runtime")] ), .target(name: "_CJavaScriptKit"), .target( diff --git a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift index 4b366d812..a41a3e1ca 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSPromise.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSPromise.swift @@ -58,6 +58,7 @@ public final class JSPromise: JSBridgedClass { self.init(unsafelyWrapping: Self.constructor!.new(closure)) } +#if !hasFeature(Embedded) public static func resolve(_ value: ConvertibleToJSValue) -> JSPromise { self.init(unsafelyWrapping: Self.constructor!.resolve!(value).object!) } @@ -65,7 +66,17 @@ public final class JSPromise: JSBridgedClass { public static func reject(_ reason: ConvertibleToJSValue) -> JSPromise { self.init(unsafelyWrapping: Self.constructor!.reject!(reason).object!) } +#else + public static func resolve(_ value: some ConvertibleToJSValue) -> JSPromise { + self.init(unsafelyWrapping: constructor!.resolve!(value).object!) + } + + public static func reject(_ reason: some ConvertibleToJSValue) -> JSPromise { + self.init(unsafelyWrapping: constructor!.reject!(reason).object!) + } +#endif +#if !hasFeature(Embedded) /// Schedules the `success` closure to be invoked on successful completion of `self`. @discardableResult public func then(success: @escaping (JSValue) -> ConvertibleToJSValue) -> JSPromise { @@ -150,4 +161,5 @@ public final class JSPromise: JSBridgedClass { } return .init(unsafelyWrapping: jsObject.finally!(closure).object!) } +#endif } diff --git a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift index 228b7e83d..a5d7c5b8e 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift @@ -11,10 +11,33 @@ For invalidation you should either store the timer in an optional property and a or deallocate the object that owns the timer. */ public final class JSTimer { + enum ClosureStorage { + case oneshot(JSOneshotClosure) + case repeating(JSClosure) + + var jsValue: JSValue { + switch self { + case .oneshot(let closure): + closure.jsValue + case .repeating(let closure): + closure.jsValue + } + } + + func release() { + switch self { + case .oneshot(let closure): + closure.release() + case .repeating(let closure): + closure.release() + } + } + } + /// Indicates whether this timer instance calls its callback repeatedly at a given delay. public let isRepeating: Bool - private let closure: JSClosureProtocol + private let closure: ClosureStorage /** Node.js and browser APIs are slightly different. `setTimeout`/`setInterval` return an object in Node.js, while browsers return a number. Fortunately, clearTimeout and clearInterval take @@ -35,21 +58,21 @@ public final class JSTimer { */ public init(millisecondsDelay: Double, isRepeating: Bool = false, callback: @escaping () -> ()) { if isRepeating { - closure = JSClosure { _ in + closure = .repeating(JSClosure { _ in callback() return .undefined - } + }) } else { - closure = JSOneshotClosure { _ in + closure = .oneshot(JSOneshotClosure { _ in callback() return .undefined - } + }) } self.isRepeating = isRepeating if isRepeating { - value = global.setInterval.function!(closure, millisecondsDelay) + value = global.setInterval.function!(arguments: [closure.jsValue, millisecondsDelay.jsValue]) } else { - value = global.setTimeout.function!(closure, millisecondsDelay) + value = global.setTimeout.function!(arguments: [closure.jsValue, millisecondsDelay.jsValue]) } } diff --git a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift index 6566e54f3..57df7c865 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTypedArray.swift @@ -1,7 +1,7 @@ // // 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 @@ -187,3 +187,4 @@ extension Float32: TypedArrayElement { extension Float64: TypedArrayElement { public static var typedArrayClass = JSObject.global.Float64Array.function! } +#endif \ No newline at end of file diff --git a/Sources/JavaScriptKit/ConvertibleToJSValue.swift b/Sources/JavaScriptKit/ConvertibleToJSValue.swift index ebf24c74c..660d72f16 100644 --- a/Sources/JavaScriptKit/ConvertibleToJSValue.swift +++ b/Sources/JavaScriptKit/ConvertibleToJSValue.swift @@ -88,6 +88,7 @@ extension JSObject: JSValueCompatible { private let objectConstructor = JSObject.global.Object.function! private let arrayConstructor = JSObject.global.Array.function! +#if !hasFeature(Embedded) extension Dictionary where Value == ConvertibleToJSValue, Key == String { public var jsValue: JSValue { let object = objectConstructor.new() @@ -97,6 +98,7 @@ extension Dictionary where Value == ConvertibleToJSValue, Key == String { return .object(object) } } +#endif extension Dictionary: ConvertibleToJSValue where Value: ConvertibleToJSValue, Key == String { public var jsValue: JSValue { @@ -158,6 +160,7 @@ extension Array: ConvertibleToJSValue where Element: ConvertibleToJSValue { } } +#if !hasFeature(Embedded) extension Array where Element == ConvertibleToJSValue { public var jsValue: JSValue { let array = arrayConstructor.new(count) @@ -167,6 +170,7 @@ extension Array where Element == ConvertibleToJSValue { return .object(array) } } +#endif extension Array: ConstructibleFromJSValue where Element: ConstructibleFromJSValue { public static func construct(from value: JSValue) -> [Element]? { @@ -252,13 +256,13 @@ extension JSValue { } } -extension Array where Element == ConvertibleToJSValue { +extension Array where Element: ConvertibleToJSValue { func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T { // fast path for empty array guard self.count != 0 else { return body([]) } func _withRawJSValues( - _ values: [ConvertibleToJSValue], _ index: Int, + _ values: Self, _ index: Int, _ results: inout [RawJSValue], _ body: ([RawJSValue]) -> T ) -> T { if index == values.count { return body(results) } @@ -272,8 +276,24 @@ extension Array where Element == ConvertibleToJSValue { } } -extension Array where Element: ConvertibleToJSValue { +#if !hasFeature(Embedded) +extension Array where Element == ConvertibleToJSValue { func withRawJSValues(_ body: ([RawJSValue]) -> T) -> T { - [ConvertibleToJSValue].withRawJSValues(self)(body) + // fast path for empty array + guard self.count != 0 else { return body([]) } + + func _withRawJSValues( + _ values: [ConvertibleToJSValue], _ index: Int, + _ results: inout [RawJSValue], _ body: ([RawJSValue]) -> T + ) -> T { + if index == values.count { return body(results) } + return values[index].jsValue.withRawJSValue { (rawValue) -> T in + results.append(rawValue) + return _withRawJSValues(values, index + 1, &results, body) + } + } + var _results = [RawJSValue]() + return _withRawJSValues(self, 0, &_results, body) } } +#endif \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 80fa2cf94..a7a93ba75 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -32,7 +32,7 @@ public class JSOneshotClosure: JSObject, JSClosureProtocol { }) } - #if compiler(>=5.5) + #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSOneshotClosure { JSOneshotClosure(makeAsyncClosure(body)) @@ -113,7 +113,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { Self.sharedClosures[hostFuncRef] = (self, body) } - #if compiler(>=5.5) + #if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) public static func async(_ body: @escaping ([JSValue]) async throws -> JSValue) -> JSClosure { JSClosure(makeAsyncClosure(body)) @@ -129,7 +129,7 @@ public class JSClosure: JSFunction, JSClosureProtocol { #endif } -#if compiler(>=5.5) +#if compiler(>=5.5) && !hasFeature(Embedded) @available(macOS 10.15, iOS 13.0, watchOS 6.0, tvOS 13.0, *) private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSValue) -> (([JSValue]) -> JSValue) { { arguments in @@ -195,7 +195,7 @@ func _call_host_function_impl( guard let (_, hostFunc) = JSClosure.sharedClosures[hostFuncRef] else { return true } - let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map(\.jsValue) + let arguments = UnsafeBufferPointer(start: argv, count: Int(argc)).map { $0.jsValue} let result = hostFunc(arguments) let callbackFuncRef = JSFunction(id: callbackFuncRef) _ = callbackFuncRef(result) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 543146133..ace2aa911 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -10,7 +10,8 @@ import _CJavaScriptKit /// alert("Hello, world") /// ``` /// -public class JSFunction: JSObject { +public class JSFunction: JSObject, _JSFunctionProtocol { +#if !hasFeature(Embedded) /// 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. @@ -60,6 +61,11 @@ public class JSFunction: JSObject { } } + /// A variadic arguments version of `new`. + public func new(_ arguments: ConvertibleToJSValue...) -> JSObject { + new(arguments: arguments) + } + /// A modifier to call this function as a throwing function /// /// @@ -79,11 +85,21 @@ public class JSFunction: JSObject { JSThrowingFunction(self) } - /// A variadic arguments version of `new`. - public func new(_ arguments: ConvertibleToJSValue...) -> JSObject { - new(arguments: arguments) +#else + @discardableResult + public func callAsFunction(arguments: [JSValue]) -> JSValue { + invokeNonThrowingJSFunction(arguments: arguments).jsValue } + public func new(arguments: [JSValue]) -> JSObject { + arguments.withRawJSValues { rawValues in + rawValues.withUnsafeBufferPointer { bufferPointer in + JSObject(id: swjs_call_new(self.id, bufferPointer.baseAddress!, Int32(bufferPointer.count))) + } + } + } +#endif + @available(*, unavailable, message: "Please use JSClosure instead") public static func from(_: @escaping ([JSValue]) -> JSValue) -> JSFunction { fatalError("unavailable") @@ -93,43 +109,136 @@ public class JSFunction: JSObject { .function(self) } +#if hasFeature(Embedded) + final func invokeNonThrowingJSFunction(arguments: [JSValue]) -> RawJSValue { + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } + } + + final func invokeNonThrowingJSFunction(arguments: [JSValue], this: JSObject) -> RawJSValue { + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) } + } +#else final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue { - let id = self.id - return arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer in - let argv = bufferPointer.baseAddress - let argc = bufferPointer.count - var payload1 = JavaScriptPayload1() - var payload2 = JavaScriptPayload2() - let resultBitPattern = swjs_call_function_no_catch( - id, argv, Int32(argc), - &payload1, &payload2 - ) - let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) - assert(!kindAndFlags.isException) - let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result - } - } + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } } final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue], this: JSObject) -> RawJSValue { - let id = self.id - return arguments.withRawJSValues { rawValues in - rawValues.withUnsafeBufferPointer { bufferPointer in - let argv = bufferPointer.baseAddress - let argc = bufferPointer.count - var payload1 = JavaScriptPayload1() - var payload2 = JavaScriptPayload2() - let resultBitPattern = swjs_call_function_with_this_no_catch(this.id, - id, argv, Int32(argc), - &payload1, &payload2 - ) - let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) - assert(!kindAndFlags.isException) - let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) - return result - } + arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) } + } +#endif + + final private func invokeNonThrowingJSFunction(rawValues: [RawJSValue]) -> RawJSValue { + rawValues.withUnsafeBufferPointer { [id] bufferPointer in + let argv = bufferPointer.baseAddress + let argc = bufferPointer.count + var payload1 = JavaScriptPayload1() + var payload2 = JavaScriptPayload2() + let resultBitPattern = swjs_call_function_no_catch( + id, argv, Int32(argc), + &payload1, &payload2 + ) + let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + #if !hasFeature(Embedded) + assert(!kindAndFlags.isException) + #endif + let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) + return result + } + } + + final private func invokeNonThrowingJSFunction(rawValues: [RawJSValue], this: JSObject) -> RawJSValue { + rawValues.withUnsafeBufferPointer { [id] bufferPointer in + let argv = bufferPointer.baseAddress + let argc = bufferPointer.count + var payload1 = JavaScriptPayload1() + var payload2 = JavaScriptPayload2() + let resultBitPattern = swjs_call_function_with_this_no_catch(this.id, + id, argv, Int32(argc), + &payload1, &payload2 + ) + let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + #if !hasFeature(Embedded) + assert(!kindAndFlags.isException) + #endif + let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) + return result } } } + +public protocol _JSFunctionProtocol: JSFunction {} + +#if hasFeature(Embedded) +public extension _JSFunctionProtocol { + // hand-made "varidacs" for Embedded + + @discardableResult + func callAsFunction(this: JSObject) -> JSValue { + invokeNonThrowingJSFunction(arguments: [], this: this).jsValue + } + + @discardableResult + func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue], this: this).jsValue + } + + @discardableResult + func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue], this: this).jsValue + } + + @discardableResult + func callAsFunction(this: JSObject, _ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue], this: this).jsValue + } + + @discardableResult + func callAsFunction(this: JSObject, arguments: [JSValue]) -> JSValue { + invokeNonThrowingJSFunction(arguments: arguments, this: this).jsValue + } + + @discardableResult + func callAsFunction() -> JSValue { + invokeNonThrowingJSFunction(arguments: []).jsValue + } + + @discardableResult + func callAsFunction(_ arg0: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue]).jsValue + } + + @discardableResult + func callAsFunction(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue]).jsValue + } + + @discardableResult + func callAsFunction(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSValue { + invokeNonThrowingJSFunction(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue]).jsValue + } + + func new() -> JSObject { + new(arguments: []) + } + + func new(_ arg0: some ConvertibleToJSValue) -> JSObject { + new(arguments: [arg0.jsValue]) + } + + func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue]) + } + + func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue]) + } + + func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue, arg3: some ConvertibleToJSValue) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue]) + } + + func new(_ arg0: some ConvertibleToJSValue, _ arg1: some ConvertibleToJSValue, _ arg2: some ConvertibleToJSValue, _ arg3: some ConvertibleToJSValue, _ arg4: some ConvertibleToJSValue, _ arg5: some ConvertibleToJSValue, _ arg6: some ConvertibleToJSValue) -> JSObject { + new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue]) + } +} +#endif \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 861758497..3891520c8 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: _JSObjectProtocol, Equatable { @_spi(JSObject_id) public var id: JavaScriptObjectRef @_spi(JSObject_id) @@ -23,6 +23,7 @@ public class JSObject: Equatable { self.id = id } +#if !hasFeature(Embedded) /// Returns the `name` member method binding this object as `this` context. /// /// e.g. @@ -65,6 +66,7 @@ public class JSObject: Equatable { public subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue)? { self[name] } +#endif /// A convenience method of `subscript(_ name: String) -> JSValue` /// to access the member through Dynamic Member Lookup. @@ -105,6 +107,7 @@ public class JSObject: Equatable { set { setJSValue(this: self, symbol: name, value: newValue) } } +#if !hasFeature(Embedded) /// A modifier to call methods as throwing methods capturing `this` /// /// @@ -125,6 +128,7 @@ public class JSObject: Equatable { public var throwing: JSThrowingObject { JSThrowingObject(self) } +#endif /// Return `true` if this value is an instance of the passed `constructor` function. /// - Parameter constructor: The constructor function to check. @@ -197,6 +201,7 @@ extension JSObject: Hashable { } } +#if !hasFeature(Embedded) /// A `JSObject` wrapper that enables throwing method calls capturing `this`. /// Exceptions produced by JavaScript functions will be thrown as `JSValue`. @dynamicMemberLookup @@ -224,3 +229,32 @@ public class JSThrowingObject { self[name] } } +#endif + +public protocol _JSObjectProtocol: JSObject { +} + +#if hasFeature(Embedded) +public extension _JSObjectProtocol { + @_disfavoredOverload + subscript(dynamicMember name: String) -> (() -> JSValue)? { + self[name].function.map { function in + { function(this: self) } + } + } + + @_disfavoredOverload + subscript(dynamicMember name: String) -> ((A0) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0) } + } + } + + @_disfavoredOverload + subscript(dynamicMember name: String) -> ((A0, A1) -> JSValue)? { + self[name].function.map { function in + { function(this: self, $0, $1) } + } + } +} +#endif \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift index 99d4813f2..2eec4ef42 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -31,9 +31,12 @@ public struct JSString: LosslessStringConvertible, Equatable { var bytesRef: JavaScriptObjectRef = 0 let bytesLength = Int(swjs_encode_string(jsRef, &bytesRef)) // +1 for null terminator - let buffer = malloc(Int(bytesLength + 1))!.assumingMemoryBound(to: UInt8.self) + // TODO: revert this back to malloc and free + // let buffer = malloc(Int(bytesLength + 1))!.assumingMemoryBound(to: UInt8.self) + let buffer = UnsafeMutablePointer.allocate(capacity: Int(bytesLength + 1)) defer { - free(buffer) + buffer.deallocate() + // free(buffer) swjs_release(bytesRef) } swjs_load_string(bytesRef, buffer) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index f5d194e25..e7d1a2fd2 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -10,7 +10,11 @@ public class JSSymbol: JSObject { public init(_ description: JSString) { // can’t do `self =` so we have to get the ID manually + #if hasFeature(Embedded) + let result = Symbol.invokeNonThrowingJSFunction(arguments: [description.jsValue]) + #else let result = Symbol.invokeNonThrowingJSFunction(arguments: [description]) + #endif precondition(result.kind == .symbol) super.init(id: UInt32(result.payload1)) } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift index 4763a8779..95bc2bd9c 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSThrowingFunction.swift @@ -1,3 +1,4 @@ +#if !hasFeature(Embedded) import _CJavaScriptKit @@ -94,3 +95,4 @@ private func invokeJSFunction(_ jsFunc: JSFunction, arguments: [ConvertibleToJSV } return result } +#endif \ No newline at end of file diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 7f27e7f50..5166986ff 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -101,11 +101,13 @@ public enum JSValue: Equatable { } public extension JSValue { +#if !hasFeature(Embedded) /// 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. subscript(dynamicMember name: String) -> ((ConvertibleToJSValue...) -> JSValue) { object![dynamicMember: name]! } +#endif /// An unsafe convenience method of `JSObject.subscript(_ index: Int) -> JSValue` /// - Precondition: `self` must be a JavaScript Object. diff --git a/Sources/JavaScriptKit/JSValueDecoder.swift b/Sources/JavaScriptKit/JSValueDecoder.swift index b1d59af63..73ee9310c 100644 --- a/Sources/JavaScriptKit/JSValueDecoder.swift +++ b/Sources/JavaScriptKit/JSValueDecoder.swift @@ -1,3 +1,4 @@ +#if !hasFeature(Embedded) private struct _Decoder: Decoder { fileprivate let node: JSValue @@ -248,3 +249,4 @@ public class JSValueDecoder { return try T(from: decoder) } } +#endif \ No newline at end of file diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index a6e63a1b8..c658bd545 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -1,8 +1,18 @@ #include "_CJavaScriptKit.h" +#if __wasm32__ +#if __Embedded +#if __has_include("malloc.h") +#include +#endif +extern void *malloc(size_t size); +extern void free(void *ptr); +extern void *memset (void *, int, size_t); +extern void *memcpy (void *__restrict, const void *__restrict, size_t); +#else #include #include -#if __wasm32__ +#endif bool _call_host_function_impl(const JavaScriptHostFuncRef host_func_ref, const RawJSValue *argv, const int argc, diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index b8ef2b7b0..7bbabf2c6 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -1,7 +1,11 @@ #ifndef _CJavaScriptKit_h #define _CJavaScriptKit_h +#if __Embedded +#include +#else #include +#endif #include #include diff --git a/swift-build-embedded b/swift-build-embedded new file mode 100755 index 000000000..247658466 --- /dev/null +++ b/swift-build-embedded @@ -0,0 +1,34 @@ +swift build --target JavaScriptKit \ + --triple wasm32-unknown-none-wasm \ + -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ + -Xswiftc -enable-experimental-feature -Xswiftc Extern \ + -Xswiftc -wmo -Xswiftc -disable-cmo \ + -Xswiftc -Xfrontend -Xswiftc -gnone \ + -Xswiftc -Xfrontend -Xswiftc -disable-stack-protector \ + -Xswiftc -cxx-interoperability-mode=default \ + -Xcc -D__Embedded -Xcc -fdeclspec \ + -Xlinker --export-if-defined=__main_argc_argv \ + -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor + + + # -Xlinker --export-if-defined=_initialize + # -Xswiftc -static-stdlib \ + + # --verbose + + # -Xswiftc -Xclang-linker -Xswiftc -nostdlib \ + +####### +# swift build -c release --product ExampleApp \ +# --sdk DEVELOPMENT-SNAPSHOT-2024-09-20-a-wasm32-unknown-wasi \ +# --triple wasm32-unknown-none-wasm \ +# -Xcc -D__Embedded \ +# -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ +# -Xswiftc -enable-experimental-feature -Xswiftc Extern \ + # -Xswiftc -wmo -Xswiftc -disable-cmo \ + # -Xcc -fdeclspec \ + # -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \ + # -Xlinker --export-if-defined=__main_argc_argv \ + + +#cp .build/release/ExampleApp.wasm ./TestPage/app.wasm \ No newline at end of file From 11897e1dbd63e708a27bc31ff52449b441293084 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Tue, 8 Oct 2024 00:26:42 +0200 Subject: [PATCH 2/9] fixed C bit field thing --- .../FundamentalObjects/JSFunction.swift | 29 +++++++++++++++---- .../_CJavaScriptKit/include/_CJavaScriptKit.h | 5 ++++ 2 files changed, 29 insertions(+), 5 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index ace2aa911..f918687f3 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -137,10 +137,8 @@ public class JSFunction: JSObject, _JSFunctionProtocol { id, argv, Int32(argc), &payload1, &payload2 ) - let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) - #if !hasFeature(Embedded) + let kindAndFlags = valueKindAndFlagsFromBits(resultBitPattern) assert(!kindAndFlags.isException) - #endif let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) return result } @@ -156,7 +154,7 @@ public class JSFunction: JSObject, _JSFunctionProtocol { id, argv, Int32(argc), &payload1, &payload2 ) - let kindAndFlags = unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + let kindAndFlags = valueKindAndFlagsFromBits(resultBitPattern) #if !hasFeature(Embedded) assert(!kindAndFlags.isException) #endif @@ -241,4 +239,25 @@ public extension _JSFunctionProtocol { new(arguments: [arg0.jsValue, arg1.jsValue, arg2.jsValue, arg3.jsValue, arg4.jsValue, arg5.jsValue, arg6.jsValue]) } } -#endif \ No newline at end of file + +// C bit fields seem to not work with Embedded +// in "normal mode" this is defined as a C struct +private struct JavaScriptValueKindAndFlags { + let errorBit: UInt32 = 1 << 32 + let kind: JavaScriptValueKind + let isException: Bool + + init(bitPattern: UInt32) { + self.kind = JavaScriptValueKind(rawValue: bitPattern & ~errorBit)! + self.isException = (bitPattern & errorBit) != 0 + } +} +#endif + +private func valueKindAndFlagsFromBits(_ bits: UInt32) -> JavaScriptValueKindAndFlags { + #if hasFeature(Embedded) + JavaScriptValueKindAndFlags(bitPattern: bits) + #else + unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + #endif +} \ No newline at end of file diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index 7bbabf2c6..8daf7cdc6 100644 --- a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h +++ b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h @@ -29,10 +29,15 @@ typedef enum __attribute__((enum_extensibility(closed))) { JavaScriptValueKindBigInt = 8, } JavaScriptValueKind; +#if __Embedded +// something about the bit field widths is not working with embedded +typedef unsigned short JavaScriptValueKindAndFlags; +#else typedef struct { JavaScriptValueKind kind: 31; bool isException: 1; } JavaScriptValueKindAndFlags; +#endif typedef unsigned JavaScriptPayload1; typedef double JavaScriptPayload2; From 26b7a8e99cdacc09ba4b5b2da5c2964680c03477 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:46:24 +0200 Subject: [PATCH 3/9] added embedded example and conditional package flags --- Examples/Basic/build.sh | 2 +- Examples/Embedded/.gitignore | 6 + Examples/Embedded/Package.swift | 20 + Examples/Embedded/README.md | 6 + .../_thingsThatShouldNotBeNeeded.swift | 18 + .../Embedded/Sources/EmbeddedApp/main.swift | 23 + .../_bundling_does_not_work_with_embedded | 0 Examples/Embedded/_Runtime/index.js | 579 ++++++++++++++++++ Examples/Embedded/_Runtime/index.mjs | 569 +++++++++++++++++ Examples/Embedded/build.sh | 12 + Examples/Embedded/index.html | 12 + Examples/Embedded/index.js | 33 + Package.swift | 12 +- Sources/JavaScriptKit/Features.swift | 4 +- .../FundamentalObjects/JSClosure.swift | 12 +- .../FundamentalObjects/JSFunction.swift | 2 +- Sources/JavaScriptKit/JSValue.swift | 19 + Sources/_CJavaScriptKit/_CJavaScriptKit.c | 25 - swift-build-embedded | 34 - 19 files changed, 1316 insertions(+), 72 deletions(-) create mode 100644 Examples/Embedded/.gitignore create mode 100644 Examples/Embedded/Package.swift create mode 100644 Examples/Embedded/README.md create mode 100644 Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift create mode 100644 Examples/Embedded/Sources/EmbeddedApp/main.swift create mode 100644 Examples/Embedded/_Runtime/_bundling_does_not_work_with_embedded create mode 100644 Examples/Embedded/_Runtime/index.js create mode 100644 Examples/Embedded/_Runtime/index.mjs create mode 100755 Examples/Embedded/build.sh create mode 100644 Examples/Embedded/index.html create mode 100644 Examples/Embedded/index.js delete mode 100755 swift-build-embedded diff --git a/Examples/Basic/build.sh b/Examples/Basic/build.sh index f92c05639..2e4c3735b 100755 --- a/Examples/Basic/build.sh +++ b/Examples/Basic/build.sh @@ -1 +1 @@ -swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-07-09-a-wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv +swift build --swift-sdk DEVELOPMENT-SNAPSHOT-2024-09-20-a-wasm32-unknown-wasi -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor -Xlinker --export=__main_argc_argv diff --git a/Examples/Embedded/.gitignore b/Examples/Embedded/.gitignore new file mode 100644 index 000000000..31492b35d --- /dev/null +++ b/Examples/Embedded/.gitignore @@ -0,0 +1,6 @@ +.DS_Store +/.build +/Packages +/*.xcodeproj +xcuserdata/ +Package.resolved \ No newline at end of file diff --git a/Examples/Embedded/Package.swift b/Examples/Embedded/Package.swift new file mode 100644 index 000000000..f0c03bd87 --- /dev/null +++ b/Examples/Embedded/Package.swift @@ -0,0 +1,20 @@ +// swift-tools-version:5.10 + +import PackageDescription + +let package = Package( + name: "Embedded", + dependencies: [ + .package(name: "JavaScriptKit", path: "../../"), + .package(url: "https://github.com/swifweb/EmbeddedFoundation", branch: "0.1.0") + ], + targets: [ + .executableTarget( + name: "EmbeddedApp", + dependencies: [ + "JavaScriptKit", + .product(name: "Foundation", package: "EmbeddedFoundation") + ] + ) + ] +) diff --git a/Examples/Embedded/README.md b/Examples/Embedded/README.md new file mode 100644 index 000000000..92dd6be40 --- /dev/null +++ b/Examples/Embedded/README.md @@ -0,0 +1,6 @@ +# Embedded example + +```sh +$ ./build.sh +$ npx serve +``` diff --git a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift new file mode 100644 index 000000000..50d838b96 --- /dev/null +++ b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift @@ -0,0 +1,18 @@ +import JavaScriptKit + +// NOTE: it seems the embedded tree shaker gets rid of these exports if they are not used somewhere +func _i_need_to_be_here_for_wasm_exports_to_work() { + _ = _library_features + _ = _call_host_function_impl + _ = _free_host_function_impl +} + +// TODO: why do I need this? and surely this is not ideal... figure this out, or at least have this come from a C lib +@_cdecl("strlen") +func strlen(_ s: UnsafePointer) -> Int { + var p = s + while p.pointee != 0 { + p += 1 + } + return p - s +} diff --git a/Examples/Embedded/Sources/EmbeddedApp/main.swift b/Examples/Embedded/Sources/EmbeddedApp/main.swift new file mode 100644 index 000000000..345d8b0a7 --- /dev/null +++ b/Examples/Embedded/Sources/EmbeddedApp/main.swift @@ -0,0 +1,23 @@ +import JavaScriptKit + +let alert = JSObject.global.alert.function! +let document = JSObject.global.document + +print("Document title: \(document.title.string ?? "")") + +var divElement = document.createElement("div") +divElement.innerText = "Hello, world 2" +_ = document.body.appendChild(divElement) + +var buttonElement = document.createElement("button") +buttonElement.innerText = "Alert demo" +buttonElement.onclick = JSValue.object(JSClosure { _ in + divElement.innerText = "Hello, world 3" + return .undefined +}) + +_ = document.body.appendChild(buttonElement) + +func print(_ message: String) { + _ = JSObject.global.console.log(message) +} diff --git a/Examples/Embedded/_Runtime/_bundling_does_not_work_with_embedded b/Examples/Embedded/_Runtime/_bundling_does_not_work_with_embedded new file mode 100644 index 000000000..e69de29bb diff --git a/Examples/Embedded/_Runtime/index.js b/Examples/Embedded/_Runtime/index.js new file mode 100644 index 000000000..9d29b4329 --- /dev/null +++ b/Examples/Embedded/_Runtime/index.js @@ -0,0 +1,579 @@ +(function (global, factory) { + typeof exports === 'object' && typeof module !== 'undefined' ? factory(exports) : + typeof define === 'function' && define.amd ? define(['exports'], factory) : + (global = typeof globalThis !== 'undefined' ? globalThis : global || self, factory(global.JavaScriptKit = {})); +})(this, (function (exports) { 'use strict'; + + /// Memory lifetime of closures in Swift are managed by Swift side + class SwiftClosureDeallocator { + constructor(exports) { + if (typeof FinalizationRegistry === "undefined") { + throw new Error("The Swift part of JavaScriptKit was configured to require " + + "the availability of JavaScript WeakRefs. Please build " + + "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " + + "disable features that use WeakRefs."); + } + this.functionRegistry = new FinalizationRegistry((id) => { + exports.swjs_free_host_function(id); + }); + } + track(func, func_ref) { + this.functionRegistry.register(func, func_ref); + } + } + + function assertNever(x, message) { + throw new Error(message); + } + + const decode = (kind, payload1, payload2, memory) => { + switch (kind) { + case 0 /* Boolean */: + switch (payload1) { + case 0: + return false; + case 1: + return true; + } + case 2 /* Number */: + return payload2; + case 1 /* String */: + case 3 /* Object */: + case 6 /* Function */: + case 7 /* Symbol */: + case 8 /* BigInt */: + return memory.getObject(payload1); + case 4 /* Null */: + return null; + case 5 /* Undefined */: + return undefined; + default: + assertNever(kind, `JSValue Type kind "${kind}" is not supported`); + } + }; + // Note: + // `decodeValues` assumes that the size of RawJSValue is 16. + const decodeArray = (ptr, length, memory) => { + // fast path for empty array + if (length === 0) { + return []; + } + let result = []; + // It's safe to hold DataView here because WebAssembly.Memory.buffer won't + // change within this function. + const view = memory.dataView(); + for (let index = 0; index < length; index++) { + const base = ptr + 16 * index; + const kind = view.getUint32(base, true); + const payload1 = view.getUint32(base + 4, true); + const payload2 = view.getFloat64(base + 8, true); + result.push(decode(kind, payload1, payload2, memory)); + } + return result; + }; + // A helper function to encode a RawJSValue into a pointers. + // Please prefer to use `writeAndReturnKindBits` to avoid unnecessary + // memory stores. + // This function should be used only when kind flag is stored in memory. + const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { + const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory); + memory.writeUint32(kind_ptr, kind); + }; + const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => { + const exceptionBit = (is_exception ? 1 : 0) << 31; + if (value === null) { + return exceptionBit | 4 /* Null */; + } + const writeRef = (kind) => { + memory.writeUint32(payload1_ptr, memory.retain(value)); + return exceptionBit | kind; + }; + const type = typeof value; + switch (type) { + case "boolean": { + memory.writeUint32(payload1_ptr, value ? 1 : 0); + return exceptionBit | 0 /* Boolean */; + } + case "number": { + memory.writeFloat64(payload2_ptr, value); + return exceptionBit | 2 /* Number */; + } + case "string": { + return writeRef(1 /* String */); + } + case "undefined": { + return exceptionBit | 5 /* Undefined */; + } + case "object": { + return writeRef(3 /* Object */); + } + case "function": { + return writeRef(6 /* Function */); + } + case "symbol": { + return writeRef(7 /* Symbol */); + } + case "bigint": { + return writeRef(8 /* BigInt */); + } + default: + assertNever(type, `Type "${type}" is not supported yet`); + } + throw new Error("Unreachable"); + }; + + let globalVariable; + if (typeof globalThis !== "undefined") { + globalVariable = globalThis; + } + else if (typeof window !== "undefined") { + globalVariable = window; + } + else if (typeof global !== "undefined") { + globalVariable = global; + } + else if (typeof self !== "undefined") { + globalVariable = self; + } + + class SwiftRuntimeHeap { + constructor() { + this._heapValueById = new Map(); + this._heapValueById.set(0, globalVariable); + this._heapEntryByValue = new Map(); + this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 }); + // Note: 0 is preserved for global + this._heapNextKey = 1; + } + retain(value) { + const entry = this._heapEntryByValue.get(value); + if (entry) { + entry.rc++; + return entry.id; + } + const id = this._heapNextKey++; + this._heapValueById.set(id, value); + this._heapEntryByValue.set(value, { id: id, rc: 1 }); + return id; + } + release(ref) { + const value = this._heapValueById.get(ref); + const entry = this._heapEntryByValue.get(value); + entry.rc--; + if (entry.rc != 0) + return; + this._heapEntryByValue.delete(value); + this._heapValueById.delete(ref); + } + referenceHeap(ref) { + const value = this._heapValueById.get(ref); + if (value === undefined) { + throw new ReferenceError("Attempted to read invalid reference " + ref); + } + return value; + } + } + + class Memory { + constructor(exports) { + this.heap = new SwiftRuntimeHeap(); + this.retain = (value) => this.heap.retain(value); + this.getObject = (ref) => this.heap.referenceHeap(ref); + this.release = (ref) => this.heap.release(ref); + this.bytes = () => new Uint8Array(this.rawMemory.buffer); + this.dataView = () => new DataView(this.rawMemory.buffer); + this.writeBytes = (ptr, bytes) => this.bytes().set(bytes, ptr); + this.readUint32 = (ptr) => this.dataView().getUint32(ptr, true); + this.readUint64 = (ptr) => this.dataView().getBigUint64(ptr, true); + this.readInt64 = (ptr) => this.dataView().getBigInt64(ptr, true); + this.readFloat64 = (ptr) => this.dataView().getFloat64(ptr, true); + this.writeUint32 = (ptr, value) => this.dataView().setUint32(ptr, value, true); + this.writeUint64 = (ptr, value) => this.dataView().setBigUint64(ptr, value, true); + this.writeInt64 = (ptr, value) => this.dataView().setBigInt64(ptr, value, true); + this.writeFloat64 = (ptr, value) => this.dataView().setFloat64(ptr, value, true); + this.rawMemory = exports.memory; + } + } + + class SwiftRuntime { + constructor(options) { + this.version = 708; + this.textDecoder = new TextDecoder("utf-8"); + this.textEncoder = new TextEncoder(); // Only support utf-8 + /** @deprecated Use `wasmImports` instead */ + this.importObjects = () => this.wasmImports; + this._instance = null; + this._memory = null; + this._closureDeallocator = null; + this.tid = null; + this.options = options || {}; + } + setInstance(instance) { + this._instance = instance; + if (typeof this.exports._start === "function") { + throw new Error(`JavaScriptKit supports only WASI reactor ABI. + Please make sure you are building with: + -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor + `); + } + if (this.exports.swjs_library_version() != this.version) { + throw new Error(`The versions of JavaScriptKit are incompatible. + WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`); + } + } + main() { + const instance = this.instance; + try { + if (typeof instance.exports.main === "function") { + instance.exports.main(); + } + else if (typeof instance.exports.__main_argc_argv === "function") { + // Swift 6.0 and later use `__main_argc_argv` instead of `main`. + instance.exports.__main_argc_argv(0, 0); + } + } + catch (error) { + if (error instanceof UnsafeEventLoopYield) { + // Ignore the error + return; + } + // Rethrow other errors + throw error; + } + } + /** + * Start a new thread with the given `tid` and `startArg`, which + * is forwarded to the `wasi_thread_start` function. + * This function is expected to be called from the spawned Web Worker thread. + */ + startThread(tid, startArg) { + this.tid = tid; + const instance = this.instance; + try { + if (typeof instance.exports.wasi_thread_start === "function") { + instance.exports.wasi_thread_start(tid, startArg); + } + else { + throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`); + } + } + catch (error) { + if (error instanceof UnsafeEventLoopYield) { + // Ignore the error + return; + } + // Rethrow other errors + throw error; + } + } + get instance() { + if (!this._instance) + throw new Error("WebAssembly instance is not set yet"); + return this._instance; + } + get exports() { + return this.instance.exports; + } + get memory() { + if (!this._memory) { + this._memory = new Memory(this.instance.exports); + } + return this._memory; + } + get closureDeallocator() { + if (this._closureDeallocator) + return this._closureDeallocator; + const features = this.exports.swjs_library_features(); + const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; + if (librarySupportsWeakRef) { + this._closureDeallocator = new SwiftClosureDeallocator(this.exports); + } + return this._closureDeallocator; + } + callHostFunction(host_func_id, line, file, args) { + const argc = args.length; + const argv = this.exports.swjs_prepare_host_function_call(argc); + const memory = this.memory; + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + const base = argv + 16 * index; + write(argument, base, base + 4, base + 8, false, memory); + } + let output; + // This ref is released by the swjs_call_host_function implementation + const callback_func_ref = memory.retain((result) => { + output = result; + }); + const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + if (alreadyReleased) { + throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); + } + this.exports.swjs_cleanup_host_function_call(argv); + return output; + } + get wasmImports() { + return { + swjs_set_prop: (ref, name, kind, payload1, payload2) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const value = decode(kind, payload1, payload2, memory); + obj[key] = value; + }, + swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const result = obj[key]; + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); + }, + swjs_set_subscript: (ref, index, kind, payload1, payload2) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const value = decode(kind, payload1, payload2, memory); + obj[index] = value; + }, + swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(ref); + const result = obj[index]; + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_encode_string: (ref, bytes_ptr_result) => { + const memory = this.memory; + const bytes = this.textEncoder.encode(memory.getObject(ref)); + const bytes_ptr = memory.retain(bytes); + memory.writeUint32(bytes_ptr_result, bytes_ptr); + return bytes.length; + }, + swjs_decode_string: ( + // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer + this.options.sharedMemory == true + ? ((bytes_ptr, length) => { + const memory = this.memory; + const bytes = memory + .bytes() + .slice(bytes_ptr, bytes_ptr + length); + const string = this.textDecoder.decode(bytes); + return memory.retain(string); + }) + : ((bytes_ptr, length) => { + const memory = this.memory; + const bytes = memory + .bytes() + .subarray(bytes_ptr, bytes_ptr + length); + const string = this.textDecoder.decode(bytes); + return memory.retain(string); + })), + swjs_load_string: (ref, buffer) => { + const memory = this.memory; + const bytes = memory.getObject(ref); + memory.writeBytes(buffer, bytes); + }, + swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const func = memory.getObject(ref); + let result = undefined; + try { + const args = decodeArray(argv, argc, memory); + result = func(...args); + } + catch (error) { + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + } + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const func = memory.getObject(ref); + const args = decodeArray(argv, argc, memory); + const result = func(...args); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result; + try { + const args = decodeArray(argv, argc, memory); + result = func.apply(obj, args); + } + catch (error) { + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + } + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result = undefined; + const args = decodeArray(argv, argc, memory); + result = func.apply(obj, args); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_new: (ref, argv, argc) => { + const memory = this.memory; + const constructor = memory.getObject(ref); + const args = decodeArray(argv, argc, memory); + const instance = new constructor(...args); + return this.memory.retain(instance); + }, + swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => { + let memory = this.memory; + const constructor = memory.getObject(ref); + let result; + try { + const args = decodeArray(argv, argc, memory); + result = new constructor(...args); + } + catch (error) { + write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); + return -1; + } + memory = this.memory; + write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory); + return memory.retain(result); + }, + swjs_instanceof: (obj_ref, constructor_ref) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const constructor = memory.getObject(constructor_ref); + return obj instanceof constructor; + }, + swjs_create_function: (host_func_id, line, file) => { + var _a; + const fileString = this.memory.getObject(file); + const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args); + const func_ref = this.memory.retain(func); + (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, func_ref); + return func_ref; + }, + swjs_create_typed_array: (constructor_ref, elementsPtr, length) => { + const ArrayType = this.memory.getObject(constructor_ref); + const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); + // Call `.slice()` to copy the memory + return this.memory.retain(array.slice()); + }, + swjs_load_typed_array: (ref, buffer) => { + const memory = this.memory; + const typedArray = memory.getObject(ref); + const bytes = new Uint8Array(typedArray.buffer); + memory.writeBytes(buffer, bytes); + }, + swjs_release: (ref) => { + this.memory.release(ref); + }, + swjs_i64_to_bigint: (value, signed) => { + return this.memory.retain(signed ? value : BigInt.asUintN(64, value)); + }, + swjs_bigint_to_i64: (ref, signed) => { + const object = this.memory.getObject(ref); + if (typeof object !== "bigint") { + throw new Error(`Expected a BigInt, but got ${typeof object}`); + } + if (signed) { + return object; + } + else { + if (object < BigInt(0)) { + return BigInt(0); + } + return BigInt.asIntN(64, object); + } + }, + swjs_i64_to_bigint_slow: (lower, upper, signed) => { + const value = BigInt.asUintN(32, BigInt(lower)) + + (BigInt.asUintN(32, BigInt(upper)) << BigInt(32)); + return this.memory.retain(signed ? BigInt.asIntN(64, value) : BigInt.asUintN(64, value)); + }, + swjs_unsafe_event_loop_yield: () => { + throw new UnsafeEventLoopYield(); + }, + swjs_send_job_to_main_thread: (unowned_job) => { + this.postMessageToMainThread({ type: "job", data: unowned_job }); + }, + swjs_listen_message_from_main_thread: () => { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && "listenMessageFromMainThread" in threadChannel)) { + throw new Error("listenMessageFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread."); + } + threadChannel.listenMessageFromMainThread((message) => { + switch (message.type) { + case "wake": + this.exports.swjs_wake_worker_thread(); + break; + default: + const unknownMessage = message.type; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }); + }, + swjs_wake_up_worker_thread: (tid) => { + this.postMessageToWorkerThread(tid, { type: "wake" }); + }, + swjs_listen_message_from_worker_thread: (tid) => { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && "listenMessageFromWorkerThread" in threadChannel)) { + throw new Error("listenMessageFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads."); + } + threadChannel.listenMessageFromWorkerThread(tid, (message) => { + switch (message.type) { + case "job": + this.exports.swjs_enqueue_main_job_from_worker(message.data); + break; + default: + const unknownMessage = message.type; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }); + }, + swjs_terminate_worker_thread: (tid) => { + var _a; + const threadChannel = this.options.threadChannel; + if (threadChannel && "terminateWorkerThread" in threadChannel) { + (_a = threadChannel.terminateWorkerThread) === null || _a === void 0 ? void 0 : _a.call(threadChannel, tid); + } // Otherwise, just ignore the termination request + }, + swjs_get_worker_thread_id: () => { + // Main thread's tid is always -1 + return this.tid || -1; + }, + }; + } + postMessageToMainThread(message) { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && "postMessageToMainThread" in threadChannel)) { + throw new Error("postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread."); + } + threadChannel.postMessageToMainThread(message); + } + postMessageToWorkerThread(tid, message) { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) { + throw new Error("postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads."); + } + threadChannel.postMessageToWorkerThread(tid, message); + } + } + /// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` + /// to JavaScript. This is usually thrown when: + /// - The entry point of the Swift program is `func main() async` + /// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()` + /// - Calling exported `main` or `__main_argc_argv` function from JavaScript + /// + /// This exception must be caught by the caller of the exported function and the caller should + /// catch this exception and just ignore it. + /// + /// FAQ: Why this error is thrown? + /// This error is thrown to unwind the call stack of the Swift program and return the control to + /// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()` + /// because the event loop expects `exit()` call before the end of the event loop. + class UnsafeEventLoopYield extends Error { + } + + exports.SwiftRuntime = SwiftRuntime; + + Object.defineProperty(exports, '__esModule', { value: true }); + +})); diff --git a/Examples/Embedded/_Runtime/index.mjs b/Examples/Embedded/_Runtime/index.mjs new file mode 100644 index 000000000..9201b7712 --- /dev/null +++ b/Examples/Embedded/_Runtime/index.mjs @@ -0,0 +1,569 @@ +/// Memory lifetime of closures in Swift are managed by Swift side +class SwiftClosureDeallocator { + constructor(exports) { + if (typeof FinalizationRegistry === "undefined") { + throw new Error("The Swift part of JavaScriptKit was configured to require " + + "the availability of JavaScript WeakRefs. Please build " + + "with `-Xswiftc -DJAVASCRIPTKIT_WITHOUT_WEAKREFS` to " + + "disable features that use WeakRefs."); + } + this.functionRegistry = new FinalizationRegistry((id) => { + exports.swjs_free_host_function(id); + }); + } + track(func, func_ref) { + this.functionRegistry.register(func, func_ref); + } +} + +function assertNever(x, message) { + throw new Error(message); +} + +const decode = (kind, payload1, payload2, memory) => { + switch (kind) { + case 0 /* Boolean */: + switch (payload1) { + case 0: + return false; + case 1: + return true; + } + case 2 /* Number */: + return payload2; + case 1 /* String */: + case 3 /* Object */: + case 6 /* Function */: + case 7 /* Symbol */: + case 8 /* BigInt */: + return memory.getObject(payload1); + case 4 /* Null */: + return null; + case 5 /* Undefined */: + return undefined; + default: + assertNever(kind, `JSValue Type kind "${kind}" is not supported`); + } +}; +// Note: +// `decodeValues` assumes that the size of RawJSValue is 16. +const decodeArray = (ptr, length, memory) => { + // fast path for empty array + if (length === 0) { + return []; + } + let result = []; + // It's safe to hold DataView here because WebAssembly.Memory.buffer won't + // change within this function. + const view = memory.dataView(); + for (let index = 0; index < length; index++) { + const base = ptr + 16 * index; + const kind = view.getUint32(base, true); + const payload1 = view.getUint32(base + 4, true); + const payload2 = view.getFloat64(base + 8, true); + result.push(decode(kind, payload1, payload2, memory)); + } + return result; +}; +// A helper function to encode a RawJSValue into a pointers. +// Please prefer to use `writeAndReturnKindBits` to avoid unnecessary +// memory stores. +// This function should be used only when kind flag is stored in memory. +const write = (value, kind_ptr, payload1_ptr, payload2_ptr, is_exception, memory) => { + const kind = writeAndReturnKindBits(value, payload1_ptr, payload2_ptr, is_exception, memory); + memory.writeUint32(kind_ptr, kind); +}; +const writeAndReturnKindBits = (value, payload1_ptr, payload2_ptr, is_exception, memory) => { + const exceptionBit = (is_exception ? 1 : 0) << 31; + if (value === null) { + return exceptionBit | 4 /* Null */; + } + const writeRef = (kind) => { + memory.writeUint32(payload1_ptr, memory.retain(value)); + return exceptionBit | kind; + }; + const type = typeof value; + switch (type) { + case "boolean": { + memory.writeUint32(payload1_ptr, value ? 1 : 0); + return exceptionBit | 0 /* Boolean */; + } + case "number": { + memory.writeFloat64(payload2_ptr, value); + return exceptionBit | 2 /* Number */; + } + case "string": { + return writeRef(1 /* String */); + } + case "undefined": { + return exceptionBit | 5 /* Undefined */; + } + case "object": { + return writeRef(3 /* Object */); + } + case "function": { + return writeRef(6 /* Function */); + } + case "symbol": { + return writeRef(7 /* Symbol */); + } + case "bigint": { + return writeRef(8 /* BigInt */); + } + default: + assertNever(type, `Type "${type}" is not supported yet`); + } + throw new Error("Unreachable"); +}; + +let globalVariable; +if (typeof globalThis !== "undefined") { + globalVariable = globalThis; +} +else if (typeof window !== "undefined") { + globalVariable = window; +} +else if (typeof global !== "undefined") { + globalVariable = global; +} +else if (typeof self !== "undefined") { + globalVariable = self; +} + +class SwiftRuntimeHeap { + constructor() { + this._heapValueById = new Map(); + this._heapValueById.set(0, globalVariable); + this._heapEntryByValue = new Map(); + this._heapEntryByValue.set(globalVariable, { id: 0, rc: 1 }); + // Note: 0 is preserved for global + this._heapNextKey = 1; + } + retain(value) { + const entry = this._heapEntryByValue.get(value); + if (entry) { + entry.rc++; + return entry.id; + } + const id = this._heapNextKey++; + this._heapValueById.set(id, value); + this._heapEntryByValue.set(value, { id: id, rc: 1 }); + return id; + } + release(ref) { + const value = this._heapValueById.get(ref); + const entry = this._heapEntryByValue.get(value); + entry.rc--; + if (entry.rc != 0) + return; + this._heapEntryByValue.delete(value); + this._heapValueById.delete(ref); + } + referenceHeap(ref) { + const value = this._heapValueById.get(ref); + if (value === undefined) { + throw new ReferenceError("Attempted to read invalid reference " + ref); + } + return value; + } +} + +class Memory { + constructor(exports) { + this.heap = new SwiftRuntimeHeap(); + this.retain = (value) => this.heap.retain(value); + this.getObject = (ref) => this.heap.referenceHeap(ref); + this.release = (ref) => this.heap.release(ref); + this.bytes = () => new Uint8Array(this.rawMemory.buffer); + this.dataView = () => new DataView(this.rawMemory.buffer); + this.writeBytes = (ptr, bytes) => this.bytes().set(bytes, ptr); + this.readUint32 = (ptr) => this.dataView().getUint32(ptr, true); + this.readUint64 = (ptr) => this.dataView().getBigUint64(ptr, true); + this.readInt64 = (ptr) => this.dataView().getBigInt64(ptr, true); + this.readFloat64 = (ptr) => this.dataView().getFloat64(ptr, true); + this.writeUint32 = (ptr, value) => this.dataView().setUint32(ptr, value, true); + this.writeUint64 = (ptr, value) => this.dataView().setBigUint64(ptr, value, true); + this.writeInt64 = (ptr, value) => this.dataView().setBigInt64(ptr, value, true); + this.writeFloat64 = (ptr, value) => this.dataView().setFloat64(ptr, value, true); + this.rawMemory = exports.memory; + } +} + +class SwiftRuntime { + constructor(options) { + this.version = 708; + this.textDecoder = new TextDecoder("utf-8"); + this.textEncoder = new TextEncoder(); // Only support utf-8 + /** @deprecated Use `wasmImports` instead */ + this.importObjects = () => this.wasmImports; + this._instance = null; + this._memory = null; + this._closureDeallocator = null; + this.tid = null; + this.options = options || {}; + } + setInstance(instance) { + this._instance = instance; + if (typeof this.exports._start === "function") { + throw new Error(`JavaScriptKit supports only WASI reactor ABI. + Please make sure you are building with: + -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor + `); + } + if (this.exports.swjs_library_version() != this.version) { + throw new Error(`The versions of JavaScriptKit are incompatible. + WebAssembly runtime ${this.exports.swjs_library_version()} != JS runtime ${this.version}`); + } + } + main() { + const instance = this.instance; + try { + if (typeof instance.exports.main === "function") { + instance.exports.main(); + } + else if (typeof instance.exports.__main_argc_argv === "function") { + // Swift 6.0 and later use `__main_argc_argv` instead of `main`. + instance.exports.__main_argc_argv(0, 0); + } + } + catch (error) { + if (error instanceof UnsafeEventLoopYield) { + // Ignore the error + return; + } + // Rethrow other errors + throw error; + } + } + /** + * Start a new thread with the given `tid` and `startArg`, which + * is forwarded to the `wasi_thread_start` function. + * This function is expected to be called from the spawned Web Worker thread. + */ + startThread(tid, startArg) { + this.tid = tid; + const instance = this.instance; + try { + if (typeof instance.exports.wasi_thread_start === "function") { + instance.exports.wasi_thread_start(tid, startArg); + } + else { + throw new Error(`The WebAssembly module is not built for wasm32-unknown-wasip1-threads target.`); + } + } + catch (error) { + if (error instanceof UnsafeEventLoopYield) { + // Ignore the error + return; + } + // Rethrow other errors + throw error; + } + } + get instance() { + if (!this._instance) + throw new Error("WebAssembly instance is not set yet"); + return this._instance; + } + get exports() { + return this.instance.exports; + } + get memory() { + if (!this._memory) { + this._memory = new Memory(this.instance.exports); + } + return this._memory; + } + get closureDeallocator() { + if (this._closureDeallocator) + return this._closureDeallocator; + const features = this.exports.swjs_library_features(); + const librarySupportsWeakRef = (features & 1 /* WeakRefs */) != 0; + if (librarySupportsWeakRef) { + this._closureDeallocator = new SwiftClosureDeallocator(this.exports); + } + return this._closureDeallocator; + } + callHostFunction(host_func_id, line, file, args) { + const argc = args.length; + const argv = this.exports.swjs_prepare_host_function_call(argc); + const memory = this.memory; + for (let index = 0; index < args.length; index++) { + const argument = args[index]; + const base = argv + 16 * index; + write(argument, base, base + 4, base + 8, false, memory); + } + let output; + // This ref is released by the swjs_call_host_function implementation + const callback_func_ref = memory.retain((result) => { + output = result; + }); + const alreadyReleased = this.exports.swjs_call_host_function(host_func_id, argv, argc, callback_func_ref); + if (alreadyReleased) { + throw new Error(`The JSClosure has been already released by Swift side. The closure is created at ${file}:${line}`); + } + this.exports.swjs_cleanup_host_function_call(argv); + return output; + } + get wasmImports() { + return { + swjs_set_prop: (ref, name, kind, payload1, payload2) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const value = decode(kind, payload1, payload2, memory); + obj[key] = value; + }, + swjs_get_prop: (ref, name, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const key = memory.getObject(name); + const result = obj[key]; + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, memory); + }, + swjs_set_subscript: (ref, index, kind, payload1, payload2) => { + const memory = this.memory; + const obj = memory.getObject(ref); + const value = decode(kind, payload1, payload2, memory); + obj[index] = value; + }, + swjs_get_subscript: (ref, index, payload1_ptr, payload2_ptr) => { + const obj = this.memory.getObject(ref); + const result = obj[index]; + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_encode_string: (ref, bytes_ptr_result) => { + const memory = this.memory; + const bytes = this.textEncoder.encode(memory.getObject(ref)); + const bytes_ptr = memory.retain(bytes); + memory.writeUint32(bytes_ptr_result, bytes_ptr); + return bytes.length; + }, + swjs_decode_string: ( + // NOTE: TextDecoder can't decode typed arrays backed by SharedArrayBuffer + this.options.sharedMemory == true + ? ((bytes_ptr, length) => { + const memory = this.memory; + const bytes = memory + .bytes() + .slice(bytes_ptr, bytes_ptr + length); + const string = this.textDecoder.decode(bytes); + return memory.retain(string); + }) + : ((bytes_ptr, length) => { + const memory = this.memory; + const bytes = memory + .bytes() + .subarray(bytes_ptr, bytes_ptr + length); + const string = this.textDecoder.decode(bytes); + return memory.retain(string); + })), + swjs_load_string: (ref, buffer) => { + const memory = this.memory; + const bytes = memory.getObject(ref); + memory.writeBytes(buffer, bytes); + }, + swjs_call_function: (ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const func = memory.getObject(ref); + let result = undefined; + try { + const args = decodeArray(argv, argc, memory); + result = func(...args); + } + catch (error) { + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + } + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_no_catch: (ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const func = memory.getObject(ref); + const args = decodeArray(argv, argc, memory); + const result = func(...args); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_with_this: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result; + try { + const args = decodeArray(argv, argc, memory); + result = func.apply(obj, args); + } + catch (error) { + return writeAndReturnKindBits(error, payload1_ptr, payload2_ptr, true, this.memory); + } + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_function_with_this_no_catch: (obj_ref, func_ref, argv, argc, payload1_ptr, payload2_ptr) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const func = memory.getObject(func_ref); + let result = undefined; + const args = decodeArray(argv, argc, memory); + result = func.apply(obj, args); + return writeAndReturnKindBits(result, payload1_ptr, payload2_ptr, false, this.memory); + }, + swjs_call_new: (ref, argv, argc) => { + const memory = this.memory; + const constructor = memory.getObject(ref); + const args = decodeArray(argv, argc, memory); + const instance = new constructor(...args); + return this.memory.retain(instance); + }, + swjs_call_throwing_new: (ref, argv, argc, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr) => { + let memory = this.memory; + const constructor = memory.getObject(ref); + let result; + try { + const args = decodeArray(argv, argc, memory); + result = new constructor(...args); + } + catch (error) { + write(error, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, true, this.memory); + return -1; + } + memory = this.memory; + write(null, exception_kind_ptr, exception_payload1_ptr, exception_payload2_ptr, false, memory); + return memory.retain(result); + }, + swjs_instanceof: (obj_ref, constructor_ref) => { + const memory = this.memory; + const obj = memory.getObject(obj_ref); + const constructor = memory.getObject(constructor_ref); + return obj instanceof constructor; + }, + swjs_create_function: (host_func_id, line, file) => { + var _a; + const fileString = this.memory.getObject(file); + const func = (...args) => this.callHostFunction(host_func_id, line, fileString, args); + const func_ref = this.memory.retain(func); + (_a = this.closureDeallocator) === null || _a === void 0 ? void 0 : _a.track(func, func_ref); + return func_ref; + }, + swjs_create_typed_array: (constructor_ref, elementsPtr, length) => { + const ArrayType = this.memory.getObject(constructor_ref); + const array = new ArrayType(this.memory.rawMemory.buffer, elementsPtr, length); + // Call `.slice()` to copy the memory + return this.memory.retain(array.slice()); + }, + swjs_load_typed_array: (ref, buffer) => { + const memory = this.memory; + const typedArray = memory.getObject(ref); + const bytes = new Uint8Array(typedArray.buffer); + memory.writeBytes(buffer, bytes); + }, + swjs_release: (ref) => { + this.memory.release(ref); + }, + swjs_i64_to_bigint: (value, signed) => { + return this.memory.retain(signed ? value : BigInt.asUintN(64, value)); + }, + swjs_bigint_to_i64: (ref, signed) => { + const object = this.memory.getObject(ref); + if (typeof object !== "bigint") { + throw new Error(`Expected a BigInt, but got ${typeof object}`); + } + if (signed) { + return object; + } + else { + if (object < BigInt(0)) { + return BigInt(0); + } + return BigInt.asIntN(64, object); + } + }, + swjs_i64_to_bigint_slow: (lower, upper, signed) => { + const value = BigInt.asUintN(32, BigInt(lower)) + + (BigInt.asUintN(32, BigInt(upper)) << BigInt(32)); + return this.memory.retain(signed ? BigInt.asIntN(64, value) : BigInt.asUintN(64, value)); + }, + swjs_unsafe_event_loop_yield: () => { + throw new UnsafeEventLoopYield(); + }, + swjs_send_job_to_main_thread: (unowned_job) => { + this.postMessageToMainThread({ type: "job", data: unowned_job }); + }, + swjs_listen_message_from_main_thread: () => { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && "listenMessageFromMainThread" in threadChannel)) { + throw new Error("listenMessageFromMainThread is not set in options given to SwiftRuntime. Please set it to listen to wake events from the main thread."); + } + threadChannel.listenMessageFromMainThread((message) => { + switch (message.type) { + case "wake": + this.exports.swjs_wake_worker_thread(); + break; + default: + const unknownMessage = message.type; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }); + }, + swjs_wake_up_worker_thread: (tid) => { + this.postMessageToWorkerThread(tid, { type: "wake" }); + }, + swjs_listen_message_from_worker_thread: (tid) => { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && "listenMessageFromWorkerThread" in threadChannel)) { + throw new Error("listenMessageFromWorkerThread is not set in options given to SwiftRuntime. Please set it to listen to jobs from worker threads."); + } + threadChannel.listenMessageFromWorkerThread(tid, (message) => { + switch (message.type) { + case "job": + this.exports.swjs_enqueue_main_job_from_worker(message.data); + break; + default: + const unknownMessage = message.type; + throw new Error(`Unknown message type: ${unknownMessage}`); + } + }); + }, + swjs_terminate_worker_thread: (tid) => { + var _a; + const threadChannel = this.options.threadChannel; + if (threadChannel && "terminateWorkerThread" in threadChannel) { + (_a = threadChannel.terminateWorkerThread) === null || _a === void 0 ? void 0 : _a.call(threadChannel, tid); + } // Otherwise, just ignore the termination request + }, + swjs_get_worker_thread_id: () => { + // Main thread's tid is always -1 + return this.tid || -1; + }, + }; + } + postMessageToMainThread(message) { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && "postMessageToMainThread" in threadChannel)) { + throw new Error("postMessageToMainThread is not set in options given to SwiftRuntime. Please set it to send messages to the main thread."); + } + threadChannel.postMessageToMainThread(message); + } + postMessageToWorkerThread(tid, message) { + const threadChannel = this.options.threadChannel; + if (!(threadChannel && "postMessageToWorkerThread" in threadChannel)) { + throw new Error("postMessageToWorkerThread is not set in options given to SwiftRuntime. Please set it to send messages to worker threads."); + } + threadChannel.postMessageToWorkerThread(tid, message); + } +} +/// This error is thrown when yielding event loop control from `swift_task_asyncMainDrainQueue` +/// to JavaScript. This is usually thrown when: +/// - The entry point of the Swift program is `func main() async` +/// - The Swift Concurrency's global executor is hooked by `JavaScriptEventLoop.installGlobalExecutor()` +/// - Calling exported `main` or `__main_argc_argv` function from JavaScript +/// +/// This exception must be caught by the caller of the exported function and the caller should +/// catch this exception and just ignore it. +/// +/// FAQ: Why this error is thrown? +/// This error is thrown to unwind the call stack of the Swift program and return the control to +/// the JavaScript side. Otherwise, the `swift_task_asyncMainDrainQueue` ends up with `abort()` +/// because the event loop expects `exit()` call before the end of the event loop. +class UnsafeEventLoopYield extends Error { +} + +export { SwiftRuntime }; diff --git a/Examples/Embedded/build.sh b/Examples/Embedded/build.sh new file mode 100755 index 000000000..e62caab6c --- /dev/null +++ b/Examples/Embedded/build.sh @@ -0,0 +1,12 @@ +EXPERIMENTAL_EMBEDDED_WASM=true swift build -c release --product EmbeddedApp \ + --triple wasm32-unknown-none-wasm \ + -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ + -Xswiftc -enable-experimental-feature -Xswiftc Extern \ + -Xswiftc -wmo -Xswiftc -disable-cmo \ + -Xswiftc -Xfrontend -Xswiftc -gnone \ + -Xswiftc -Xfrontend -Xswiftc -disable-stack-protector \ + -Xswiftc -cxx-interoperability-mode=default \ + -Xcc -D__Embedded -Xcc -fdeclspec \ + -Xlinker --export-if-defined=__main_argc_argv \ + -Xlinker --export-if-defined=swjs_call_host_function \ + -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \ No newline at end of file diff --git a/Examples/Embedded/index.html b/Examples/Embedded/index.html new file mode 100644 index 000000000..d94796a09 --- /dev/null +++ b/Examples/Embedded/index.html @@ -0,0 +1,12 @@ + + + + + Getting Started + + + + + + + diff --git a/Examples/Embedded/index.js b/Examples/Embedded/index.js new file mode 100644 index 000000000..b95576135 --- /dev/null +++ b/Examples/Embedded/index.js @@ -0,0 +1,33 @@ +import { WASI, File, OpenFile, ConsoleStdout, PreopenDirectory } from 'https://esm.run/@bjorn3/browser_wasi_shim@0.3.0'; + +async function main(configuration = "release") { + // Fetch our Wasm File + const response = await fetch(`./.build/${configuration}/EmbeddedApp.wasm`); + // Create a new WASI system instance + const wasi = new WASI(/* args */["main.wasm"], /* env */[], /* fd */[ + new OpenFile(new File([])), // stdin + ConsoleStdout.lineBuffered((stdout) => { + console.log(stdout); + }), + ConsoleStdout.lineBuffered((stderr) => { + console.error(stderr); + }), + new PreopenDirectory("/", new Map()), + ]) + const { SwiftRuntime } = await import(`./_Runtime/index.mjs`); + // Create a new Swift Runtime instance to interact with JS and Swift + const swift = new SwiftRuntime(); + // Instantiate the WebAssembly file + const { instance } = await WebAssembly.instantiateStreaming(response, { + //wasi_snapshot_preview1: wasi.wasiImport, + javascript_kit: swift.wasmImports, + }); + // Set the WebAssembly instance to the Swift Runtime + swift.setInstance(instance); + // Start the WebAssembly WASI reactor instance + wasi.initialize(instance); + // Start Swift main function + swift.main() +}; + +main(); diff --git a/Package.swift b/Package.swift index fb7e5b4e5..fd9e84e36 100644 --- a/Package.swift +++ b/Package.swift @@ -1,6 +1,10 @@ // swift-tools-version:5.7 import PackageDescription +import Foundation + +// NOTE: needed for embedded customizations, ideally this will not be necessary at all in the future, or can be replaced with traits +let shouldBuildForEmbedded = ProcessInfo.processInfo.environment["EXPERIMENTAL_EMBEDDED_WASM"].flatMap(Bool.init) ?? false let package = Package( name: "JavaScriptKit", @@ -13,9 +17,11 @@ let package = Package( targets: [ .target( name: "JavaScriptKit", - dependencies: ["_CJavaScriptKit"], - //LES: TODO - make this conditional - // resources: [.copy("Runtime")] + dependencies: ["_CJavaScriptKit"], + resources: shouldBuildForEmbedded ? [] : [.copy("Runtime")], + swiftSettings: shouldBuildForEmbedded + ? [.unsafeFlags(["-Xfrontend", "-emit-empty-object-file"])] + : [] ), .target(name: "_CJavaScriptKit"), .target( diff --git a/Sources/JavaScriptKit/Features.swift b/Sources/JavaScriptKit/Features.swift index e479003c5..81bf6f9cf 100644 --- a/Sources/JavaScriptKit/Features.swift +++ b/Sources/JavaScriptKit/Features.swift @@ -2,8 +2,8 @@ enum LibraryFeatures { static let weakRefs: Int32 = 1 << 0 } -@_cdecl("_library_features") -func _library_features() -> Int32 { +@_expose(wasm, "swjs_library_features") +public func _library_features() -> Int32 { var features: Int32 = 0 #if !JAVASCRIPTKIT_WITHOUT_WEAKREFS features |= LibraryFeatures.weakRefs diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index a7a93ba75..c0a7c7885 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -186,8 +186,8 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa // └─────────────────────┴──────────────────────────┘ /// Returns true if the host function has been already released, otherwise false. -@_cdecl("_call_host_function_impl") -func _call_host_function_impl( +@_expose(wasm, "swjs_call_host_function") +public func _call_host_function_impl( _ hostFuncRef: JavaScriptHostFuncRef, _ argv: UnsafePointer, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef @@ -217,8 +217,8 @@ extension JSClosure { } } -@_cdecl("_free_host_function_impl") -func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {} +@_expose(wasm, "swjs_free_host_function") +public func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {} #else @@ -229,8 +229,8 @@ extension JSClosure { } -@_cdecl("_free_host_function_impl") -func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { +@_expose(wasm, "swjs_free_host_function") +public func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { JSClosure.sharedClosures[hostFuncRef] = nil } #endif diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index f918687f3..ae5a4aa49 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -258,6 +258,6 @@ private func valueKindAndFlagsFromBits(_ bits: UInt32) -> JavaScriptValueKindAnd #if hasFeature(Embedded) JavaScriptValueKindAndFlags(bitPattern: bits) #else - unsafeBitCast(resultBitPattern, to: JavaScriptValueKindAndFlags.self) + unsafeBitCast(bits, to: JavaScriptValueKindAndFlags.self) #endif } \ No newline at end of file diff --git a/Sources/JavaScriptKit/JSValue.swift b/Sources/JavaScriptKit/JSValue.swift index 5166986ff..fe1400e24 100644 --- a/Sources/JavaScriptKit/JSValue.swift +++ b/Sources/JavaScriptKit/JSValue.swift @@ -270,3 +270,22 @@ extension JSValue: CustomStringConvertible { JSObject.global.String.function!(self).string! } } + +#if hasFeature(Embedded) +public extension JSValue { + @_disfavoredOverload + subscript(dynamicMember name: String) -> (() -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + subscript(dynamicMember name: String) -> ((A0) -> JSValue) { + object![dynamicMember: name]! + } + + @_disfavoredOverload + subscript(dynamicMember name: String) -> ((A0, A1) -> JSValue) { + object![dynamicMember: name]! + } +} +#endif \ No newline at end of file diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index c658bd545..934b4639b 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -14,24 +14,6 @@ extern void *memcpy (void *__restrict, const void *__restrict, size_t); #endif -bool _call_host_function_impl(const JavaScriptHostFuncRef host_func_ref, - const RawJSValue *argv, const int argc, - const JavaScriptObjectRef callback_func); - -__attribute__((export_name("swjs_call_host_function"))) -bool swjs_call_host_function(const JavaScriptHostFuncRef host_func_ref, - const RawJSValue *argv, const int argc, - const JavaScriptObjectRef callback_func) { - return _call_host_function_impl(host_func_ref, argv, argc, callback_func); -} - -void _free_host_function_impl(const JavaScriptHostFuncRef host_func_ref); - -__attribute__((export_name("swjs_free_host_function"))) -void swjs_free_host_function(const JavaScriptHostFuncRef host_func_ref) { - _free_host_function_impl(host_func_ref); -} - __attribute__((export_name("swjs_prepare_host_function_call"))) void *swjs_prepare_host_function_call(const int argc) { return malloc(argc * sizeof(RawJSValue)); @@ -50,13 +32,6 @@ int swjs_library_version(void) { return 708; } -int _library_features(void); - -__attribute__((export_name("swjs_library_features"))) -int swjs_library_features(void) { - return _library_features(); -} - #endif _Thread_local void *swjs_thread_local_closures; diff --git a/swift-build-embedded b/swift-build-embedded deleted file mode 100755 index 247658466..000000000 --- a/swift-build-embedded +++ /dev/null @@ -1,34 +0,0 @@ -swift build --target JavaScriptKit \ - --triple wasm32-unknown-none-wasm \ - -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ - -Xswiftc -enable-experimental-feature -Xswiftc Extern \ - -Xswiftc -wmo -Xswiftc -disable-cmo \ - -Xswiftc -Xfrontend -Xswiftc -gnone \ - -Xswiftc -Xfrontend -Xswiftc -disable-stack-protector \ - -Xswiftc -cxx-interoperability-mode=default \ - -Xcc -D__Embedded -Xcc -fdeclspec \ - -Xlinker --export-if-defined=__main_argc_argv \ - -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor - - - # -Xlinker --export-if-defined=_initialize - # -Xswiftc -static-stdlib \ - - # --verbose - - # -Xswiftc -Xclang-linker -Xswiftc -nostdlib \ - -####### -# swift build -c release --product ExampleApp \ -# --sdk DEVELOPMENT-SNAPSHOT-2024-09-20-a-wasm32-unknown-wasi \ -# --triple wasm32-unknown-none-wasm \ -# -Xcc -D__Embedded \ -# -Xswiftc -enable-experimental-feature -Xswiftc Embedded \ -# -Xswiftc -enable-experimental-feature -Xswiftc Extern \ - # -Xswiftc -wmo -Xswiftc -disable-cmo \ - # -Xcc -fdeclspec \ - # -Xswiftc -Xclang-linker -Xswiftc -mexec-model=reactor \ - # -Xlinker --export-if-defined=__main_argc_argv \ - - -#cp .build/release/ExampleApp.wasm ./TestPage/app.wasm \ No newline at end of file From b01062425ba835744f247ca162a56242c4bc26f2 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Tue, 8 Oct 2024 13:02:37 +0200 Subject: [PATCH 4/9] ...memmove, we meet again --- .../EmbeddedApp/_thingsThatShouldNotBeNeeded.swift | 11 +++++++++++ Examples/Embedded/Sources/EmbeddedApp/main.swift | 11 +++++++---- 2 files changed, 18 insertions(+), 4 deletions(-) diff --git a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift index 50d838b96..e97965f66 100644 --- a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift +++ b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift @@ -16,3 +16,14 @@ func strlen(_ s: UnsafePointer) -> Int { } return p - s } + +// TODO: why do I need this? and surely this is not ideal... figure this out, or at least have this come from a C lib +@_cdecl("memmove") +func memmove(_ dest: UnsafeMutableRawPointer, _ src: UnsafeRawPointer, _ n: Int) -> UnsafeMutableRawPointer { + let d = dest.assumingMemoryBound(to: UInt8.self) + let s = src.assumingMemoryBound(to: UInt8.self) + for i in 0.. Date: Tue, 8 Oct 2024 17:37:22 +0200 Subject: [PATCH 5/9] readme --- Examples/Embedded/README.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Examples/Embedded/README.md b/Examples/Embedded/README.md index 92dd6be40..2f388fcdc 100644 --- a/Examples/Embedded/README.md +++ b/Examples/Embedded/README.md @@ -1,5 +1,7 @@ # Embedded example +Requires a recent DEVELOPMENT-SNAPSHOT toolchain. (tested with swift-DEVELOPMENT-SNAPSHOT-2024-09-25-a) + ```sh $ ./build.sh $ npx serve From 025b9fc8218c1c539d137d9a287523e6e9e97512 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Tue, 8 Oct 2024 22:29:12 +0200 Subject: [PATCH 6/9] revert to cdecl-based exports for swift 5.10 compatibility --- Examples/Basic/Package.swift | 3 ++ .../_thingsThatShouldNotBeNeeded.swift | 6 +-- Sources/JavaScriptKit/Features.swift | 10 ++++- .../FundamentalObjects/JSClosure.swift | 30 ++++++++++++--- Sources/_CJavaScriptKit/_CJavaScriptKit.c | 38 ++++++++++++++++--- 5 files changed, 70 insertions(+), 17 deletions(-) diff --git a/Examples/Basic/Package.swift b/Examples/Basic/Package.swift index 6484043f5..aade23359 100644 --- a/Examples/Basic/Package.swift +++ b/Examples/Basic/Package.swift @@ -4,6 +4,9 @@ import PackageDescription let package = Package( name: "Basic", + platforms: [ + .macOS(.v14) + ], dependencies: [.package(name: "JavaScriptKit", path: "../../")], targets: [ .executableTarget( diff --git a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift index e97965f66..20a26e085 100644 --- a/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift +++ b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift @@ -2,9 +2,9 @@ import JavaScriptKit // NOTE: it seems the embedded tree shaker gets rid of these exports if they are not used somewhere func _i_need_to_be_here_for_wasm_exports_to_work() { - _ = _library_features - _ = _call_host_function_impl - _ = _free_host_function_impl + _ = _swjs_library_features + _ = _swjs_call_host_function + _ = _swjs_free_host_function } // TODO: why do I need this? and surely this is not ideal... figure this out, or at least have this come from a C lib diff --git a/Sources/JavaScriptKit/Features.swift b/Sources/JavaScriptKit/Features.swift index 81bf6f9cf..112f3e943 100644 --- a/Sources/JavaScriptKit/Features.swift +++ b/Sources/JavaScriptKit/Features.swift @@ -2,11 +2,17 @@ enum LibraryFeatures { static let weakRefs: Int32 = 1 << 0 } -@_expose(wasm, "swjs_library_features") -public func _library_features() -> Int32 { +@_cdecl("_library_features") +func _library_features() -> Int32 { var features: Int32 = 0 #if !JAVASCRIPTKIT_WITHOUT_WEAKREFS features |= LibraryFeatures.weakRefs #endif return features } + +#if hasFeature(Embedded) +// cdecls currently don't work in embedded, and expose for wasm only works >=6.0 +@_expose(wasm, "swjs_library_features") +public func _swjs_library_features() -> Int32 { _library_features() } +#endif \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index c0a7c7885..1686f864a 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -186,8 +186,8 @@ private func makeAsyncClosure(_ body: @escaping ([JSValue]) async throws -> JSVa // └─────────────────────┴──────────────────────────┘ /// Returns true if the host function has been already released, otherwise false. -@_expose(wasm, "swjs_call_host_function") -public func _call_host_function_impl( +@_cdecl("_call_host_function_impl") +func _call_host_function_impl( _ hostFuncRef: JavaScriptHostFuncRef, _ argv: UnsafePointer, _ argc: Int32, _ callbackFuncRef: JavaScriptObjectRef @@ -217,8 +217,9 @@ extension JSClosure { } } -@_expose(wasm, "swjs_free_host_function") -public func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {} + +@_cdecl("_free_host_function_impl") +func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {} #else @@ -229,8 +230,25 @@ extension JSClosure { } -@_expose(wasm, "swjs_free_host_function") -public func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { +@_cdecl("_free_host_function_impl") +func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { JSClosure.sharedClosures[hostFuncRef] = nil } #endif + +#if hasFeature(Embedded) +// cdecls currently don't work in embedded, and expose for wasm only works >=6.0 +@_expose(wasm, "swjs_call_host_function") +public func _swjs_call_host_function( + _ hostFuncRef: JavaScriptHostFuncRef, + _ argv: UnsafePointer, _ argc: Int32, + _ callbackFuncRef: JavaScriptObjectRef) -> Bool { + + _call_host_function_impl(hostFuncRef, argv, argc, callbackFuncRef) +} + +@_expose(wasm, "swjs_free_host_function") +public func _swjs_free_host_function(_ hostFuncRef: JavaScriptHostFuncRef) { + _free_host_function_impl(hostFuncRef) +} +#endif \ No newline at end of file diff --git a/Sources/_CJavaScriptKit/_CJavaScriptKit.c b/Sources/_CJavaScriptKit/_CJavaScriptKit.c index 934b4639b..6fc3fa916 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -13,6 +13,13 @@ extern void *memcpy (void *__restrict, const void *__restrict, size_t); #include #endif +/// The compatibility runtime library version. +/// Notes: If you change any interface of runtime library, please increment +/// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`. +__attribute__((export_name("swjs_library_version"))) +int swjs_library_version(void) { + return 708; +} __attribute__((export_name("swjs_prepare_host_function_call"))) void *swjs_prepare_host_function_call(const int argc) { @@ -24,14 +31,33 @@ void swjs_cleanup_host_function_call(void *argv_buffer) { free(argv_buffer); } -/// The compatibility runtime library version. -/// Notes: If you change any interface of runtime library, please increment -/// this and `SwiftRuntime.version` in `./Runtime/src/index.ts`. -__attribute__((export_name("swjs_library_version"))) -int swjs_library_version(void) { - return 708; +#ifndef __Embedded +// cdecls don't work in Embedded, also @_expose(wasm) can be used with Swift >=6.0 +bool _call_host_function_impl(const JavaScriptHostFuncRef host_func_ref, + const RawJSValue *argv, const int argc, + const JavaScriptObjectRef callback_func); + +__attribute__((export_name("swjs_call_host_function"))) +bool swjs_call_host_function(const JavaScriptHostFuncRef host_func_ref, + const RawJSValue *argv, const int argc, + const JavaScriptObjectRef callback_func) { + return _call_host_function_impl(host_func_ref, argv, argc, callback_func); +} + +void _free_host_function_impl(const JavaScriptHostFuncRef host_func_ref); + +__attribute__((export_name("swjs_free_host_function"))) +void swjs_free_host_function(const JavaScriptHostFuncRef host_func_ref) { + _free_host_function_impl(host_func_ref); } +int _library_features(void); + +__attribute__((export_name("swjs_library_features"))) +int swjs_library_features(void) { + return _library_features(); +} +#endif #endif _Thread_local void *swjs_thread_local_closures; From 77bbc8c3eed1b639614c8cf3bde3d484a36b655c Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 9 Oct 2024 10:10:41 +0200 Subject: [PATCH 7/9] added compiler gates for @_expose --- Sources/JavaScriptKit/Features.swift | 2 +- Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/Sources/JavaScriptKit/Features.swift b/Sources/JavaScriptKit/Features.swift index 112f3e943..db6e00f26 100644 --- a/Sources/JavaScriptKit/Features.swift +++ b/Sources/JavaScriptKit/Features.swift @@ -11,7 +11,7 @@ func _library_features() -> Int32 { return features } -#if hasFeature(Embedded) +#if compiler(>=6.0) && hasFeature(Embedded) // cdecls currently don't work in embedded, and expose for wasm only works >=6.0 @_expose(wasm, "swjs_library_features") public func _swjs_library_features() -> Int32 { _library_features() } diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 1686f864a..5d367ba38 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift @@ -236,7 +236,7 @@ func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { } #endif -#if hasFeature(Embedded) +#if compiler(>=6.0) && hasFeature(Embedded) // cdecls currently don't work in embedded, and expose for wasm only works >=6.0 @_expose(wasm, "swjs_call_host_function") public func _swjs_call_host_function( From 7f2f7c667243724d34d8bc6733ed05f62f70eea9 Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 9 Oct 2024 13:32:37 +0200 Subject: [PATCH 8/9] less embedded conditions and added comments --- .../FundamentalObjects/JSFunction.swift | 12 +++++++----- .../JavaScriptKit/FundamentalObjects/JSObject.swift | 4 ++++ .../JavaScriptKit/FundamentalObjects/JSString.swift | 5 +---- .../JavaScriptKit/FundamentalObjects/JSSymbol.swift | 4 ---- 4 files changed, 12 insertions(+), 13 deletions(-) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index ae5a4aa49..443063981 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift @@ -84,8 +84,8 @@ public class JSFunction: JSObject, _JSFunctionProtocol { public var `throws`: JSThrowingFunction { JSThrowingFunction(self) } +#endif -#else @discardableResult public func callAsFunction(arguments: [JSValue]) -> JSValue { invokeNonThrowingJSFunction(arguments: arguments).jsValue @@ -98,7 +98,6 @@ public class JSFunction: JSObject, _JSFunctionProtocol { } } } -#endif @available(*, unavailable, message: "Please use JSClosure instead") public static func from(_: @escaping ([JSValue]) -> JSValue) -> JSFunction { @@ -109,7 +108,6 @@ public class JSFunction: JSObject, _JSFunctionProtocol { .function(self) } -#if hasFeature(Embedded) final func invokeNonThrowingJSFunction(arguments: [JSValue]) -> RawJSValue { arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } } @@ -117,7 +115,8 @@ public class JSFunction: JSObject, _JSFunctionProtocol { final func invokeNonThrowingJSFunction(arguments: [JSValue], this: JSObject) -> RawJSValue { arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0, this: this) } } -#else + +#if !hasFeature(Embedded) final func invokeNonThrowingJSFunction(arguments: [ConvertibleToJSValue]) -> RawJSValue { arguments.withRawJSValues { invokeNonThrowingJSFunction(rawValues: $0) } } @@ -164,11 +163,14 @@ public class JSFunction: JSObject, _JSFunctionProtocol { } } +/// Internal protocol to support generic arguments for `JSFunction`. +/// +/// In Swift Embedded, non-final classes cannot have generic methods. public protocol _JSFunctionProtocol: JSFunction {} #if hasFeature(Embedded) +// NOTE: once embedded supports variadic generics, we can remove these overloads public extension _JSFunctionProtocol { - // hand-made "varidacs" for Embedded @discardableResult func callAsFunction(this: JSObject) -> JSValue { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 3891520c8..6d8442540 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift @@ -231,10 +231,14 @@ public class JSThrowingObject { } #endif +/// Internal protocol to support generic arguments for `JSObject`. +/// +/// In Swift Embedded, non-final classes cannot have generic methods. public protocol _JSObjectProtocol: JSObject { } #if hasFeature(Embedded) +// NOTE: once embedded supports variadic generics, we can remove these overloads public extension _JSObjectProtocol { @_disfavoredOverload subscript(dynamicMember name: String) -> (() -> JSValue)? { diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift index 2eec4ef42..686d1ba11 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -31,12 +31,9 @@ public struct JSString: LosslessStringConvertible, Equatable { var bytesRef: JavaScriptObjectRef = 0 let bytesLength = Int(swjs_encode_string(jsRef, &bytesRef)) // +1 for null terminator - // TODO: revert this back to malloc and free - // let buffer = malloc(Int(bytesLength + 1))!.assumingMemoryBound(to: UInt8.self) - let buffer = UnsafeMutablePointer.allocate(capacity: Int(bytesLength + 1)) + let buffer = UnsafeMutablePointer.allocate(capacity: bytesLength + 1) defer { buffer.deallocate() - // free(buffer) swjs_release(bytesRef) } swjs_load_string(bytesRef, buffer) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index e7d1a2fd2..d768b6675 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -10,11 +10,7 @@ public class JSSymbol: JSObject { public init(_ description: JSString) { // can’t do `self =` so we have to get the ID manually - #if hasFeature(Embedded) let result = Symbol.invokeNonThrowingJSFunction(arguments: [description.jsValue]) - #else - let result = Symbol.invokeNonThrowingJSFunction(arguments: [description]) - #endif precondition(result.kind == .symbol) super.init(id: UInt32(result.payload1)) } From 69c58dda72f4c1161f06012969819023b033f22b Mon Sep 17 00:00:00 2001 From: Simon Leeb <52261246+sliemeobn@users.noreply.github.com> Date: Wed, 9 Oct 2024 14:41:44 +0200 Subject: [PATCH 9/9] added explicit returns for 5.8 compatibility --- Sources/JavaScriptKit/BasicObjects/JSTimer.swift | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift index a5d7c5b8e..d2eee6fcc 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift @@ -17,10 +17,8 @@ public final class JSTimer { var jsValue: JSValue { switch self { - case .oneshot(let closure): - closure.jsValue - case .repeating(let closure): - closure.jsValue + case .oneshot(let closure): return closure.jsValue + case .repeating(let closure): return closure.jsValue } }