javascript-kit-swift
Version:
A runtime library of JavaScriptKit which is Swift framework to interact with JavaScript through WebAssembly.
580 lines (569 loc) • 27.3 kB
JavaScript
(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 });
}));