UNPKG

@bytecodealliance/jco

Version:

JavaScript tooling for working with WebAssembly Components

1,633 lines (1,339 loc) 380 kB
"use components"; import { environment, exit as exit$1, stderr, stdin, stdout, terminalInput, terminalOutput, terminalStderr, terminalStdin, terminalStdout } from '@bytecodealliance/preview2-shim/cli'; import { preopens, types } from '@bytecodealliance/preview2-shim/filesystem'; import { error, streams } from '@bytecodealliance/preview2-shim/io'; import { random } from '@bytecodealliance/preview2-shim/random'; const { getEnvironment } = environment; if (getEnvironment=== undefined) { const err = new Error("unexpectedly undefined local import 'getEnvironment', was 'getEnvironment' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } getEnvironment._isHostProvided = true; const { exit } = exit$1; if (exit=== undefined) { const err = new Error("unexpectedly undefined local import 'exit', was 'exit' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } exit._isHostProvided = true; const { getStderr } = stderr; if (getStderr=== undefined) { const err = new Error("unexpectedly undefined local import 'getStderr', was 'getStderr' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } getStderr._isHostProvided = true; const { getStdin } = stdin; if (getStdin=== undefined) { const err = new Error("unexpectedly undefined local import 'getStdin', was 'getStdin' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } getStdin._isHostProvided = true; const { getStdout } = stdout; if (getStdout=== undefined) { const err = new Error("unexpectedly undefined local import 'getStdout', was 'getStdout' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } getStdout._isHostProvided = true; const { TerminalInput } = terminalInput; if (TerminalInput=== undefined) { const err = new Error("unexpectedly undefined local import 'TerminalInput', was 'TerminalInput' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } TerminalInput._isHostProvided = true; const { TerminalOutput } = terminalOutput; if (TerminalOutput=== undefined) { const err = new Error("unexpectedly undefined local import 'TerminalOutput', was 'TerminalOutput' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } TerminalOutput._isHostProvided = true; const { getTerminalStderr } = terminalStderr; if (getTerminalStderr=== undefined) { const err = new Error("unexpectedly undefined local import 'getTerminalStderr', was 'getTerminalStderr' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } getTerminalStderr._isHostProvided = true; const { getTerminalStdin } = terminalStdin; if (getTerminalStdin=== undefined) { const err = new Error("unexpectedly undefined local import 'getTerminalStdin', was 'getTerminalStdin' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } getTerminalStdin._isHostProvided = true; const { getTerminalStdout } = terminalStdout; if (getTerminalStdout=== undefined) { const err = new Error("unexpectedly undefined local import 'getTerminalStdout', was 'getTerminalStdout' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } getTerminalStdout._isHostProvided = true; const { getDirectories } = preopens; if (getDirectories=== undefined) { const err = new Error("unexpectedly undefined local import 'getDirectories', was 'getDirectories' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } getDirectories._isHostProvided = true; const { Descriptor, DirectoryEntryStream, filesystemErrorCode } = types; if (Descriptor=== undefined) { const err = new Error("unexpectedly undefined local import 'Descriptor', was 'Descriptor' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } Descriptor._isHostProvided = true; if (DirectoryEntryStream=== undefined) { const err = new Error("unexpectedly undefined local import 'DirectoryEntryStream', was 'DirectoryEntryStream' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } DirectoryEntryStream._isHostProvided = true; if (filesystemErrorCode=== undefined) { const err = new Error("unexpectedly undefined local import 'filesystemErrorCode', was 'filesystemErrorCode' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } filesystemErrorCode._isHostProvided = true; const { Error: Error$1 } = error; if (Error$1=== undefined) { const err = new Error("unexpectedly undefined local import 'Error$1', was 'Error' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } Error$1._isHostProvided = true; const { InputStream, OutputStream } = streams; if (InputStream=== undefined) { const err = new Error("unexpectedly undefined local import 'InputStream', was 'InputStream' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } InputStream._isHostProvided = true; if (OutputStream=== undefined) { const err = new Error("unexpectedly undefined local import 'OutputStream', was 'OutputStream' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } OutputStream._isHostProvided = true; const { getRandomBytes } = random; if (getRandomBytes=== undefined) { const err = new Error("unexpectedly undefined local import 'getRandomBytes', was 'getRandomBytes' available at instantiation?"); console.error("ERROR:", err.toString()); throw err; } getRandomBytes._isHostProvided = true; function promiseWithResolvers() { if (Promise.withResolvers) { return Promise.withResolvers(); } else { let resolve; let reject; const promise = new Promise((res, rej) => { resolve = res; reject = rej; }); return { promise, resolve, reject }; } } const symbolDispose = Symbol.dispose || Symbol.for('dispose'); const symbolAsyncIterator = Symbol.asyncIterator; const symbolIterator = Symbol.iterator; const _debugLog = (...args) => { if (!globalThis?.process?.env?.JCO_DEBUG) { return; } console.debug(...args); }; const ASYNC_DETERMINISM = 'random'; const GLOBAL_COMPONENT_MEMORY_MAP = new Map(); const CURRENT_TASK_META = {}; function _getGlobalCurrentTaskMeta(componentIdx) { const v = CURRENT_TASK_META[componentIdx]; if (v === undefined) { return v; } return { ...v }; } function _setGlobalCurrentTaskMeta(args) { if (!args) { throw new TypeError('args missing'); } if (args.taskID === undefined) { throw new TypeError('missing task ID'); } if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); } const { taskID, componentIdx } = args; return CURRENT_TASK_META[componentIdx] = { taskID, componentIdx }; } function _withGlobalCurrentTaskMeta(args) { _debugLog('[_withGlobalCurrentTaskMeta()] args', args); if (!args) { throw new TypeError('args missing'); } if (args.taskID === undefined) { throw new TypeError('missing task ID'); } if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); } if (!args.fn) { throw new TypeError('missing fn'); } const { taskID, componentIdx, fn } = args; try { CURRENT_TASK_META[componentIdx] = { taskID, componentIdx }; return fn(); } catch (err) { _debugLog("error while executing sync callee/callback", { ...args, err, }); throw err; } finally { CURRENT_TASK_META[componentIdx] = null; } } async function _withGlobalCurrentTaskMetaAsync(args) { _debugLog('[_withGlobalCurrentTaskMetaAsync()] args', args); if (!args) { throw new TypeError('args missing'); } if (args.taskID === undefined) { throw new TypeError('missing task ID'); } if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); } if (!args.fn) { throw new TypeError('missing fn'); } const { taskID, componentIdx, fn } = args; // If there is already an async task executing, we must wait for it // to complete before we can can run the closure we were given // let current = CURRENT_TASK_META[componentIdx]; let cstate; if (current && current.taskID !== taskID) { cstate = getOrCreateAsyncState(componentIdx); while (current && current.taskID !== taskID) { const { promise, resolve } = Promise.withResolvers(); cstate.onNextExclusiveRelease(resolve); await promise; current = CURRENT_TASK_META[componentIdx]; } // Since we've just waited for the component to not be locked, re-lock // exclusivity so we can run the fn below (likely a callee/callback) cstate.exclusiveLock(); } try { CURRENT_TASK_META[componentIdx] = { taskID, componentIdx }; return await fn(); } catch (err) { _debugLog("error while executing async callee/callback", { ...args, err, }); throw err; } finally { CURRENT_TASK_META[componentIdx] = null; } } async function _clearCurrentTask(args) { _debugLog('[_clearCurrentTask()] args', args); if (!args) { throw new TypeError('args missing'); } if (args.taskID === undefined) { throw new TypeError('missing task ID'); } if (args.componentIdx === undefined) { throw new TypeError('missing component idx'); } const { taskID, componentIdx } = args; const meta = CURRENT_TASK_META[componentIdx]; if (!meta) { throw new Error(`missing current task meta for component idx [${componentIdx}]n`); } if (meta.taskID !== taskID) { throw new Error(`task ID [${meta.taskID}] != requested ID [${taskID}]`); } if (meta.componentIdx !== componentIdx) { throw new Error(`component idx [${meta.componentIdx}] != requested idx [${componentIdx}]`); } CURRENT_TASK_META[componentIdx] = null; } function lookupMemoriesForComponent(args) { const { componentIdx } = args ?? {}; if (args.componentIdx === undefined) { throw new TypeError("missing component idx"); } const metas = GLOBAL_COMPONENT_MEMORY_MAP.get(componentIdx); if (!metas) { return []; } if (args.memoryIdx === undefined) { return Object.values(metas); } const meta = metas[args.memoryIdx]; return meta?.memory; } function registerGlobalMemoryForComponent(args) { const { componentIdx, memory, memoryIdx } = args ?? {}; if (componentIdx === undefined) { throw new TypeError('missing component idx'); } if (memory === undefined && memoryIdx === undefined) { throw new TypeError('missing both memory & memory idx'); } let inner = GLOBAL_COMPONENT_MEMORY_MAP.get(componentIdx); if (!inner) { inner = {}; GLOBAL_COMPONENT_MEMORY_MAP.set(componentIdx, inner); } inner[memoryIdx] = { memory, memoryIdx, componentIdx }; } class RepTable { #data = [0, null]; #target; constructor(args) { this.target = args?.target; } data() { return this.#data; } insert(val) { _debugLog('[RepTable#insert()] args', { val, target: this.target }); const freeIdx = this.#data[0]; if (freeIdx === 0) { this.#data.push(val); this.#data.push(null); const rep = (this.#data.length >> 1) - 1; _debugLog('[RepTable#insert()] inserted', { val, target: this.target, rep }); return rep; } this.#data[0] = this.#data[freeIdx << 1]; const placementIdx = freeIdx << 1; this.#data[placementIdx] = val; this.#data[placementIdx + 1] = null; _debugLog('[RepTable#insert()] inserted', { val, target: this.target, rep: freeIdx }); return freeIdx; } get(rep) { _debugLog('[RepTable#get()] args', { rep, target: this.target }); if (rep === 0) { throw new Error('invalid resource rep during get, (cannot be 0)'); } const baseIdx = rep << 1; const val = this.#data[baseIdx]; return val; } contains(rep) { _debugLog('[RepTable#contains()] args', { rep, target: this.target }); if (rep === 0) { throw new Error('invalid resource rep during contains, (cannot be 0)'); } const baseIdx = rep << 1; return !!this.#data[baseIdx]; } remove(rep) { _debugLog('[RepTable#remove()] args', { rep, target: this.target }); if (rep === 0) { throw new Error('invalid resource rep during remove, (cannot be 0)'); } if (this.#data.length === 2) { throw new Error('invalid'); } const baseIdx = rep << 1; const val = this.#data[baseIdx]; this.#data[baseIdx] = this.#data[0]; this.#data[0] = rep; return val; } clear() { _debugLog('[RepTable#clear()] args', { rep, target: this.target }); this.#data = [0, null]; } } const _coinFlip = () => { return Math.random() > 0.5; }; let SCOPE_ID = 0; const I32_MIN = -2_147_483_648; const I32_MAX = 2_147_483_647; function _isValidNumericPrimitive(ty, v) { if (v === undefined || v === null) { return false; } switch (ty) { case 'bool': return v === 0 || v === 1; break; case 'u8': return v >= 0 && v <= 255; break; case 's8': return v >= -128 && v <= 127; break; case 'u16': return v >= 0 && v <= 65535; break; case 's16': return v >= -32768 && v <= 32767; case 'u32': return v >= 0 && v <= 4_294_967_295; case 's32': return v >= -2_147_483_648 && v <= 2_147_483_647; case 'u64': return typeof v === 'bigint' && v >= 0 && v <= 18_446_744_073_709_551_615n; case 's64': return typeof v === 'bigint' && v >= -9223372036854775808n && v <= 9223372036854775807n; break; case 'f32': case 'f64': return typeof v === 'number'; default: return false; } return true; } function _requireValidNumericPrimitive(ty, v) { if (v === undefined || v === null || !_isValidNumericPrimitive(ty, v)) { throw new TypeError(`invalid ${ty} value [${v}]`); } return true; } const _typeCheckValidI32 = (n) => typeof n === 'number' && n >= I32_MIN && n <= I32_MAX; const _typeCheckAsyncFn= (f) => { return f instanceof ASYNC_FN_CTOR; }; let RESOURCE_CALL_BORROWS = [];const ASYNC_FN_CTOR = (async () => {}).constructor; function clearCurrentTask(componentIdx, taskID) { _debugLog('[clearCurrentTask()] args', { componentIdx, taskID }); if (componentIdx === undefined || componentIdx === null) { throw new Error('missing/invalid component instance index while ending current task'); } const tasks = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx); if (!tasks || !Array.isArray(tasks)) { throw new Error('missing/invalid tasks for component instance while ending task'); } if (tasks.length == 0) { throw new Error(`no current tasks for component instance [${componentIdx}] while ending task`); } if (taskID !== undefined) { const last = tasks[tasks.length - 1]; if (last.id !== taskID) { // throw new Error('current task does not match expected task ID'); return; } } ASYNC_CURRENT_TASK_IDS.pop(); ASYNC_CURRENT_COMPONENT_IDXS.pop(); const taskMeta = tasks.pop(); return taskMeta.task; } const CURRENT_TASK_MAY_BLOCK = new WebAssembly.Global({ value: 'i32', mutable: true }, 0); const ASYNC_CURRENT_TASK_IDS = []; const ASYNC_CURRENT_COMPONENT_IDXS = []; function unpackCallbackResult(result) { if (!(_typeCheckValidI32(result))) { throw new Error('invalid callback return value [' + result + '], not a valid i32'); } const eventCode = result & 0xF; if (eventCode < 0 || eventCode > 3) { throw new Error('invalid async return value [' + eventCode + '], outside callback code range'); } if (result < 0 || result >= 2**32) { throw new Error('invalid callback result'); } // TODO: table max length check? const waitableSetRep = result >> 4; return [eventCode, waitableSetRep]; } class AsyncSubtask { static _ID = 0n; static State = { STARTING: 0, STARTED: 1, RETURNED: 2, CANCELLED_BEFORE_STARTED: 3, CANCELLED_BEFORE_RETURNED: 4, }; #id; #state = AsyncSubtask.State.STARTING; #componentIdx; #parentTask; #childTask = null; #dropped = false; #cancelRequested = false; #memoryIdx = null; #lenders = null; #waitable = null; #callbackFn = null; #callbackFnName = null; #postReturnFn = null; #onProgressFn = null; #pendingEventFn = null; #callMetadata = {}; #resolved = false; #onResolveHandlers = []; #onStartHandlers = []; #result = null; #resultSet = false; fnName; target; isAsync; isManualAsync; constructor(args) { if (typeof args.componentIdx !== 'number') { throw new Error('invalid componentIdx for subtask creation'); } this.#componentIdx = args.componentIdx; this.#id = ++AsyncSubtask._ID; this.fnName = args.fnName; if (!args.parentTask) { throw new Error('missing parent task during subtask creation'); } this.#parentTask = args.parentTask; if (args.childTask) { this.#childTask = args.childTask; } if (args.memoryIdx) { this.#memoryIdx = args.memoryIdx; } if (!args.waitable) { throw new Error("missing/invalid waitable"); } this.#waitable = args.waitable; if (args.callMetadata) { this.#callMetadata = args.callMetadata; } this.#lenders = []; this.target = args.target; this.isAsync = args.isAsync; this.isManualAsync = args.isManualAsync; } id() { return this.#id; } parentTaskID() { return this.#parentTask?.id(); } childTaskID() { return this.#childTask?.id(); } state() { return this.#state; } waitable() { return this.#waitable; } waitableRep() { return this.#waitable.idx(); } join() { return this.#waitable.join(...arguments); } getPendingEvent() { return this.#waitable.getPendingEvent(...arguments); } hasPendingEvent() { return this.#waitable.hasPendingEvent(...arguments); } setPendingEvent() { return this.#waitable.setPendingEvent(...arguments); } setTarget(tgt) { this.target = tgt; } getResult() { if (!this.#resultSet) { throw new Error("subtask result has not been set") } return this.#result; } setResult(v) { if (this.#resultSet) { throw new Error("subtask result has already been set"); } this.#result = v; this.#resultSet = true; } componentIdx() { return this.#componentIdx; } setChildTask(t) { if (!t) { throw new Error('cannot set missing/invalid child task on subtask'); } if (this.#childTask) { throw new Error('child task is already set on subtask'); } if (this.#parentTask === t) { throw new Error("parent cannot be child"); } this.#childTask = t; } getChildTask(t) { return this.#childTask; } getParentTask() { return this.#parentTask; } setCallbackFn(f, name) { if (!f) { return; } if (this.#callbackFn) { throw new Error('callback fn can only be set once'); } this.#callbackFn = f; this.#callbackFnName = name; } getCallbackFnName() { if (!this.#callbackFn) { return undefined; } return this.#callbackFn.name; } setPostReturnFn(f) { if (!f) { return; } if (this.#postReturnFn) { throw new Error('postReturn fn can only be set once'); } this.#postReturnFn = f; } setOnProgressFn(f) { if (this.#onProgressFn) { throw new Error('on progress fn can only be set once'); } this.#onProgressFn = f; } isNotStarted() { return this.#state == AsyncSubtask.State.STARTING; } registerOnStartHandler(f) { this.#onStartHandlers.push(f); } onStart(args) { _debugLog('[AsyncSubtask#onStart()] args', { componentIdx: this.#componentIdx, subtaskID: this.#id, parentTaskID: this.parentTaskID(), fnName: this.fnName, }); if (this.#onProgressFn) { this.#onProgressFn(); } this.#state = AsyncSubtask.State.STARTED; let result; // If we have been provided a helper start function as a result of // component fusion performed by wasmtime tooling, then we can call that helper and lifts/lowers will // be performed for us. // // See also documentation on `HostIntrinsic::PrepareCall` // if (this.#callMetadata.startFn) { result = this.#callMetadata.startFn.apply(null, args?.startFnParams ?? []); } return result; } registerOnResolveHandler(f) { this.#onResolveHandlers.push(f); } reject(subtaskErr) { this.#childTask?.reject(subtaskErr); } onResolve(subtaskValue) { _debugLog('[AsyncSubtask#onResolve()] args', { componentIdx: this.#componentIdx, subtaskID: this.#id, isAsync: this.isAsync, childTaskID: this.childTaskID(), parentTaskID: this.parentTaskID(), parentTaskFnName: this.#parentTask?.entryFnName(), fnName: this.fnName, }); if (this.#resolved) { throw new Error('subtask has already been resolved'); } if (this.#onProgressFn) { this.#onProgressFn(); } if (subtaskValue === null) { if (this.#cancelRequested) { throw new Error('cancel was not requested, but no value present at return'); } if (this.#state === AsyncSubtask.State.STARTING) { this.#state = AsyncSubtask.State.CANCELLED_BEFORE_STARTED; } else { if (this.#state !== AsyncSubtask.State.STARTED) { throw new Error('resolved subtask must have been started before cancellation'); } this.#state = AsyncSubtask.State.CANCELLED_BEFORE_RETURNED; } } else { if (this.#state !== AsyncSubtask.State.STARTED) { throw new Error('resolved subtask must have been started before completion'); } this.#state = AsyncSubtask.State.RETURNED; } this.setResult(subtaskValue); for (const f of this.#onResolveHandlers) { try { f(subtaskValue); } catch (err) { console.error("error during subtask resolve handler", err); throw err; } } const callMetadata = this.getCallMetadata(); // TODO(fix): we should be able to easily have the caller's meomry // to lower into here, but it's not present in PrepareCall const memory = callMetadata.memory ?? this.#parentTask?.getReturnMemory() ?? lookupMemoriesForComponent({ componentIdx: this.#parentTask?.componentIdx() })[0]; if (callMetadata && !callMetadata.returnFn && this.isAsync && callMetadata.resultPtr && memory) { const { resultPtr, realloc } = callMetadata; const lowers = callMetadata.lowers; // may have been updated in task.return of the child if (lowers && lowers.length > 0) { lowers[0]({ componentIdx: this.#componentIdx, memory, realloc, vals: [subtaskValue], storagePtr: resultPtr, stringEncoding: callMetadata.stringEncoding, }); } } this.#resolved = true; this.#parentTask.removeSubtask(this); } getStateNumber() { return this.#state; } isReturned() { return this.#state === AsyncSubtask.State.RETURNED; } getCallMetadata() { return this.#callMetadata; } isResolved() { if (this.#state === AsyncSubtask.State.STARTING || this.#state === AsyncSubtask.State.STARTED) { return false; } if (this.#state === AsyncSubtask.State.RETURNED || this.#state === AsyncSubtask.State.CANCELLED_BEFORE_STARTED || this.#state === AsyncSubtask.State.CANCELLED_BEFORE_RETURNED) { return true; } throw new Error('unrecognized internal Subtask state [' + this.#state + ']'); } addLender(handle) { _debugLog('[AsyncSubtask#addLender()] args', { handle }); if (!Number.isNumber(handle)) { throw new Error('missing/invalid lender handle [' + handle + ']'); } if (this.#lenders.length === 0 || this.isResolved()) { throw new Error('subtask has no lendors or has already been resolved'); } handle.lends++; this.#lenders.push(handle); } deliverResolve() { _debugLog('[AsyncSubtask#deliverResolve()] args', { lenders: this.#lenders, parentTaskID: this.parentTaskID(), subtaskID: this.#id, childTaskID: this.childTaskID(), resolved: this.isResolved(), resolveDelivered: this.resolveDelivered(), }); const cannotDeliverResolve = this.resolveDelivered() || !this.isResolved(); if (cannotDeliverResolve) { throw new Error('subtask cannot deliver resolution twice, and the subtask must be resolved'); } for (const lender of this.#lenders) { lender.lends--; } this.#lenders = null; } resolveDelivered() { _debugLog('[AsyncSubtask#resolveDelivered()] args', { }); if (this.#lenders === null && !this.isResolved()) { throw new Error('invalid subtask state, lenders missing and subtask has not been resolved'); } return this.#lenders === null; } drop() { _debugLog('[AsyncSubtask#drop()] args', { componentIdx: this.#componentIdx, parentTaskID: this.#parentTask?.id(), parentTaskFnName: this.#parentTask?.entryFnName(), childTaskID: this.#childTask?.id(), childTaskFnName: this.#childTask?.entryFnName(), subtaskFnName: this.fnName, }); if (!this.#waitable) { throw new Error('missing/invalid inner waitable'); } if (!this.resolveDelivered()) { throw new Error('cannot drop subtask before resolve is delivered'); } if (this.#waitable) { this.#waitable.drop() } this.#dropped = true; } #getComponentState() { const state = getOrCreateAsyncState(this.#componentIdx); if (!state) { throw new Error('invalid/missing async state for component [' + componentIdx + ']'); } return state; } getWaitableHandleIdx() { _debugLog('[AsyncSubtask#getWaitableHandleIdx()] args', { }); if (!this.#waitable) { throw new Error('missing/invalid waitable'); } return this.waitableRep(); } } function _prepareCall( memoryIdx, getMemoryFn, startFn, returnFn, callerComponentIdx, calleeComponentIdx, taskReturnTypeIdx, calleeIsAsyncInt, stringEncoding, resultCountOrAsync, ) { _debugLog('[_prepareCall()]', { memoryIdx, callerComponentIdx, calleeComponentIdx, taskReturnTypeIdx, calleeIsAsyncInt, stringEncoding, resultCountOrAsync, }); const argArray = [...arguments]; // value passed in *may* be as large as u32::MAX which may be mangled into -2 resultCountOrAsync >>>= 0; let isAsync = false; let hasResultPointer = false; if (resultCountOrAsync === 2**32 - 1) { // prepare async with no result (u32::MAX) isAsync = true; hasResultPointer = false; } else if (resultCountOrAsync === 2**32 - 2) { // prepare async with result (u32::MAX - 1) isAsync = true; hasResultPointer = true; } const currentCallerTaskMeta = getCurrentTask(callerComponentIdx); if (!currentCallerTaskMeta) { throw new Error('invalid/missing current task for caller during prepare call'); } const currentCallerTask = currentCallerTaskMeta.task; if (!currentCallerTask) { throw new Error('unexpectedly missing task in meta for caller during prepare call'); } if (currentCallerTask.componentIdx() !== callerComponentIdx) { throw new Error(`task component idx [${ currentCallerTask.componentIdx() }] !== [${ callerComponentIdx }] (callee ${ calleeComponentIdx })`); } let getCalleeParamsFn; let resultPtr = null; let directParamsArr; if (hasResultPointer) { directParamsArr = argArray.slice(10, argArray.length - 1); getCalleeParamsFn = () => directParamsArr; resultPtr = argArray[argArray.length - 1]; } else { directParamsArr = argArray.slice(10); getCalleeParamsFn = () => directParamsArr; } let encoding; switch (stringEncoding) { case 0: encoding = 'utf8'; break; case 1: encoding = 'utf16'; break; case 2: encoding = 'compact-utf16'; break; default: throw new Error(`unrecognized string encoding enum [${stringEncoding}]`); } const subtask = currentCallerTask.createSubtask({ componentIdx: callerComponentIdx, parentTask: currentCallerTask, isAsync, callMetadata: { getMemoryFn, memoryIdx, resultPtr, returnFn, startFn, stringEncoding, } }); const [newTask, newTaskID] = createNewCurrentTask({ componentIdx: calleeComponentIdx, isAsync, getCalleeParamsFn, entryFnName: [ 'task', subtask.getParentTask().id(), 'subtask', subtask.id(), 'new-prepared-async-task' ].join('/'), stringEncoding, }); newTask.setParentSubtask(subtask); newTask.setReturnMemoryIdx(memoryIdx); newTask.setReturnMemory(getMemoryFn); subtask.setChildTask(newTask); newTask.subtaskMeta = { subtask, calleeComponentIdx, callerComponentIdx, getCalleeParamsFn, stringEncoding, isAsync, }; _setGlobalCurrentTaskMeta({ taskID: newTask.id(), componentIdx: newTask.componentIdx(), }); } function _asyncStartCall(args, callee, paramCount, resultCount, flags) { const componentIdx = ASYNC_CURRENT_COMPONENT_IDXS.at(-1); const globalTaskMeta = _getGlobalCurrentTaskMeta(componentIdx); if (!globalTaskMeta) { throw new Error('missing global current task globalTaskMeta'); } const taskID = globalTaskMeta.taskID; _debugLog('[_asyncStartCall()] args', { args, componentIdx }); const { getCallbackFn, callbackIdx, getPostReturnFn, postReturnIdx } = args; const preparedTaskMeta = getCurrentTask(componentIdx, taskID); if (!preparedTaskMeta) { throw new Error('unexpectedly missing current task'); } const preparedTask = preparedTaskMeta.task; if (!preparedTask) { throw new Error('unexpectedly missing current task'); } if (!preparedTask.subtaskMeta) { throw new Error('missing subtask meta from prepare'); } const { subtask, returnMemoryIdx, getReturnMemoryFn, callerComponentIdx, calleeComponentIdx, getCalleeParamsFn, isAsync, stringEncoding, } = preparedTask.subtaskMeta; if (!subtask) { throw new Error("missing subtask from cstate during async start call"); } if (calleeComponentIdx !== preparedTask.componentIdx()) { throw new Error(`meta callee idx [${calleeComponentIdx}] != current task idx [${preparedTask.componentIdx()}] during async start call`); } if (calleeComponentIdx !== componentIdx) { throw new Error("mismatched componentIdx for async start call (does not match prepare)"); } const argArray = [...arguments]; if (resultCount < 0 || resultCount > 1) { throw new Error('invalid/unsupported result count'); } const callbackFnName = 'callback_' + callbackIdx; const callbackFn = getCallbackFn(); preparedTask.setCallbackFn(callbackFn, callbackFnName); preparedTask.setPostReturnFn(getPostReturnFn()); if (resultCount < 0 || resultCount > 1) { throw new Error(`unsupported result count [${ resultCount }]`); } const params = preparedTask.getCalleeParams(); if (paramCount !== params.length) { throw new Error(`unexpected callee param count [${ params.length }], _asyncStartCall invocation expected [${ paramCount }]`); } const callerComponentState = getOrCreateAsyncState(subtask.componentIdx()); const calleeComponentState = getOrCreateAsyncState(preparedTask.componentIdx()); const calleeBackpressure = calleeComponentState.hasBackpressure(); // Set up a handler on subtask completion to lower results from the call into the caller's memory region. // // NOTE: during fused guest->guest calls this handler is triggered, but does not actually perform // lowering manually, as fused modules provider helper functions that can subtask.registerOnResolveHandler((res) => { _debugLog('[_asyncStartCall()] handling subtask result', { res, subtaskID: subtask.id() }); let subtaskCallMeta = subtask.getCallMetadata(); // NOTE: in the case of guest -> guest async calls, there may be no memory/realloc present, // as the host will intermediate the value storage/movement between calls. // // We can simply take the value and lower it as a parameter if (subtaskCallMeta.memory || subtaskCallMeta.realloc) { throw new Error("call metadata unexpectedly contains memory/realloc for guest->guest call"); } const callerTask = subtask.getParentTask(); const calleeTask = preparedTask; const callerMemoryIdx = callerTask.getReturnMemoryIdx(); const callerComponentIdx = callerTask.componentIdx(); // If a helper function was provided we are likely in a fused guest->guest call, // and the result will be delivered (lift/lowered) via helper function if (subtaskCallMeta && subtaskCallMeta.returnFn) { _debugLog('[_asyncStartCall()] return function present while handling subtask result, returning early (skipping lower)'); // TODO: centralize calling of returnFn to *one place* (if possible) if (subtaskCallMeta.returnFnCalled) { return; } subtaskCallMeta.returnFn.apply(null, [subtaskCallMeta.resultPtr]); return; } // If there is no where to lower the results, exit early if (!subtaskCallMeta.resultPtr) { _debugLog('[_asyncStartCall()] no result ptr during subtask result handling, returning early (skipping lower)'); return; } let callerMemory; if (callerMemoryIdx !== null && callerMemoryIdx !== undefined) { callerMemory = lookupMemoriesForComponent({ componentIdx: callerComponentIdx, memoryIdx: callerMemoryIdx }); } else { const callerMemories = lookupMemoriesForComponent({ componentIdx: callerComponentIdx }); if (callerMemories.length !== 1) { throw new Error(`unsupported amount of caller memories`); } callerMemory = callerMemories[0]; } if (!callerMemory) { _debugLog('[_asyncStartCall()] missing memory', { subtaskID: subtask.id(), res }); throw new Error(`missing memory for to guest->guest call result (subtask [${subtask.id()}])`); } const lowerFns = calleeTask.getReturnLowerFns(); if (!lowerFns || lowerFns.length === 0) { _debugLog('[_asyncStartCall()] missing result lower metadata for guest->guest call', { subtaskID: subtask.id() }); throw new Error(`missing result lower metadata for guest->guest call (subtask [${subtask.id()}])`); } if (lowerFns.length !== 1) { _debugLog('[_asyncStartCall()] only single result reportetd for guest->guest call', { subtaskID: subtask.id() }); throw new Error(`only single result supported for guest->guest calls (subtask [${subtask.id()}])`); } _debugLog('[_asyncStartCall()] lowering results', { subtaskID: subtask.id() }); lowerFns[0]({ realloc: undefined, memory: callerMemory, vals: [res], storagePtr: subtaskCallMeta.resultPtr, componentIdx: callerComponentIdx, stringEncoding: subtaskCallMeta.stringEncoding, }); }); subtask.setOnProgressFn(() => { subtask.setPendingEvent(() => { if (subtask.isResolved()) { subtask.deliverResolve(); } const event = { code: ASYNC_EVENT_CODE.SUBTASK, payload0: subtask.waitableRep(), payload1: subtask.getStateNumber(), }; return event; }); }); // Start the (event) driver loop that will resolve the task queueMicrotask(async () => { let startRes = subtask.onStart({ startFnParams: params }); startRes = Array.isArray(startRes) ? startRes : [startRes]; await calleeComponentState.suspendTask({ task: preparedTask, readyFn: () => !calleeComponentState.isExclusivelyLocked(), }); const started = await preparedTask.enter(); if (!started) { _debugLog('[_asyncStartCall()] task failed early', { taskID: preparedTask.id(), subtaskID: subtask.id(), }); throw new Error("task failed to start"); return; } let callbackResult; try { let jspiCallee = WebAssembly.promising(callee); callbackResult = await _withGlobalCurrentTaskMetaAsync({ taskID: preparedTask.id(), componentIdx: preparedTask.componentIdx(), fn: () => { return jspiCallee.apply(null, startRes); } }); } catch(err) { _debugLog("[_asyncStartCall()] initial subtask callee run failed", err); // NOTE: a good place to rejectt the parent task, if rejection API is enabled // subtask.reject(err); // subtask.getParentTask().reject(err); subtask.getParentTask().setErrored(err); return; } // If there was no callback function, we're dealing with a sync function // that was lifted as async without one, there is only the callee. if (!callbackFn) { _debugLog("[_asyncStartCall()] no callback, resolving w/ callee result", { taskID: preparedTask.id(), componentIdx: preparedTask.componentIdx(), preparedTask, stateNumber: preparedTask.taskState(), isResolved: preparedTask.isResolved(), callbackFn, }); preparedTask.resolve([callbackResult]); return; } let fnName = callbackFn.fnName; if (!fnName) { fnName = [ '<task ', subtask.parentTaskID(), '/subtask ', subtask.id(), '/task ', preparedTask.id(), '>', ].join(""); } try { _debugLog("[_asyncStartCall()] starting driver loop", { fnName, componentIdx: preparedTask.componentIdx(), subtaskID: subtask.id(), childTaskID: subtask.childTaskID(), parentTaskID: subtask.parentTaskID(), }); await _driverLoop({ componentState: calleeComponentState, task: preparedTask, fnName, isAsync: true, callbackResult, resolve, reject }); } catch (err) { _debugLog("[AsyncStartCall] drive loop call failure", { err }); } }); const subtaskState = subtask.getStateNumber(); if (subtaskState < 0 || subtaskState > 2**5) { throw new Error('invalid subtask state, out of valid range'); } _debugLog('[_asyncStartCall()] returning subtask rep & state', { subtask: { rep: subtask.waitableRep(), state: subtaskState, } }); return Number(subtask.waitableRep()) << 4 | subtaskState; } function _syncStartCall(callbackIdx) { _debugLog('[_syncStartCall()] args', { callbackIdx }); throw new Error('synchronous start call not implemented!'); } class Waitable { #componentIdx; #pendingEventFn = null; #promise; #resolve; #reject; #waitableSet = null; #idx = null; // to component-global waitables target; constructor(args) { const { componentIdx, target } = args; this.#componentIdx = componentIdx; this.target = args.target; this.#resetPromise(); } componentIdx() { return this.#componentIdx; } isInSet() { return this.#waitableSet !== null; } idx() { return this.#idx; } setIdx(idx) { if (idx === 0) { throw new Error("waitable idx cannot be zero"); } this.#idx = idx; } setTarget(tgt) { this.target = tgt; } #resetPromise() { const { promise, resolve, reject } = promiseWithResolvers() this.#promise = promise; this.#resolve = resolve; this.#reject = reject; } resolve() { this.#resolve(); } reject(err) { this.#reject(err); } promise() { return this.#promise; } hasPendingEvent() { // _debugLog('[Waitable#hasPendingEvent()]', { // componentIdx: this.#componentIdx, // waitable: this, // waitableSet: this.#waitableSet, // hasPendingEvent: this.#pendingEventFn !== null, // }); return this.#pendingEventFn !== null; } setPendingEvent(fn) { _debugLog('[Waitable#setPendingEvent()] args', { waitable: this, inSet: this.#waitableSet, }); this.#pendingEventFn = fn; } getPendingEvent() { _debugLog('[Waitable#getPendingEvent()] args', { waitable: this, inSet: this.#waitableSet, hasPendingEvent: this.#pendingEventFn !== null, }); if (this.#pendingEventFn === null) { return null; } const eventFn = this.#pendingEventFn; this.#pendingEventFn = null; const e = eventFn(); this.#resetPromise(); return e; } join(waitableSet) { _debugLog('[Waitable#join()] args', { waitable: this, waitableSet: waitableSet, }); if (this.#waitableSet) { this.#waitableSet.removeWaitable(this); } if (!waitableSet) { this.#waitableSet = null; return; } waitableSet.addWaitable(this); this.#waitableSet = waitableSet; } drop() { _debugLog('[Waitable#drop()] args', { componentIdx: this.#componentIdx, waitable: this, }); if (this.hasPendingEvent()) { throw new Error('waitables with pending events cannot be dropped'); } this.join(null); } } const ERR_CTX_TABLES = {}; let dv = new DataView(new ArrayBuffer()); const dataView = mem => dv.buffer === mem.buffer ? dv : dv = new DataView(mem.buffer); function toUint64(val) { const converted = BigInt(val) return BigInt.asUintN(64, converted); } function toUint32(val) { return val >>> 0; } const utf16Decoder = new TextDecoder('utf-16'); const TEXT_DECODER_UTF8 = new TextDecoder(); const TEXT_ENCODER_UTF8 = new TextEncoder(); function _utf8AllocateAndEncode(s, realloc, memory) { if (typeof s !== 'string') { throw new TypeError('expected a string, received [' + typeof s + ']'); } if (s.length === 0) { return { ptr: 1, len: 0 }; } let buf = TEXT_ENCODER_UTF8.encode(s); let ptr = realloc(0, 0, 1, buf.length); new Uint8Array(memory.buffer).set(buf, ptr); const res = { ptr, len: buf.length, codepoints: [...s].length }; return res; } const T_FLAG = 1 << 30; function rscTableCreateOwn(table, rep) { const free = table[0] & ~T_FLAG; if (free === 0) { table.push(0); table.push(rep | T_FLAG); return (table.length >> 1) - 1; } table[0] = table[free << 1]; table[free << 1] = 0; table[(free << 1) + 1] = rep | T_FLAG; return free; } function rscTableRemove(table, handle) { const scope = table[handle << 1]; const val = table[(handle << 1) + 1]; const own = (val & T_FLAG) !== 0; const rep = val & ~T_FLAG; if (val === 0 || (scope & T_FLAG) !== 0) { throw new TypeError("Invalid handle"); } table[handle << 1] = table[0] | T_FLAG; table[0] = handle | T_FLAG; return { rep, scope, own }; } let curResourceBorrows = []; function getCurrentTask(componentIdx, taskID) { let usedGlobal = false; if (componentIdx === undefined || componentIdx === null) { throw new Error('missing component idx'); // TODO(fix) // componentIdx = ASYNC_CURRENT_COMPONENT_IDXS.at(-1); // usedGlobal = true; } const taskMetas = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx); if (taskMetas === undefined || taskMetas.length === 0) { return undefined; } if (taskID) { return taskMetas.find(meta => meta.task.id() === taskID); } const taskMeta = taskMetas[taskMetas.length - 1]; if (!taskMeta || !taskMeta.task) { return undefined; } return taskMeta; } function createNewCurrentTask(args) { _debugLog('[createNewCurrentTask()] args', args); const { componentIdx, isAsync, isManualAsync, entryFnName, parentSubtaskID, callbackFnName, getCallbackFn, getParamsFn, stringEncoding, errHandling, getCalleeParamsFn, resultPtr, callingWasmExport, } = args; if (componentIdx === undefined || componentIdx === null) { throw new Error('missing/invalid component instance index while starting task'); } let taskMetas = ASYNC_TASKS_BY_COMPONENT_IDX.get(componentIdx); const callbackFn = getCallbackFn ? getCallbackFn() : null; const newTask = new AsyncTask({ componentIdx, isAsync, isManualAsync, entryFnName, callbackFn, callbackFnName, stringEncoding, getCalleeParamsFn, resultPtr, errHandling, }); const newTaskID = newTask.id(); const newTaskMeta = { id: newTaskID, componentIdx, task: newTask }; // NOTE: do not track host tasks ASYNC_CURRENT_TASK_IDS.push(newTaskID); ASYNC_CURRENT_COMPONENT_IDXS.push(componentIdx); if (!taskMetas) { taskMetas = [newTaskMeta]; ASYNC_TASKS_BY_COMPONENT_IDX.set(componentIdx, [newTaskMeta]); } else { taskMetas.push(newTaskMeta); } return [newTask, newTaskID]; } const ASYNC_TASKS_BY_COMPONENT_IDX = new Map(); class AsyncTask { static _ID = 0n; static State = { INITIAL: 'initial', CANCELLED: 'cancelled', CANCEL_PENDING: 'cancel-pending', CANCEL_DELIVERED: 'cancel-delivered', RESOLVED: 'resolved', } static BlockResult = { CANCELLED: 'block.cancelled', NOT_CANCELLED: 'block.not-cancelled', } #id; #componentIdx; #state; #isAsync; #isManualAsync; #entryFnName = null; #onResolveHandlers = []; #completionPromise = null; #rejected = false; #exitPromise = null; #onExitHandlers = []; #memoryIdx = null; #memory = null; #callbackFn = null; #callbackFnName = null; #postReturnFn = null; #getCalleeParamsFn = null; #stringEncoding = null; #parentSubtask = null; #needsExclusiveLock = false; #errHandling; #backpressurePromise; #backpressureWaiters = 0n; #returnLowerFns = null; #subtasks = []; #entered = false; #exited = false; #errored = null; cancelled = false; cancelRequested = false; alwaysTaskReturn = false; returnCalls = 0; storage = [0, 0]; borrowedHandles = {}; tmpRetI64HighBits = 0|0; constructor(opts) { this.#id = ++AsyncTask._ID; if (opts?.componentIdx === undefined) { throw new TypeError('missing component id during task creation'); } this.#componentIdx = opts.componentIdx; this.#state = AsyncTask.State.INITIAL; this.#isAsync = opts?.isAsync ?? false; this.#isManualAsync = opts?.isManualAsync ?? false; this.#entryFnName = opts.entryFnName; const { promise: completionPromise, resolve: resolveCompletionPromise, reject: rejectCompletionPromise, } = promiseWithResolvers(); this.#completionPromise = completionPromise; this.#onResolveHandlers.push((results) => { if (this.#errored !== null) { rejectCompletionPromise(this.#errored); return; } else if (this.#rejected) { rejectCompletionPromise(results); return; } resolveCompletionPromise(results); }); const { promise: exitPromise, resolve: resolveExitPromise, reject: rejectExitPromise, } = promiseWithResolvers(); this.#exitPromise = exitPromise; this.#onExitHandlers.push(() => { resolveExitPromise(); }); if (opts.callbackFn) { this.#callbackFn = opts.callbackFn; } if (opts.callbackFnName) { this.#callbackFnName = opts.callbackFnName; } if (opts.getCalleeParamsFn) { this.#getCalleeParamsFn = opts.getCalleeParamsFn; } if (opts.stringEncoding) { this.#stringEncoding = opts.stringEncoding; } if (opts.parentSubtask) { this.#parentSubtask = opts.parentSubtask; } this.#needsExclusiveLock = this.isSync() || !this.hasCallback(); if (opts.errHandling) { this.#errHandling = opts.errHandling; } } taskState() { return this.#state; } id() { return this.#id; } componentIdx() { return this.#componentIdx; } entryFnName() { return this.#entryFnName; } completionPromise() { return this.#completionPromise; } exitPromise() { return this.#exitPromise; } isAsync() { return this.#isAsync; } isSync() { return !this.isAsync(); } getErrHandling() { return this.#errHandling; } hasCallback() { return this.#callbackFn !== null; } getReturnMemoryIdx() { return this.#memoryIdx; } setReturnMemoryIdx(idx) { if (idx === null) { return; } this.#memoryIdx = idx; } getReturnMemory() { return this.#memory; } setReturnMemory(m) { if (m === null) { return; } this.#memory = m; } setReturnLowerFns(fns) { this.#returnLowerFns = fns; } getReturnLowerFns() { return this.#returnLowerFns; } setParentSubtask(subtask) { if (!subtask || !(subtask instanceof AsyncSubtask)) { return } if (this.#parentSubtask) { throw new Error('parent subtask can only be set once'); } this.#parentSubtask = subtask; } getParentSubtask() { return this.#parentSubtask; } // TODO(threads): this is very inefficient, we can pass along a root task, // and ideally do not need this once thread support is in place getRootTask() { let currentSubtask = this.getParentSubtask(); let task = this; while (currentSubtask) { task = currentSubtask.getParentTask(); currentSubtask = task.getParentSubtask(); } return task; } setPostReturnFn(f) { if (!f) { return; } if (this.#postReturnFn) { throw new Error('postReturn fn can only be set once'); } this.#postReturnFn = f; } se