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/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..2f388fcdc --- /dev/null +++ b/Examples/Embedded/README.md @@ -0,0 +1,8 @@ +# Embedded example + +Requires a recent DEVELOPMENT-SNAPSHOT toolchain. (tested with swift-DEVELOPMENT-SNAPSHOT-2024-09-25-a) + +```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..20a26e085 --- /dev/null +++ b/Examples/Embedded/Sources/EmbeddedApp/_thingsThatShouldNotBeNeeded.swift @@ -0,0 +1,29 @@ +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() { + _ = _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 +@_cdecl("strlen") +func strlen(_ s: UnsafePointer) -> Int { + var p = s + while p.pointee != 0 { + p += 1 + } + 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.. { + 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 aa529c772..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,8 +17,11 @@ let package = Package( targets: [ .target( name: "JavaScriptKit", - dependencies: ["_CJavaScriptKit"], - 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/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..d2eee6fcc 100644 --- a/Sources/JavaScriptKit/BasicObjects/JSTimer.swift +++ b/Sources/JavaScriptKit/BasicObjects/JSTimer.swift @@ -11,10 +11,31 @@ 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): return closure.jsValue + case .repeating(let closure): return 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 +56,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/Features.swift b/Sources/JavaScriptKit/Features.swift index e479003c5..db6e00f26 100644 --- a/Sources/JavaScriptKit/Features.swift +++ b/Sources/JavaScriptKit/Features.swift @@ -10,3 +10,9 @@ func _library_features() -> Int32 { #endif return features } + +#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() } +#endif \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift b/Sources/JavaScriptKit/FundamentalObjects/JSClosure.swift index 80fa2cf94..5d367ba38 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) @@ -217,6 +217,7 @@ extension JSClosure { } } + @_cdecl("_free_host_function_impl") func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) {} @@ -234,3 +235,20 @@ func _free_host_function_impl(_ hostFuncRef: JavaScriptHostFuncRef) { JSClosure.sharedClosures[hostFuncRef] = nil } #endif + +#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( + _ 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/JavaScriptKit/FundamentalObjects/JSFunction.swift b/Sources/JavaScriptKit/FundamentalObjects/JSFunction.swift index 543146133..443063981 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 /// /// @@ -78,10 +84,19 @@ public class JSFunction: JSObject { public var `throws`: JSThrowingFunction { JSThrowingFunction(self) } +#endif - /// A variadic arguments version of `new`. - public func new(_ arguments: ConvertibleToJSValue...) -> JSObject { - new(arguments: arguments) + @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))) + } + } } @available(*, unavailable, message: "Please use JSClosure instead") @@ -93,43 +108,158 @@ public class JSFunction: JSObject { .function(self) } + 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) } + } + +#if !hasFeature(Embedded) 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 = valueKindAndFlagsFromBits(resultBitPattern) + assert(!kindAndFlags.isException) + 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 = valueKindAndFlagsFromBits(resultBitPattern) + #if !hasFeature(Embedded) + assert(!kindAndFlags.isException) + #endif + let result = RawJSValue(kind: kindAndFlags.kind, payload1: payload1, payload2: payload2) + return result } } } + +/// 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 { + + @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]) + } +} + +// 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(bits, to: JavaScriptValueKindAndFlags.self) + #endif +} \ No newline at end of file diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift b/Sources/JavaScriptKit/FundamentalObjects/JSObject.swift index 861758497..6d8442540 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,36 @@ public class JSThrowingObject { self[name] } } +#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)? { + 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..686d1ba11 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSString.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSString.swift @@ -31,9 +31,9 @@ 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) + let buffer = UnsafeMutablePointer.allocate(capacity: bytesLength + 1) defer { - free(buffer) + buffer.deallocate() swjs_release(bytesRef) } swjs_load_string(bytesRef, buffer) diff --git a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift index f5d194e25..d768b6675 100644 --- a/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift +++ b/Sources/JavaScriptKit/FundamentalObjects/JSSymbol.swift @@ -10,7 +10,7 @@ public class JSSymbol: JSObject { public init(_ description: JSString) { // can’t do `self =` so we have to get the ID manually - let result = Symbol.invokeNonThrowingJSFunction(arguments: [description]) + let result = Symbol.invokeNonThrowingJSFunction(arguments: [description.jsValue]) 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..fe1400e24 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. @@ -268,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/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..6fc3fa916 100644 --- a/Sources/_CJavaScriptKit/_CJavaScriptKit.c +++ b/Sources/_CJavaScriptKit/_CJavaScriptKit.c @@ -1,9 +1,38 @@ #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 +/// 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) { + return malloc(argc * sizeof(RawJSValue)); +} + +__attribute__((export_name("swjs_cleanup_host_function_call"))) +void swjs_cleanup_host_function_call(void *argv_buffer) { + free(argv_buffer); +} +#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); @@ -22,31 +51,13 @@ 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)); -} - -__attribute__((export_name("swjs_cleanup_host_function_call"))) -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; -} - 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; diff --git a/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h b/Sources/_CJavaScriptKit/include/_CJavaScriptKit.h index b8ef2b7b0..8daf7cdc6 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 @@ -25,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;