WebAssembly W5: the Memory, Table and Global constructors, Module.imports/exports/customSections, and function references from tables (#1410)

This commit is contained in:
Chris Tate
2026-10-20 12:12:19 -05:00
committed by GitHub
55 changed files with 3314 additions and 194 deletions
@@ -1,5 +1,5 @@
scriptc translates WebAssembly modules to native code when it builds the program; there is no interpreter or JIT in the executable.
A build translates every module whose bytes it can see: files at paths the compiler resolves (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`, `new URL("./x.wasm", import.meta.url)`, `path.join(__dirname, ...)`), modules inlined as base64 or byte-array literals, `.wasm` files shipped by packages whose code uses WebAssembly, and files listed in a `scriptc.wasm` array in `package.json`.
A build translates every module whose bytes it can see: files at paths the compiler resolves (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`, `new URL("./x.wasm", import.meta.url)`, `path.join(__dirname, ...)`), including `.so` WebAssembly shared libraries such as quickjs-wasi's extensions, modules inlined as base64 or byte-array literals, `.wasm` files shipped by packages whose code uses WebAssembly, and files listed in a `scriptc.wasm` array in `package.json`.
At run time `WebAssembly.compile`, `instantiate`, `validate` and `new WebAssembly.Module` find the translated module by the SHA-256 of the bytes they are given.
Bytes the build did not see, including bytes produced at run time, reject with a `CompileError` that says the module was not translated, and `WebAssembly.validate` answers `false` for them.
`scriptc coverage` lists the modules a build finds, with their size, whether they validate, and the WebAssembly proposals they use.
@@ -7,22 +7,23 @@ Bytes the build did not see, including bytes produced at run time, reject with a
Translated modules can use the WebAssembly 2.0 feature set: the MVP plus sign-extension operators, non-trapping float-to-int conversions, bulk memory, multi-value, mutable globals, reference types, and extended constant expressions.
Modules that use SIMD, threads, exception handling, tail calls, typed function references or GC, memory64, or multiple memories are not translated, so loading them rejects with the `CompileError` above; packages that fall back to a scalar build when that happens, such as undici and es-module-lexer, take their fallback.
Supported: `WebAssembly.compile`, `instantiate` (with bytes or a `Module`), `validate`, `new WebAssembly.Module`, `new WebAssembly.Instance`, and `instanceof` for every class; the error classes `CompileError`, `LinkError` and `RuntimeError`, constructed with or without `new`; `globalThis.WebAssembly` and its `compile`, `instantiate` and `validate` functions as values.
Instances expose exported functions, memories (`buffer`, `grow`), tables (`get`, `set`, `grow`, `length`) and globals (`value`).
Imports can be functions, numbers or BigInts for immutable globals, and memories, tables and globals exported by other instances.
Supported: `WebAssembly.compile`, `instantiate` (with bytes or a `Module`), `validate`, `new WebAssembly.Module`, `new WebAssembly.Instance`, `new WebAssembly.Memory`, `new WebAssembly.Table`, `new WebAssembly.Global`, `WebAssembly.Module.imports`, `exports` and `customSections`, and `instanceof` for every class; the error classes `CompileError`, `LinkError` and `RuntimeError`, constructed with or without `new`; `globalThis.WebAssembly` and its `compile`, `instantiate` and `validate` functions as values.
Instances expose exported functions, memories (`buffer`, `grow`), tables (`get`, `set`, `grow`, `length`) and globals (`value`, `valueOf`).
Imports can be functions, numbers or BigInts for immutable globals, and memories, tables and globals that the program constructs or other instances export; their limits, types and mutability are checked with Node.js's `LinkError` messages.
Function references from a table or global reach JavaScript as callable functions, and exported functions can be stored in tables and globals, which is what dynamic linkers such as quickjs-wasi's extension loader do.
Traps throw `WebAssembly.RuntimeError` with Node.js's messages, unbounded recursion throws `RangeError`, and an exception thrown by an imported function propagates through the WebAssembly frames unchanged.
A `WebAssembly.Module` can be sent to a worker through `workerData` or `postMessage`.
Not supported yet: the `Memory`, `Table`, `Global`, `Tag` and `Exception` constructors, `WebAssembly.Module.imports`, `exports` and `customSections`, `compileStreaming` and `instantiateStreaming`, and the classes used as values other than with `new` and `instanceof`.
Not supported yet: the `Tag` and `Exception` constructors, shared memories (`new WebAssembly.Memory({ shared: true, ... })` throws a `TypeError`), `compileStreaming` and `instantiateStreaming`, and the classes used as values other than with `new`, `instanceof` and `WebAssembly.Module`'s static functions.
TypeScript sources fail to build with SC2020 at each such use, and JavaScript sources throw that diagnostic when the statement runs.
Programs built for `wasm32-wasi` can't use the WebAssembly API.
Differences from Node.js:
- `Memory.prototype.grow` does not detach the previous `ArrayBuffer`.
The memory never moves, so views created before the growth stay valid but keep their old length.
- Growing a memory, from JavaScript or inside the module, does not detach the previous `ArrayBuffer`.
The memory never moves, so views created before the growth stay valid but keep their old length; code that reads `memory.buffer` again after growing, as WebAssembly glue code does, behaves as in Node.js.
- `valueOf()` answers a `WebAssembly.Global`'s value only when the receiver's static type is `WebAssembly.Global`; through an untyped value it answers the object, and converting a `Global` to a primitive uses the object rather than its value.
- Instances are never freed, together with their memories, imports and exports.
- A function reference (`funcref`) can reach JavaScript only as a function the module exports; other function references throw a `TypeError`.
- A load whose result the module never uses may be removed instead of trapping when its address is out of bounds.
Loads whose result is dropped explicitly still trap.
- NaN payloads may differ, for example when a signaling NaN passes through arithmetic or a JavaScript number.
@@ -34248,7 +34248,11 @@
"tests/corpus/webassembly-spec-float/main.mjs",
"tests/corpus/webassembly-spec-control/main.mjs",
"tests/corpus/webassembly-spec-memory/main.mjs",
"tests/corpus/webassembly-spec-table/main.mjs"
"tests/corpus/webassembly-spec-table/main.mjs",
"tests/corpus/webassembly-spec-imports/main.mjs",
"tests/corpus/webassembly-jsapi.mjs",
"tests/corpus/webassembly-wpt-jsapi.mjs",
"tests/corpus/webassembly-dylink/main.mjs"
]
}
}
@@ -52299,13 +52299,17 @@
"tests/corpus/6331-jose-root-entry/main.ts",
"tests/corpus/text-codec-values/main.ts",
"tests/corpus/webassembly-api.mjs",
"tests/corpus/webassembly-dylink/main.mjs",
"tests/corpus/webassembly-jsapi.mjs",
"tests/corpus/webassembly-spec-control/main.mjs",
"tests/corpus/webassembly-spec-float/main.mjs",
"tests/corpus/webassembly-spec-imports/main.mjs",
"tests/corpus/webassembly-spec-int/main.mjs",
"tests/corpus/webassembly-spec-memory/main.mjs",
"tests/corpus/webassembly-spec-table/main.mjs",
"tests/corpus/webassembly-typed.ts",
"tests/corpus/webassembly-worker.ts",
"tests/corpus/webassembly-wpt-jsapi.mjs",
"tests/fixtures/node-types/navigator.ts",
"tests/harness/fetch-conformance.test.ts",
"tests/harness/project-config.test.ts"
@@ -53667,7 +53671,11 @@
"tests/corpus/webassembly-spec-float/main.mjs",
"tests/corpus/webassembly-spec-control/main.mjs",
"tests/corpus/webassembly-spec-memory/main.mjs",
"tests/corpus/webassembly-spec-table/main.mjs"
"tests/corpus/webassembly-spec-table/main.mjs",
"tests/corpus/webassembly-spec-imports/main.mjs",
"tests/corpus/webassembly-jsapi.mjs",
"tests/corpus/webassembly-wpt-jsapi.mjs",
"tests/corpus/webassembly-dylink/main.mjs"
]
},
"anchorSource": "exact"
+1 -1
View File
@@ -28,7 +28,7 @@
"webstreams": "unreviewed"
},
"evidenceByManifestPrefix": {
"stdlib.webassembly": ["tests/corpus/webassembly-api.mjs", "tests/corpus/webassembly-typed.ts", "tests/corpus/webassembly-worker.ts", "tests/corpus/webassembly-spec-int/main.mjs", "tests/corpus/webassembly-spec-float/main.mjs", "tests/corpus/webassembly-spec-control/main.mjs", "tests/corpus/webassembly-spec-memory/main.mjs", "tests/corpus/webassembly-spec-table/main.mjs"],
"stdlib.webassembly": ["tests/corpus/webassembly-api.mjs", "tests/corpus/webassembly-typed.ts", "tests/corpus/webassembly-worker.ts", "tests/corpus/webassembly-spec-int/main.mjs", "tests/corpus/webassembly-spec-float/main.mjs", "tests/corpus/webassembly-spec-control/main.mjs", "tests/corpus/webassembly-spec-memory/main.mjs", "tests/corpus/webassembly-spec-table/main.mjs", "tests/corpus/webassembly-spec-imports/main.mjs", "tests/corpus/webassembly-jsapi.mjs", "tests/corpus/webassembly-wpt-jsapi.mjs", "tests/corpus/webassembly-dylink/main.mjs"],
"node-builtin.fs.readFileSync": ["tests/corpus/1403-buffer-fs.ts", "tests/corpus/2595-fs-arg-ladders.cjs", "tests/corpus/2919-fs-function-values.ts", "tests/corpus/2920-fs-promises-function-values.ts", "tests/corpus/fs-read-encodings.ts"],
"syntax.using-declarations": ["tests/corpus/2841-using-sync.ts"],
"syntax.await-using-declarations": ["tests/corpus/2842-await-using.ts"],
@@ -881,6 +881,14 @@ export const LIB_FN_SYMS: Record<string, string> = {
"wasm.instanceNew": "scr_wasm_instance_new",
"wasm.is": "scr_wasm_is",
"wasm.errorNew": "scr_wasm_error_new",
"wasm.memoryNew": "scr_wasm_memory_construct",
"wasm.tableNew": "scr_wasm_table_construct",
"wasm.globalNew": "scr_wasm_global_construct",
"wasm.withoutNew": "scr_wasm_construct_without_new",
"wasm.moduleImports": "scr_wasm_module_imports",
"wasm.moduleExports": "scr_wasm_module_exports",
"wasm.moduleCustomSections": "scr_wasm_module_custom_sections",
"wasm.valueOf": "scr_wasm_value_of",
"wasm.function": "scr_wasm_function",
"wasm.namespace": "scr_wasm_namespace",
"wasm.globalView": "scr_wasm_global_view",
@@ -48,7 +48,10 @@ export function emitWasmModules(
if (bytes === null) continue;
let translation;
try {
translation = translateWasmModule(bytes, hexBytes(module.sha256), { boundsChecks });
translation = translateWasmModule(bytes, hexBytes(module.sha256), {
boundsChecks,
customSections: mod.wasmCustomSections === true,
});
} catch (error) {
if (error instanceof WasmTranslateError) continue;
throw error;
@@ -454,7 +454,7 @@ export function generateSurfaceManifest(compilerVersion: string): SurfaceManifes
kind: "stdlib",
name: "globalThis.WebAssembly",
status: "static",
note: `compile, instantiate, validate, Module, Instance, instanceof, the error classes, exports, memories, tables, globals and Module transfer to workers, over the modules translated at build time; the rest refuses with ${refusal.code} (${refusal.hint ?? ""})`,
note: `compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, instanceof, the error classes, exports, imports and Module transfer to workers, over the modules translated at build time; the rest refuses with ${refusal.code} (${refusal.hint ?? ""})`,
});
}
@@ -741,18 +741,19 @@ export function noLoweringDiag(
}
/** A REACHED use of a part of the WebAssembly API scriptc does not
* implement yet (SC2020): the Memory, Table and Global constructors,
* Module.imports/exports/customSections, the streaming functions, the
* classes as values. compile, instantiate, validate, Module, Instance and
* the error classes lower (builtins/webassembly.ts). `surface` names what
* was used ('new WebAssembly.Memory', 'WebAssembly.Module.imports'). */
* implement yet (SC2020): the streaming functions, Tag and Exception, the
* classes as values. compile, instantiate, validate, the Module, Instance,
* Memory, Table and Global constructors, Module.imports/exports/
* customSections and the error classes lower (builtins/webassembly.ts).
* `surface` names what was used ('WebAssembly.compileStreaming',
* 'WebAssembly.Memory'). */
export function webAssemblyDiag(surface: string, loc: SrcLoc): ScrDiagnostic {
return {
code: "SC2020",
message: `'${surface}' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, Module, Instance and the error classes over modules translated at build time, but not this part of the API`,
message: `'${surface}' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, and the error classes over modules translated at build time, but not this part of the API`,
loc,
milestone: "later",
hint: "the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)",
hint: "the streaming functions, Tag and Exception, and the classes used as values other than with new and instanceof are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)",
};
}
@@ -6,6 +6,7 @@ import { BOOL, DYN, F64, STRING, type IrExpr } from "../../../ir/ir.js";
import { dynUndefinedExpr } from "../../../ir/build.js";
import { lowerDynObjectLiteral } from "../expressions/object-literals.js";
import { stdlibGlobalNameOf } from "../surfaces.js";
import { isWebAssemblyType } from "../../type-mapper.js";
/* The WebAssembly API. scriptc.d.ts declares its types (lib.dom's
* namespace) and the type mapper gives them the checked-dynamic
@@ -13,16 +14,18 @@ import { stdlibGlobalNameOf } from "../surfaces.js";
* native code (wasm/translate.ts) and found at run time by the SHA-256 of
* their bytes (scr_wasm.c):
* - WebAssembly.compile, instantiate and validate calls, new
* WebAssembly.Module and new WebAssembly.Instance lower to wasm.*
* libCalls; members of the values they produce (exports, memory
* WebAssembly.Module, Instance, Memory, Table and Global, and
* WebAssembly.Module.imports, exports and customSections lower to
* wasm.* libCalls; members of the values they produce (exports, memory
* buffers, globals, tables) dispatch through the native handles;
* - the classes called without `new` throw Node's TypeError
* (wasm.withoutNew);
* - `v instanceof WebAssembly.<Class>` lowers to wasm.is;
* - the error classes construct with or without `new` (wasm.errorNew).
* Every other reached use of the API value (the bare namespace, the
* Memory, Table and Global constructors, Module.imports and friends, the
* streaming functions) still fences at its expression with
* webAssemblyDiag: a build error in TypeScript sources, a located runtime
* fence in JavaScript ones. typeof needs no value and folds to Node's
* Every other reached use of the API value (the classes as values, the
* streaming functions, Tag and Exception) still fences at its expression
* with webAssemblyDiag: a build error in TypeScript sources, a located
* runtime fence in JavaScript ones. typeof needs no value and folds to Node's
* answer. A lowering marks the lowerer, so the build translates the
* modules it discovers (lowerToIr). */
@@ -170,15 +173,38 @@ function lowerArg(lowerer: Lowerer, arg: ts.Expression): IrExpr {
: lowerer.lowerExprExpecting(arg, DYN);
}
/** The arguments, padded with undefined to `count`; null for spreads or
* extra arguments (they stay refused). */
/** An argument whose evaluation has no effect: a literal, a plain
* identifier, or an empty object or array literal. */
function isInert(arg: ts.Expression): boolean {
const node = bare(arg);
if (ts.isObjectLiteralExpression(node)) return node.properties.nodes.length === 0;
if (ts.isArrayLiteralExpression(node)) return node.elements.nodes.length === 0;
return (
ts.isIdentifier(node) ||
ts.isNumericLiteral(node) ||
node.kind === ts.SyntaxKind.BigIntLiteral ||
ts.isStringLiteral(node) ||
ts.isNoSubstitutionTemplateLiteral(node) ||
node.kind === ts.SyntaxKind.NullKeyword ||
node.kind === ts.SyntaxKind.TrueKeyword ||
node.kind === ts.SyntaxKind.FalseKeyword
);
}
/** The arguments, padded with undefined to `count`. Extra arguments are
* ignored, as the API ignores them, when evaluating them has no effect;
* null for spreads or other extra arguments (they stay refused). */
function lowerArgs(
lowerer: Lowerer,
args: readonly ts.Expression[],
count: number,
loc: IrExpr["loc"],
): IrExpr[] | null {
if (args.length > count || args.some(ts.isSpreadElement)) return null;
if (args.some(ts.isSpreadElement)) return null;
if (args.length > count) {
if (!args.slice(count).every(isInert)) return null;
args = args.slice(0, count);
}
const out = args.map((arg) => lowerArg(lowerer, arg));
while (out.length < count) out.push(dynUndefinedExpr(loc));
return out;
@@ -195,14 +221,57 @@ function errorNew(member: string, args: IrExpr[], loc: IrExpr["loc"]): IrExpr {
};
}
/** The classes that require `new`, in wasm.withoutNew's numbering. */
const NEW_ONLY = ["Module", "Instance", "Memory", "Table", "Global"];
/** `WebAssembly.Module.imports(module)`, `exports(module)` and
* `customSections(module, name)`. Their results are arrays of descriptors
* or ArrayBuffers built as checked-dynamic values, then taken into the
* call's own type. Null for anything else. */
function lowerModuleStatic(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
const surface = webAssemblySurface(lowerer, call.expression);
const name = surface?.startsWith("WebAssembly.Module.")
? surface.slice("WebAssembly.Module.".length)
: null;
if (name !== "imports" && name !== "exports" && name !== "customSections") return null;
const loc = locOf(call);
const args = lowerArgs(lowerer, call.arguments.nodes, name === "customSections" ? 2 : 1, loc);
if (args === null) return null;
lowerer.usesWebAssembly = true;
if (name === "customSections") lowerer.usesWasmCustomSections = true;
const fn =
name === "imports"
? "wasm.moduleImports"
: name === "exports"
? "wasm.moduleExports"
: "wasm.moduleCustomSections";
const result: IrExpr = { kind: "libCall", fn, args, type: DYN, loc };
return lowerer.coerceInto(call, result, lowerer.irTypeOf(call));
}
/** `WebAssembly.compile(bytes)`, `WebAssembly.instantiate(source,
* imports?)`, `WebAssembly.validate(bytes)`, and the error classes called
* without `new`. Null for anything else. */
* imports?)`, `WebAssembly.validate(bytes)`, the error classes called
* without `new`, the other classes called without `new` (a TypeError), and
* Module's static functions. Null for anything else. */
export function lowerWebAssemblyCall(lowerer: Lowerer, call: ts.CallExpression): IrExpr | null {
if (call.questionDotToken) return null;
const member = webAssemblyMember(lowerer, call.expression);
if (member === null) return null;
if (member === null) return lowerModuleStatic(lowerer, call);
const loc = locOf(call);
const newOnly = NEW_ONLY.indexOf(member);
if (newOnly >= 0) {
// The arguments evaluate, then the call throws.
const args = lowerArgs(lowerer, call.arguments.nodes, 2, loc);
if (args === null) return null;
lowerer.usesWebAssembly = true;
return {
kind: "libCall",
fn: "wasm.withoutNew",
args: [{ kind: "numLit", value: newOnly, type: F64, loc }, args[0]!, args[1]!],
type: DYN,
loc,
};
}
const isError = member === "CompileError" || member === "LinkError" || member === "RuntimeError";
if (member !== "compile" && member !== "instantiate" && member !== "validate" && !isError)
return null;
@@ -234,14 +303,17 @@ export function lowerWebAssemblyCall(lowerer: Lowerer, call: ts.CallExpression):
}
/** `new WebAssembly.Module(bytes)`, `new WebAssembly.Instance(module,
* imports?)` and the error classes. Null for anything else. */
* imports?)`, `new WebAssembly.Memory(descriptor)`, `new
* WebAssembly.Table(descriptor, value?)`, `new
* WebAssembly.Global(descriptor, value?)` and the error classes. Null for
* anything else. */
export function lowerWebAssemblyNew(lowerer: Lowerer, expr: ts.NewExpression): IrExpr | null {
const member = webAssemblyMember(lowerer, expr.expression);
if (member === null) return null;
const isError = member === "CompileError" || member === "LinkError" || member === "RuntimeError";
if (member !== "Module" && member !== "Instance" && !isError) return null;
if (!NEW_ONLY.includes(member) && !isError) return null;
const loc = locOf(expr);
const used = member === "Module" ? 1 : 2;
const used = member === "Module" || member === "Memory" ? 1 : 2;
const args = lowerArgs(lowerer, expr.arguments?.nodes ?? [], used, loc);
if (args === null) return null;
lowerer.usesWebAssembly = true;
@@ -249,6 +321,12 @@ export function lowerWebAssemblyNew(lowerer: Lowerer, expr: ts.NewExpression): I
if (isError) result = errorNew(member, args, loc);
else if (member === "Module")
result = { kind: "libCall", fn: "wasm.moduleNew", args: [args[0]!], type: DYN, loc };
else if (member === "Memory")
result = { kind: "libCall", fn: "wasm.memoryNew", args: [args[0]!], type: DYN, loc };
else if (member === "Table")
result = { kind: "libCall", fn: "wasm.tableNew", args: [args[0]!, args[1]!], type: DYN, loc };
else if (member === "Global")
result = { kind: "libCall", fn: "wasm.globalNew", args: [args[0]!, args[1]!], type: DYN, loc };
else
result = {
kind: "libCall",
@@ -309,3 +387,18 @@ export function lowerWebAssemblyValue(lowerer: Lowerer, expr: ts.Expression): Ir
if (index < 0 || !ts.isPropertyAccessExpression(node)) return null;
return fn("wasm.function", [{ kind: "numLit", value: index, type: F64, loc }]);
}
/** `global.valueOf()` on a receiver typed as a WebAssembly value: a
* Global answers its value (the generic dyn.valueOf answers the object
* itself for native handles other than Date). Null for other receivers. */
export function lowerWebAssemblyValueOf(
lowerer: Lowerer,
receiver: ts.Expression,
recv: IrExpr,
loc: IrExpr["loc"],
): IrExpr | null {
const ctx = { checker: lowerer.checker, isStdlibFile: lowerer.isStdlibFile };
if (!isWebAssemblyType(lowerer.typeOf(receiver), ctx)) return null;
lowerer.usesWebAssembly = true;
return { kind: "libCall", fn: "wasm.valueOf", args: [recv], type: DYN, loc };
}
@@ -4,6 +4,7 @@ import { lowerBuiltinClassCall } from "./builtins/builtin-class-dyn.js";
import { refuseModuleMemberThis } from "./module-member-this.js";
import { lowerUnionEquality, tagEqualityMayMissAlias } from "./strict-equality.js";
import { lowerHashStreamCall } from "./builtins/hash-stream.js";
import { lowerWebAssemblyValueOf } from "./builtins/webassembly.js";
import { lowerResponseStaticJson } from "./lower-stdlib.js";
import { lowerArrayCallbackCall } from "./containers/array-callback-call.js";
/** Dispatch source calls, builtin members and receiver-specific operations.
@@ -3674,6 +3675,8 @@ function lowerDynReceiverMethodCall(
// answers each kind's own: primitives and plain objects themselves, a
// Date its time value, an own or inherited valueOf member its result.
if (access.name.text === "valueOf" && call.arguments.nodes.length === 0) {
const webAssembly = lowerWebAssemblyValueOf(lowerer, access.expression, recv, locOf(call));
if (webAssembly !== null) return webAssembly;
return {
kind: "libCall",
fn: "dyn.valueOf",
@@ -1179,6 +1179,7 @@ export function lowerToIr(
wasmModules.push({ sha256: report.sha256, bytes: encodeBase64(bytes) });
}
result.module.wasmModules = wasmModules;
if (resultLowerer.usesWasmCustomSections) result.module.wasmCustomSections = true;
timing("wasm-discovery", { modules: wasmModules.length });
}
if (options.coverage !== true) return result;
@@ -2851,6 +2852,9 @@ export class Lowerer {
/** The program uses the WebAssembly API (builtins/webassembly.ts): the
* build translates the modules it discovers. */
usesWebAssembly = false;
/** The program calls WebAssembly.Module.customSections: translated
* modules keep their custom sections. */
usesWasmCustomSections = false;
/** The program constructs a LifetimeRegion (lifetime-regions.ts). */
usesLifetimeRegions = false;
/** Per-directory answers of experimental-import.ts's declaration-file test. */
@@ -141,6 +141,14 @@ export const MAY_THROW_LIB_FNS: ReadonlySet<IrLibFn> = new Set([
"wasm.moduleNew",
"wasm.instanceNew",
"wasm.errorNew",
"wasm.memoryNew",
"wasm.tableNew",
"wasm.globalNew",
"wasm.withoutNew",
"wasm.moduleImports",
"wasm.moduleExports",
"wasm.moduleCustomSections",
"wasm.valueOf",
"events.addAbortListener",
"fetch.streamNew",
"fetch.streamFrom",
@@ -122,6 +122,24 @@ export const LIB_FN_SIGS = defineLibFnSignatures({
"wasm.instanceNew": { argTypes: [DYN, DYN], result: DYN },
"wasm.is": { argTypes: [DYN, F64], result: BOOL },
"wasm.errorNew": { argTypes: [F64, DYN, DYN], result: DYN },
/** new WebAssembly.Memory(descriptor), Table(descriptor, value) and
* Global(descriptor, value) (an undefined value is the type's default);
* `wasm.withoutNew` throws the TypeError for Module, Instance, Memory,
* Table or Global (kinds 0-4) called without new, after its arguments
* evaluate. */
"wasm.memoryNew": { argTypes: [DYN], result: DYN },
"wasm.tableNew": { argTypes: [DYN, DYN], result: DYN },
"wasm.globalNew": { argTypes: [DYN, DYN], result: DYN },
"wasm.withoutNew": { argTypes: [F64, DYN, DYN], result: DYN },
/** WebAssembly.Module.imports(module), exports(module) and
* customSections(module, name): checked-dynamic arrays of descriptor
* objects and ArrayBuffers. */
"wasm.moduleImports": { argTypes: [DYN], result: DYN },
"wasm.moduleExports": { argTypes: [DYN], result: DYN },
"wasm.moduleCustomSections": { argTypes: [DYN, DYN], result: DYN },
/** `v.valueOf()` on a WebAssembly-typed receiver: a Global's value,
* dyn.valueOf's answer for anything else. */
"wasm.valueOf": { argTypes: [DYN], result: DYN },
/** WebAssembly.compile (0), instantiate (1) and validate (2) as function
* values, and the namespace object holding them. */
"wasm.function": { argTypes: [F64], result: DYN },
+4
View File
@@ -1049,6 +1049,10 @@ export interface IrModule {
* API (possibly empty). The backend appends each module's translation
* and emits the registry the runtime looks modules up in. */
wasmModules?: IrWasmModule[];
/** The program calls WebAssembly.Module.customSections, so translated
* modules keep their custom sections (otherwise they carry none, and
* debug sections cost nothing). */
wasmCustomSections?: boolean;
}
/** A WebAssembly module whose bytes the build saw, validated within the
+19 -5
View File
@@ -6,7 +6,9 @@
* 1. path: a `.wasm` path the compiler resolves from the source text
* (`require.resolve`, `createRequire(...).resolve`, `import.meta.resolve`,
* `new URL("./x.wasm", import.meta.url)`, and `path.join(__dirname, ...)`
* or `__dirname + "/x.wasm"`);
* or `__dirname + "/x.wasm"`), or a `.so` path spelled the same ways
* whose file is a module (WebAssembly shared libraries, such as
* quickjs-wasi's extensions);
* 2. inline: a string literal that decodes as base64 to a module, or an
* array literal of its bytes;
* 3. package: every `.wasm` file shipped by a reached npm package whose
@@ -78,6 +80,9 @@ const BASE64_MAGIC = "AGFzbQ";
const BYTES_MAGIC = /\[\s*(?:0|0x0+)\s*,\s*(?:97|0x61)\s*,\s*(?:115|0x73)\s*,\s*(?:109|0x6d)\s*,/i;
/** Package directories deeper than this are not searched for modules. */
const PACKAGE_SCAN_DEPTH = 6;
/** A `.so` path in a string literal: the cheap test before a file with no
* `.wasm` mention is walked for shared-library paths. */
const SHARED_LIBRARY_PATH = /\.so["'`]/;
/** Larger files are reported by size but not read. */
const MAX_MODULE_BYTES = 256 * 1024 * 1024;
@@ -139,6 +144,8 @@ class Discovery {
return;
}
if (bytes.length > MAX_MODULE_BYTES) return;
// A shared-library path names a module only when the file is one.
if (source.kind === "path" && !path.endsWith(".wasm") && !isWasmHeader(bytes)) return;
this.byPath.set(path, this.addBytes(bytes, path, source));
}
@@ -146,7 +153,7 @@ class Discovery {
scanFile(sf: ts.SourceFile): void {
const text = sf.text;
const paths = text.includes(".wasm");
const paths = text.includes(".wasm") || SHARED_LIBRARY_PATH.test(text);
const base64 = text.includes(BASE64_MAGIC);
const bytes = BYTES_MAGIC.test(text);
if (!paths && !base64 && !bytes) return;
@@ -184,7 +191,8 @@ class Discovery {
);
}
/** The module path a resolution expression names, when it ends in .wasm. */
/** The module path a resolution expression names, when it ends in .wasm
* (or .so). */
resolvedPath(sf: ts.SourceFile, node: ts.Node): string | null {
const dir = dirname(sf.fileName);
if (ts.isCallExpression(node)) {
@@ -245,7 +253,7 @@ class Discovery {
if (!ts.isStringLiteral(arg) && !ts.isNoSubstitutionTemplateLiteral(arg)) return null;
parts.push(arg.text);
}
if (!parts[parts.length - 1]!.endsWith(".wasm")) return null;
if (!isModulePath(parts[parts.length - 1]!)) return null;
let path = parts[0]!;
for (let i = 1; i < parts.length; i++) path = resolve(path, parts[i]!);
return path;
@@ -314,9 +322,15 @@ function addSource(report: WasmModuleReport, source: WasmSource): void {
report.sources.push(source);
}
/** A path that may name a module: `.wasm`, or `.so` for WebAssembly shared
* libraries (addFile keeps those only when the file is a module). */
function isModulePath(text: string): boolean {
return text.endsWith(".wasm") || text.endsWith(".so");
}
function wasmLiteral(node: ts.Expression): string | null {
if (!ts.isStringLiteral(node) && !ts.isNoSubstitutionTemplateLiteral(node)) return null;
return node.text.endsWith(".wasm") ? node.text : null;
return isModulePath(node.text) ? node.text : null;
}
function isDirname(node: ts.Expression): boolean {
@@ -60,6 +60,43 @@ describe("translateWasmModule", () => {
for (const decl of out.decls) expect(decl).toMatch(/^declare /);
});
test("keeps custom sections only when asked, and gives unexported table functions call thunks", () => {
// (module (type $t (func (param i32) (result i32))) (table 2 funcref)
// (elem (i32.const 0) $hidden $shown) (func $hidden (type $t) local.get 0)
// (func $shown (export "shown") (type $t) local.get 0)), plus a custom
// section "meta" with "hi".
const refs = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 6, 1, 96, 1, 127, 1, 127, 3, 3, 2, 0, 0, 4, 4, 1, 112, 0, 2,
7, 9, 1, 5, 115, 104, 111, 119, 110, 0, 1, 9, 8, 1, 0, 65, 0, 11, 2, 0, 1, 10, 11, 2, 4, 0,
32, 0, 11, 4, 0, 32, 0, 11, 0, 7, 4, 109, 101, 116, 97, 104, 105,
]);
const prefix = wasmSymbolPrefix(SHA);
const plain = translateWasmModule(refs, SHA, { boundsChecks: false }).text.join("\n");
expect(plain).toContain(
`@${prefix}_customs = private constant [0 x { ptr, i64, ptr, i64 }] []`,
);
expect(plain).not.toContain(`c"hi"`);
const kept = translateWasmModule(refs, SHA, { boundsChecks: false, customSections: true });
const text = kept.text.join("\n");
expect(text).toContain(`@${prefix}_c0 = private unnamed_addr constant [2 x i8] c"hi", align 1`);
expect(text).toContain(`@${prefix}_customs = private constant [1 x { ptr, i64, ptr, i64 }]`);
// One call thunk for the signature, used by the unexported function
// only (the exported one has its export thunk).
expect(text).toContain(
`define internal void @${prefix}_k0(ptr %code, ptr %inst, ptr %a, ptr %r)`,
);
expect(text).toContain(`%res = call i32 %code(ptr %inst, i32 `);
expect(text).toContain(
`@${prefix}_calls = private constant [2 x ptr] [ptr @${prefix}_k0, ptr null]`,
);
for (const line of topLevelLines(kept.text)) {
expect(
line === "" || /^(define internal |@[\w.$-]+ = (private|internal) )/.test(line),
line,
).toBe(true);
}
});
test("checks memory accesses explicitly only when asked", () => {
const guarded = translateWasmModule(ADD, SHA, { boundsChecks: false }).text.join("\n");
const checked = translateWasmModule(ADD, SHA, { boundsChecks: true }).text.join("\n");
@@ -67,6 +104,20 @@ describe("translateWasmModule", () => {
expect(checked).toContain("%msize");
});
test("reads a memarg with an explicit memory index as one immediate", () => {
// (func (result i32) (i32.load 0 offset=8 (i32.const 0))) with memory 1:
// memarg flags 0x42 (alignment 2, memory index present), memory 0,
// offset 8, then `end`.
const bytes = new Uint8Array([
0, 97, 115, 109, 1, 0, 0, 0, 1, 5, 1, 96, 0, 1, 127, 3, 2, 1, 0, 5, 3, 1, 0, 1, 10, 10, 1, 8,
0, 65, 0, 40, 66, 0, 8, 11,
]);
expect(analyzeWasm(bytes).status).toBe("valid");
const text = translateWasmModule(bytes, SHA, { boundsChecks: false }).text.join("\n");
expect(text).toContain("add i64 %v1, 8");
expect(text).toContain("load i32, ptr %v3");
});
test("translates every valid module of the WebAssembly spec test suite", () => {
const cases = readFileSync(join(import.meta.dirname, "../../test/wasm-spec-cases.txt"), "utf8")
.split("\n")
+86 -2
View File
@@ -69,7 +69,7 @@ export const TRAP_NULL = 9;
export const TRAP_EXHAUSTION = 100;
/** The descriptor ABI version both sides agree on. */
export const WASM_ABI_VERSION = 1;
export const WASM_ABI_VERSION = 2;
/** Instance slot offsets (bytes). */
const OFF_MEMBASE = 8;
@@ -82,6 +82,8 @@ const FUNCREF_SIZE = 24;
export interface WasmTranslateOptions {
/** Explicit bounds checks instead of guard pages. */
boundsChecks: boolean;
/** Keep the custom sections, for WebAssembly.Module.customSections. */
customSections?: boolean;
}
export interface WasmTranslation {
@@ -388,6 +390,44 @@ class ModuleTranslator {
return name;
}
/** A call thunk for signature `type`: `(code, inst, args, results)`
* calls `code` (a function of that signature) with the arguments in
* 64-bit slots. The runtime calls a function reference that reaches
* JavaScript through it when the module does not export the function. */
callThunk(type: number): string {
const name = `@${this.p}_k${type}`;
const ft = this.m.types[type]!;
const out: string[] = [];
out.push(`define internal void ${name}(ptr %code, ptr %inst, ptr %a, ptr %r) nounwind {`);
out.push(`entry:`);
const args: string[] = ["ptr %inst"];
for (let i = 0; i < ft.params.length; i++) {
out.push(` %ap${i} = getelementptr i64, ptr %a, i64 ${i}`);
out.push(` %as${i} = load i64, ptr %ap${i}`);
args.push(`${llType(ft.params[i]!)} ${fromSlot(out, ft.params[i]!, `%as${i}`, `a${i}`)}`);
}
const ret = retType(ft.results);
if (ft.results.length === 0) {
out.push(` call void %code(${args.join(", ")})`);
} else {
out.push(` %res = call ${ret} %code(${args.join(", ")})`);
for (let i = 0; i < ft.results.length; i++) {
let v = "%res";
if (ft.results.length > 1) {
out.push(` %rv${i} = extractvalue ${ret} %res, ${i}`);
v = `%rv${i}`;
}
const slot = toSlot(out, ft.results[i]!, v, `r${i}`);
out.push(` %rp${i} = getelementptr i64, ptr %r, i64 ${i}`);
out.push(` store i64 ${slot}, ptr %rp${i}`);
}
}
out.push(` ret void`);
out.push(`}`);
this.text.push(out.join("\n"));
return name;
}
/** Emits the init function: globals, table initializers and segments,
* in the specification's order. The runtime calls the start function
* (which takes and returns nothing) separately. */
@@ -538,12 +578,27 @@ class ModuleTranslator {
// every function a reference can name.
const nf = m.funcTypes.length;
const codes: string[] = [];
const callThunks: string[] = [];
const callThunkSyms = new Map<number, string>();
const types: string[] = [];
const sigs: string[] = [];
const sigSyms = new Map<string, string>();
for (let f = 0; f < nf; f++) {
const referenced = this.refs.has(f);
codes.push(referenced ? `ptr ${this.fn(f)}` : "ptr null");
// Referenced functions the module does not export reach JavaScript
// through their signature's call thunk.
if (referenced && !thunks.has(f)) {
const type = m.funcTypes[f]!;
let thunk = callThunkSyms.get(type);
if (thunk === undefined) {
thunk = this.callThunk(type);
callThunkSyms.set(type, thunk);
}
callThunks.push(`ptr ${thunk}`);
} else {
callThunks.push("ptr null");
}
types.push(`i64 ${this.typeHashes[m.funcTypes[f]!]!}`);
const sig = sigString(this.funcType(f));
let sym = sigSyms.get(sig);
@@ -556,7 +611,9 @@ class ModuleTranslator {
const codesSym = `@${p}_codes`;
const typesSym = `@${p}_types`;
const sigsSym = `@${p}_sigs`;
const callThunksSym = `@${p}_calls`;
lines.push(`${codesSym} = private constant [${nf} x ptr] [${codes.join(", ")}]`);
lines.push(`${callThunksSym} = private constant [${nf} x ptr] [${callThunks.join(", ")}]`);
lines.push(`${typesSym} = private constant [${nf} x i64] [${types.join(", ")}]`);
lines.push(`${sigsSym} = private constant [${nf} x ptr] [${sigs.join(", ")}]`);
// Tables.
@@ -624,6 +681,30 @@ class ModuleTranslator {
lines.push(
`${elemsSym} = private constant [${elemRows.length} x { i64, i64, ptr }] [${elemRows.join(", ")}]`,
);
// Custom sections, when the program reads them.
const customRows: string[] = [];
if (this.opts.customSections === true) {
for (let s = 0; s < m.customs.length; s++) {
const custom = m.customs[s]!;
const name = textStr(custom.name);
const len = custom.payload.end - custom.payload.start;
let data = "ptr null";
if (len > 0) {
const sym = `@${p}_c${s}`;
lines.push(
`${sym} = private unnamed_addr constant [${len} x i8] c"${cBytes(m.bytes, custom.payload.start, custom.payload.end)}", align 1`,
);
data = `ptr ${sym}`;
}
customRows.push(
`{ ptr, i64, ptr, i64 } { ptr ${name.sym}, i64 ${name.len}, ${data}, i64 ${len} }`,
);
}
}
const customsSym = `@${p}_customs`;
lines.push(
`${customsSym} = private constant [${customRows.length} x { ptr, i64, ptr, i64 }] [${customRows.join(", ")}]`,
);
const memory = m.memories[0];
const sha: string[] = [];
for (let i = 0; i < 32; i++) sha.push(`i8 ${sha256[i]!}`);
@@ -655,9 +736,12 @@ class ModuleTranslator {
`ptr ${elemsSym}`,
`ptr ${init}`,
`ptr ${m.start === null ? "null" : this.fn(m.start)}`,
`i64 ${customRows.length}`,
`ptr ${customsSym}`,
`ptr ${callThunksSym}`,
];
const descType =
"{ [32 x i8], i64, i64, i64, i64, ptr, i64, ptr, i64, i64, ptr, ptr, ptr, i64, ptr, i64, i64, i64, i64, ptr, i64, ptr, i64, ptr, ptr, ptr }";
"{ [32 x i8], i64, i64, i64, i64, ptr, i64, ptr, i64, i64, ptr, ptr, ptr, i64, ptr, i64, i64, i64, i64, ptr, i64, ptr, i64, ptr, ptr, ptr, i64, ptr, ptr }";
lines.push(`${desc} = internal constant ${descType} { ${fields.join(", ")} }`);
this.text.push(lines.join("\n"));
return desc;
+1 -1
View File
@@ -5714,7 +5714,7 @@
"kind": "stdlib",
"name": "globalThis.WebAssembly",
"status": "static",
"note": "compile, instantiate, validate, Module, Instance, instanceof, the error classes, exports, memories, tables, globals and Module transfer to workers, over the modules translated at build time; the rest refuses with SC2020 (the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations))"
"note": "compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, instanceof, the error classes, exports, imports and Module transfer to workers, over the modules translated at build time; the rest refuses with SC2020 (the streaming functions, Tag and Exception, and the classes used as values other than with new and instanceof are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations))"
},
{
"id": "stdlib.webcrypto.CryptoKey",
@@ -0,0 +1,7 @@
{
"order": [
"<repo>/tests/corpus/webassembly-dylink/extensions.js",
"<repo>/tests/corpus/webassembly-dylink/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/webassembly-jsapi.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/webassembly-lifetimes.mjs"
],
"diags": []
}
@@ -0,0 +1,8 @@
{
"order": [
"<repo>/tests/wasm-spec/runner.mjs",
"<repo>/tests/corpus/webassembly-spec-imports/data.mjs",
"<repo>/tests/corpus/webassembly-spec-imports/main.mjs"
],
"diags": []
}
@@ -0,0 +1,6 @@
{
"order": [
"<repo>/tests/corpus/webassembly-wpt-jsapi.mjs"
],
"diags": []
}
+512 -52
View File
@@ -36,12 +36,16 @@
* LinkError or RuntimeError (not an own property, as in Node), which is
* the brand instanceof tests.
*
* Gaps (results/opt-wasm-design/DESIGN.md, stages W5-W7): no Memory,
* Table or Global constructors; Memory.grow does not detach the previous
* ArrayBuffer (the memory never moves, so old views stay valid with their
* old length); instances, their imports and exports are never freed (the
* cycle collector does not trace them yet); funcref values cross to
* JavaScript only as exported functions. */
* The Memory, Table and Global constructors build the same objects an
* instance exports, so either kind can be imported; WebAssembly.Module.
* imports, exports and customSections read the descriptor.
*
* Gaps (results/opt-wasm-design/DESIGN.md, stages W6-W9): Memory.grow
* does not detach the previous ArrayBuffer (the memory never moves with
* guard pages, and explicit-bounds memories keep their previous blocks, so
* old views stay valid with their old length); shared memories are not
* constructed; instances, their imports and exports are never freed (the
* cycle collector does not trace them yet). */
#if !defined(_GNU_SOURCE) && defined(__linux__)
#define _GNU_SOURCE /* REG_RIP and friends in <ucontext.h> */
#endif
@@ -86,6 +90,25 @@
#define SCR_WASM_ASAN 1
#endif
ScrPromise *scr_wasm_compile(ScrDyn *bytes);
bool scr_wasm_validate(ScrDyn *bytes);
ScrPromise *scr_wasm_instantiate(ScrDyn *source, ScrDyn *imports);
ScrDyn *scr_wasm_module_new(ScrDyn *bytes);
ScrDyn *scr_wasm_instance_new(ScrDyn *module, ScrDyn *imports);
bool scr_wasm_is(const ScrDyn *value, double kind);
ScrDyn *scr_wasm_error_new(double kind, ScrDyn *message, ScrDyn *options);
ScrDyn *scr_wasm_function(double kind);
ScrDyn *scr_wasm_namespace(void);
ScrDyn *scr_wasm_global_view(void);
ScrDyn *scr_wasm_memory_construct(ScrDyn *descriptor);
ScrDyn *scr_wasm_table_construct(ScrDyn *descriptor, ScrDyn *value);
ScrDyn *scr_wasm_global_construct(ScrDyn *descriptor, ScrDyn *value);
ScrDyn *scr_wasm_construct_without_new(double kind, ScrDyn *arg0, ScrDyn *arg1);
ScrDyn *scr_wasm_module_imports(ScrDyn *module);
ScrDyn *scr_wasm_module_exports(ScrDyn *module);
ScrDyn *scr_wasm_module_custom_sections(ScrDyn *module, ScrDyn *name);
ScrDyn *scr_wasm_value_of(ScrDyn *value);
/* Entry points of translated code. */
/* Sanitized builds do not declare it noreturn: AddressSanitizer would
* call __asan_handle_no_return before every call, which warns on the
@@ -134,6 +157,12 @@ typedef struct ScrWasmInst {
bool *data_dropped;
bool *elem_dropped;
ScrDyn *exports;
/* The Memory, Table and Global objects of the instance, by index: the
* imported object itself, or the one created when the module exports it,
* so every export of the same index is the same object, as in V8. */
ScrDyn *memory_obj;
ScrDyn **table_objs;
ScrDyn **global_objs;
} ScrWasmInst;
static ScrWasmInst *scr_wasm_host_of(void *inst) { return ((ScrWasmInst **)inst)[0]; }
@@ -387,9 +416,10 @@ static void scr_wasm_set_stack_limit(void) {
#endif
}
/* Calls a thunk with traps caught; answers a SCR_WASM_TRAP_* code. */
static __attribute__((noinline)) int scr_wasm_enter(ScrWasmThunk fn, void *inst, const uint64_t *args,
uint64_t *results) {
/* Calls a thunk (or `call` on `code`, a function reference's code) with
* traps caught; answers a SCR_WASM_TRAP_* code. */
static __attribute__((noinline)) int scr_wasm_enter_ref(ScrWasmThunk fn, ScrWasmCallThunk call, void *ref_code,
void *inst, const uint64_t *args, uint64_t *results) {
#if SCR_WASM_SIGNALS
scr_wasm_thread_init();
#endif
@@ -407,7 +437,8 @@ static __attribute__((noinline)) int scr_wasm_enter(ScrWasmThunk fn, void *inst,
int code = 0;
if (SCR_WASM_SETJMP(entry.jmp) == 0) {
scr_wasm_running = true;
fn(inst, args, results);
if (call) call(ref_code, inst, args, results);
else fn(inst, args, results);
} else {
code = scr_wasm_trap_code;
}
@@ -416,6 +447,10 @@ static __attribute__((noinline)) int scr_wasm_enter(ScrWasmThunk fn, void *inst,
return code;
}
static int scr_wasm_enter(ScrWasmThunk fn, void *inst, const uint64_t *args, uint64_t *results) {
return scr_wasm_enter_ref(fn, NULL, NULL, inst, args, results);
}
/* Called by each caller of scr_wasm_enter once it has converted the
* call's results: after the outermost call, the references kept for it
* (import results, and references a table or global replaced while the
@@ -686,9 +721,17 @@ static ScrWasmMemory *scr_wasm_memory_retain(ScrWasmMemory *m) {
return m;
}
static void scr_wasm_memory_release(ScrWasmMemory *m) {
if (!m || --m->rc) return;
scr_dyn_release(m->buffer);
/* True when JavaScript holds none of the memory's ArrayBuffers or views
* over them: each byte store's only reference is the memory's own. */
static bool scr_wasm_memory_unexposed(const ScrWasmMemory *m) {
for (size_t i = 0; i < m->exposed_count; i++)
if (m->exposed[i]->rc != 1) return false;
return true;
}
static void scr_wasm_memory_free(ScrWasmMemory *m) {
for (size_t i = 0; i < m->exposed_count; i++) scr_bytes_release(m->exposed[i]);
free(m->exposed);
#if SCR_WASM_SIGNALS
if (m->guarded) {
scr_wasm_range_remove(m->base);
@@ -699,14 +742,46 @@ static void scr_wasm_memory_release(ScrWasmMemory *m) {
free(m);
}
/* A memory of `min` pages growing to at most `max` pages; NULL with
* Node's RangeError pending when the address space or memory is short. */
/* Released memories whose storage JavaScript still reaches through a
* buffer; freed once it no longer does (checked whenever a memory is
* created or released). */
static _Thread_local ScrWasmMemory *scr_wasm_parked;
static void scr_wasm_sweep_parked(void) {
for (ScrWasmMemory **p = &scr_wasm_parked; *p;) {
ScrWasmMemory *m = *p;
if (scr_wasm_memory_unexposed(m)) {
*p = m->next_parked;
scr_wasm_memory_free(m);
} else {
p = &m->next_parked;
}
}
}
static void scr_wasm_memory_release(ScrWasmMemory *m) {
if (!m || --m->rc) return;
scr_dyn_release(m->buffer);
m->buffer = NULL;
scr_wasm_sweep_parked();
if (scr_wasm_memory_unexposed(m)) {
scr_wasm_memory_free(m);
} else {
m->next_parked = scr_wasm_parked;
scr_wasm_parked = m;
}
}
/* A memory of `min` pages growing to at most `max` pages (UINT64_MAX:
* no declared maximum); NULL with Node's RangeError pending when the
* address space or memory is short. */
static ScrWasmMemory *scr_wasm_memory_new(uint64_t min, uint64_t max, bool guarded, const char *what) {
if (max > SCR_WASM_MAX_PAGES) max = SCR_WASM_MAX_PAGES;
if (scr_wasm_parked) scr_wasm_sweep_parked();
ScrWasmMemory *m = calloc(1, sizeof *m);
if (!m) scr_trap("scriptc: out of memory\n");
m->rc = 1;
m->max_pages = max;
m->declared_max = max;
m->max_pages = max > SCR_WASM_MAX_PAGES ? SCR_WASM_MAX_PAGES : max;
m->size = min * SCR_WASM_PAGE;
#if SCR_WASM_SIGNALS
if (guarded) {
@@ -733,7 +808,7 @@ static ScrWasmMemory *scr_wasm_memory_new(uint64_t min, uint64_t max, bool guard
(void)guarded;
#endif
#if SCR_WASM_STABLE_RESERVE
uint64_t reserve = max * SCR_WASM_PAGE;
uint64_t reserve = m->max_pages * SCR_WASM_PAGE;
m->base = reserve ? scr_wasm_reserve(reserve) : NULL;
if (reserve && (!m->base || !scr_wasm_commit(m->base, 0, m->size))) {
if (m->base) {
@@ -851,6 +926,7 @@ static ScrWasmTable *scr_wasm_table_new(char elem, uint64_t min, uint64_t max) {
t->rc = 1;
t->elem = elem;
t->size = (uint32_t)min;
t->declared_max = max;
t->max = max > SCR_WASM_MAX_TABLE ? SCR_WASM_MAX_TABLE : (uint32_t)max;
t->elems = calloc(min ? min : 1, sizeof *t->elems);
if (!t->elems) scr_trap("scriptc: out of memory\n");
@@ -1100,9 +1176,15 @@ static uint32_t scr_wasm_to_int32(double n) {
static ScrWasmFuncRef *scr_wasm_funcref_of(const ScrDyn *v);
/* V8's message for a value that is neither null nor an exported
* WebAssembly function where a funcref is expected (behind a prefix that
* names the operation). */
#define SCR_WASM_FUNCREF_EXPECTED "function-typed object must be null (if nullable) or a Wasm function object"
/* ToWebAssemblyValue: false with an exception pending. References are
* borrowed from `v`. */
static bool scr_wasm_from_js(char t, const ScrDyn *v, uint64_t *out) {
* borrowed from `v`. A funcref mismatch throws a TypeError with
* `funcref_error` (the export-call message when NULL). */
static bool scr_wasm_from_js_as(char t, const ScrDyn *v, uint64_t *out, const char *funcref_error) {
if (!v) v = scr_dyn_undefined();
switch (t) {
case 'i': {
@@ -1140,7 +1222,7 @@ static bool scr_wasm_from_js(char t, const ScrDyn *v, uint64_t *out) {
}
ScrWasmFuncRef *ref = scr_wasm_funcref_of(v);
if (!ref) {
scr_wasm_type_error("type incompatibility when transforming from/to JS");
scr_wasm_type_error(funcref_error ? funcref_error : "type incompatibility when transforming from/to JS");
return false;
}
*out = (uint64_t)(uintptr_t)ref;
@@ -1156,6 +1238,14 @@ static bool scr_wasm_from_js(char t, const ScrDyn *v, uint64_t *out) {
}
}
static bool scr_wasm_from_js(char t, const ScrDyn *v, uint64_t *out) { return scr_wasm_from_js_as(t, v, out, NULL); }
/* DefaultValue(type): what a missing initial value of a constructed
* global or table, or a missing table.grow/set value, stands for. */
static uint64_t scr_wasm_default_value(char t) {
return t == 'x' ? (uint64_t)(uintptr_t)scr_dyn_undefined() : 0;
}
static size_t scr_wasm_params(const char *sig) {
size_t n = 0;
while (sig[n] && sig[n] != ':') n++;
@@ -1193,7 +1283,11 @@ static ScrDyn *scr_wasm_export_call(ScrClosure *clo, ScrDyn *const *args, size_t
bool ok = true;
for (size_t i = 0; i < np && ok; i++) ok = scr_wasm_from_js(sig[i], i < argc ? args[i] : NULL, &a[i]);
if (ok) {
int trap = scr_wasm_enter(thunk, host->inst, a, r);
/* A function the module does not export runs through its signature's
* call thunk, on the code its reference record names. */
int trap = thunk ? scr_wasm_enter(thunk, host->inst, a, r)
: scr_wasm_enter_ref(NULL, host->desc->func_call_thunks[func], host->funcrefs[func].code,
host->inst, a, r);
if (trap) {
scr_wasm_throw_trap(trap);
} else if (nr == 0) {
@@ -1212,18 +1306,20 @@ static ScrDyn *scr_wasm_export_call(ScrClosure *clo, ScrDyn *const *args, size_t
return result;
}
/* The function value of exported function `func` (+1), created once per
* instance and function; NULL with a TypeError pending for a function the
* module does not export (funcref values of other functions are stage W5). */
/* The function value of function `func` (+1), created once per instance
* and function: exported functions call through their export thunk, other
* referenced functions through their signature's call thunk. */
static ScrDyn *scr_wasm_function_value(ScrWasmInst *host, uint32_t func) {
if (host->export_fns[func]) return scr_dyn_retain(host->export_fns[func]);
bool exported = false;
for (uint64_t i = 0; i < host->desc->export_count; i++) {
bool callable = host->desc->func_call_thunks[func] != NULL && host->funcrefs[func].code != NULL;
for (uint64_t i = 0; i < host->desc->export_count && !callable; i++) {
const ScrWasmExportDesc *ex = &host->desc->exports[i];
if (ex->kind == SCR_WASM_FUNC && ex->index == func) exported = true;
if (ex->kind == SCR_WASM_FUNC && ex->index == func) callable = true;
}
if (!exported) {
scr_wasm_type_error("WebAssembly function references to functions a module does not export are not supported yet");
if (!callable) {
/* Unreachable for valid modules: every function a reference can name
* is exported or has a call thunk. */
scr_wasm_type_error("type incompatibility when transforming from/to JS");
return NULL;
}
char name[24];
@@ -1313,8 +1409,18 @@ static ScrDyn *scr_wasm_memory_buffer(ScrWasmMemory *m) {
m->buffer_exposed = true;
m->buffer_size = m->size;
ScrBytes *bytes = scr_bytes_from_external(m->base, (size_t)m->size);
/* Earlier buffers JavaScript no longer holds need no tracking. */
size_t kept = 0;
for (size_t i = 0; i < m->exposed_count; i++) {
if (m->exposed[i]->rc == 1) scr_bytes_release(m->exposed[i]);
else m->exposed[kept++] = m->exposed[i];
}
m->exposed_count = kept;
ScrBytes **grown = realloc(m->exposed, (m->exposed_count + 1) * sizeof *grown);
if (!grown) scr_trap("scriptc: out of memory\n");
m->exposed = grown;
m->exposed[m->exposed_count++] = bytes; /* the memory's reference */
m->buffer = scr_array_buffer_from_bytes(bytes);
scr_bytes_release(bytes);
}
return scr_dyn_retain(m->buffer);
}
@@ -1347,7 +1453,8 @@ static bool scr_wasm_global_assign(ScrWasmGlobal *g, const ScrDyn *value) {
return false;
}
uint64_t v;
if (!scr_wasm_from_js(g->type, value, &v)) return false;
if (!scr_wasm_from_js_as(g->type, value, &v, "set WebAssembly.Global.value): " SCR_WASM_FUNCREF_EXPECTED))
return false;
if (g->type == 'x') scr_wasm_global_set_ref(g, (void *)(uintptr_t)v);
else g->value = v;
return true;
@@ -1362,12 +1469,20 @@ static bool scr_wasm_set(void *h, const char *key, size_t len, const ScrDyn *val
return false;
}
/* V8's EnforceUint32: ToNumber (its own TypeError for symbols and
* BigInts), then finite, non-negative and at most 2^32-1, truncated.
* `what` names the value ("WebAssembly.Memory.grow(): Argument 0",
* "WebAssembly.Memory(): Property 'initial'"). */
static bool scr_wasm_u32_arg(const ScrDyn *v, const char *what, uint32_t *out) {
double n;
if (!scr_dyn_number_coerce_js(v ? v : scr_dyn_undefined(), &n)) return false;
if (!isfinite(n) || n < 0 || trunc(n) > 4294967295.0) {
const char *problem = !isfinite(n) ? "must be convertible to a valid number"
: n < 0 ? "must be non-negative"
: trunc(n) > 4294967295.0 ? "must be in the unsigned long range"
: NULL;
if (problem) {
char msg[200];
snprintf(msg, sizeof msg, "%s must be convertible to a valid number", what);
snprintf(msg, sizeof msg, "%s %s", what, problem);
scr_wasm_type_error(msg);
return false;
}
@@ -1388,7 +1503,7 @@ static ScrDyn *scr_wasm_invoke(void *h, ScrDyn *self, const char *method, ScrDyn
return NULL;
}
if (o->inst && o->inst->memory == o->memory) o->inst->inst[1] = o->memory->base;
/* The previous buffer keeps its length (no detachment yet, W5). */
/* The previous buffer keeps its length (no detachment). */
scr_dyn_release(o->memory->buffer);
o->memory->buffer = NULL;
return scr_dyn_new_num((double)old);
@@ -1417,15 +1532,26 @@ static ScrDyn *scr_wasm_invoke(void *h, ScrDyn *self, const char *method, ScrDyn
scr_wasm_range_error(msg);
return NULL;
}
const ScrDyn *v = argc > 1 ? args[1] : (t->elem == 'x' ? scr_dyn_undefined() : scr_dyn_new_null());
if (!scr_wasm_from_js(t->elem, v, &value)) return NULL;
if (argc > 1) {
if (!scr_wasm_from_js_as(t->elem, args[1], &value,
"WebAssembly.Table.set(): Argument 1 is invalid for table: " SCR_WASM_FUNCREF_EXPECTED))
return NULL;
} else {
value = scr_wasm_default_value(t->elem);
}
scr_wasm_table_store(t, index, (void *)(uintptr_t)value);
return scr_dyn_retain(scr_dyn_undefined());
}
if (!strcmp(method, "grow")) {
uint32_t delta;
if (!scr_wasm_u32_arg(argc ? args[0] : NULL, "WebAssembly.Table.grow(): Argument 0", &delta)) return NULL;
if (argc > 1 && !scr_wasm_from_js(t->elem, args[1], &value)) return NULL;
if (argc > 1) {
if (!scr_wasm_from_js_as(t->elem, args[1], &value,
"WebAssembly.Table.grow(): Argument 1 is invalid: " SCR_WASM_FUNCREF_EXPECTED))
return NULL;
} else {
value = scr_wasm_default_value(t->elem);
}
int64_t old = scr_wasm_table_grow_by(t, delta, (void *)(uintptr_t)value);
if (old < 0) {
char msg[160];
@@ -1648,10 +1774,30 @@ static ScrDyn *scr_wasm_instance_from(const ScrWasmModuleDesc *desc, ScrDyn *imp
}
for (uint64_t i = 0; ok && i < n; i++) {
const ScrWasmImportDesc *im = &desc->imports[i];
if (im->kind == SCR_WASM_MEMORY && !scr_wasm_unbox(values[i], SCR_WASM_K_MEMORY)) {
if (im->kind != SCR_WASM_MEMORY) continue;
ScrWasmObj *mo = scr_wasm_unbox(values[i], SCR_WASM_K_MEMORY);
if (!mo) {
scr_wasm_link_error(what, i, im, "memory import must be a WebAssembly.Memory object");
ok = false;
break;
}
/* V8's limit checks, with its messages (a missing maximum prints as
* the unsigned -1). */
char reason[200];
uint64_t pages = mo->memory->size / SCR_WASM_PAGE;
if (pages < desc->memory_min) {
snprintf(reason, sizeof reason, "memory import has %llu pages which is smaller than the declared initial of %llu",
(unsigned long long)pages, (unsigned long long)desc->memory_min);
} else if (desc->memory_max != UINT64_MAX && mo->memory->declared_max == UINT64_MAX) {
snprintf(reason, sizeof reason, "memory import has no maximum limit, expected at most 4294967295");
} else if (desc->memory_max != UINT64_MAX && mo->memory->declared_max > desc->memory_max) {
snprintf(reason, sizeof reason, "memory import has a larger maximum size %llu than the module's declared maximum %llu",
(unsigned long long)mo->memory->declared_max, (unsigned long long)desc->memory_max);
} else {
continue;
}
scr_wasm_link_error(what, i, im, reason);
ok = false;
}
for (uint64_t i = 0; ok && i < n; i++) {
const ScrWasmImportDesc *im = &desc->imports[i];
@@ -1666,9 +1812,26 @@ static ScrDyn *scr_wasm_instance_from(const ScrWasmModuleDesc *desc, ScrDyn *imp
ok = false;
} else if (im->kind == SCR_WASM_TABLE) {
ScrWasmObj *t = scr_wasm_unbox(v, SCR_WASM_K_TABLE);
const ScrWasmTableDesc *td = &desc->tables[im->index];
if (!t) {
scr_wasm_link_error(what, i, im, "table import requires a WebAssembly.Table");
ok = false;
} else if (t->table->size < td->min || (td->max != UINT64_MAX && t->table->declared_max > td->max)) {
/* V8's size checks name the import by number only; the imported
* maximum prints in hexadecimal. */
char msg[300];
if (t->table->size < td->min)
snprintf(msg, sizeof msg, "%s: table import %llu is smaller than initial %llu, got %u", what,
(unsigned long long)i, (unsigned long long)td->min, t->table->size);
else if (t->table->declared_max == UINT64_MAX)
snprintf(msg, sizeof msg, "%s: table import %llu has no maximum length; required: %llu", what,
(unsigned long long)i, (unsigned long long)td->max);
else
snprintf(msg, sizeof msg, "%s: table import %llu has a larger maximum size %llx than the module's declared maximum %llu",
what, (unsigned long long)i, (unsigned long long)t->table->declared_max,
(unsigned long long)td->max);
scr_wasm_throw_kind(SCR_WASM_LINK_ERROR, msg);
ok = false;
} else if (t->table->elem != im->type[0]) {
scr_wasm_link_error(what, i, im, "imported table does not match the expected type");
ok = false;
@@ -1678,16 +1841,24 @@ static ScrDyn *scr_wasm_instance_from(const ScrWasmModuleDesc *desc, ScrDyn *imp
bool mutable_ = desc->globals[im->index].mutable_ != 0;
char type = im->type[0];
if (g) {
if (g->global->type != type || g->global->mutable_ != mutable_) {
if (g->global->mutable_ != mutable_) {
scr_wasm_link_error(what, i, im, "imported global does not match the expected mutability");
ok = false;
} else if (g->global->type != type) {
scr_wasm_link_error(what, i, im, "imported global does not match the expected type");
ok = false;
}
} else if (mutable_) {
scr_wasm_link_error(what, i, im, "imported mutable global must be a WebAssembly.Global object");
ok = false;
} else if (type == 'r') {
if (!v || (v->kind != SCR_DYN_NULL && !scr_wasm_funcref_of(v))) {
scr_wasm_link_error(what, i, im, SCR_WASM_FUNCREF_EXPECTED);
ok = false;
}
} else if (type == 'I' ? !(v && v->kind == SCR_DYN_BIGINT)
: type == 'x' || type == 'r' ? false
: !(v && v->kind == SCR_DYN_NUM)) {
: type == 'x' ? false
: !(v && v->kind == SCR_DYN_NUM)) {
scr_wasm_link_error(what, i, im,
"global import must be a number, valid Wasm reference, or WebAssembly.Global object");
ok = false;
@@ -1715,8 +1886,11 @@ static ScrDyn *scr_wasm_instance_from(const ScrWasmModuleDesc *desc, ScrDyn *imp
host->globals = calloc(desc->global_count ? desc->global_count : 1, sizeof *host->globals);
host->data_dropped = calloc(desc->data_count ? desc->data_count : 1, 1);
host->elem_dropped = calloc(desc->elem_count ? desc->elem_count : 1, 1);
host->table_objs = calloc(desc->table_count ? desc->table_count : 1, sizeof *host->table_objs);
host->global_objs = calloc(desc->global_count ? desc->global_count : 1, sizeof *host->global_objs);
if (!host->inst || !host->funcs || !host->funcrefs || !host->export_fns || !host->export_names ||
!host->tables || !host->globals || !host->data_dropped || !host->elem_dropped)
!host->tables || !host->globals || !host->data_dropped || !host->elem_dropped || !host->table_objs ||
!host->global_objs)
scr_trap("scriptc: out of memory\n");
void **inst = host->inst;
inst[0] = host;
@@ -1739,14 +1913,20 @@ static ScrDyn *scr_wasm_instance_from(const ScrWasmModuleDesc *desc, ScrDyn *imp
break;
case SCR_WASM_MEMORY:
host->memory = scr_wasm_memory_retain(scr_wasm_unbox(v, SCR_WASM_K_MEMORY)->memory);
host->memory_obj = v;
values[i] = NULL;
break;
case SCR_WASM_TABLE:
host->tables[im->index] = scr_wasm_table_retain(scr_wasm_unbox(v, SCR_WASM_K_TABLE)->table);
host->table_objs[im->index] = v;
values[i] = NULL;
break;
default: {
ScrWasmObj *g = scr_wasm_unbox(v, SCR_WASM_K_GLOBAL);
if (g) {
host->globals[im->index] = scr_wasm_global_retain(g->global);
host->global_objs[im->index] = v;
values[i] = NULL;
} else {
char type = im->type[0];
ScrWasmGlobal *cell = scr_wasm_global_new(type, false);
@@ -1763,9 +1943,7 @@ static ScrDyn *scr_wasm_instance_from(const ScrWasmModuleDesc *desc, ScrDyn *imp
}
free(values);
if (desc->memory_count && !host->memory) {
host->memory = scr_wasm_memory_new(desc->memory_min, desc->memory_max == UINT64_MAX ? SCR_WASM_MAX_PAGES
: desc->memory_max,
scr_wasm_guarded(desc), what);
host->memory = scr_wasm_memory_new(desc->memory_min, desc->memory_max, scr_wasm_guarded(desc), what);
if (!host->memory) return NULL;
}
if (host->memory) {
@@ -1824,12 +2002,22 @@ static ScrDyn *scr_wasm_instance_from(const ScrWasmModuleDesc *desc, ScrDyn *imp
int kind = ex->kind == SCR_WASM_MEMORY ? SCR_WASM_K_MEMORY
: ex->kind == SCR_WASM_TABLE ? SCR_WASM_K_TABLE
: SCR_WASM_K_GLOBAL;
ScrWasmObj *o = scr_wasm_obj_new(kind);
o->inst = scr_wasm_inst_retain(host);
if (kind == SCR_WASM_K_MEMORY) o->memory = scr_wasm_memory_retain(host->memory);
else if (kind == SCR_WASM_K_TABLE) o->table = scr_wasm_table_retain(host->tables[ex->index]);
else o->global = scr_wasm_global_retain(host->globals[ex->index]);
value = scr_wasm_box(o);
/* V8 exports an imported object itself, and one object per memory
* and table; a defined global gets a new object per export. */
ScrDyn **slot = kind == SCR_WASM_K_MEMORY ? &host->memory_obj
: kind == SCR_WASM_K_TABLE ? &host->table_objs[ex->index]
: &host->global_objs[ex->index];
if (*slot) {
value = scr_dyn_retain(*slot);
} else {
ScrWasmObj *o = scr_wasm_obj_new(kind);
o->inst = scr_wasm_inst_retain(host);
if (kind == SCR_WASM_K_MEMORY) o->memory = scr_wasm_memory_retain(host->memory);
else if (kind == SCR_WASM_K_TABLE) o->table = scr_wasm_table_retain(host->tables[ex->index]);
else o->global = scr_wasm_global_retain(host->globals[ex->index]);
value = scr_wasm_box(o);
if (kind != SCR_WASM_K_GLOBAL) *slot = scr_dyn_retain(value);
}
}
scr_dyn_obj_set(exports, ex->name, (size_t)ex->name_len, value);
}
@@ -1851,6 +2039,273 @@ ScrDyn *scr_wasm_instance_new(ScrDyn *module, ScrDyn *imports) {
return scr_wasm_instance_from(m->desc, imports, "WebAssembly.Instance()");
}
/* ── constructors and Module's static functions ────────────────────── */
/* Memories the program constructs are guarded exactly when the translated
* modules are (every module of a build shares the mode), so any of them
* can import one. */
static bool scr_wasm_build_guarded(void) {
#if SCR_WASM_SIGNALS
return scr_wasm_registry.count > 0 && !(scr_wasm_registry.modules[0]->flags & SCR_WASM_DESC_BOUNDS_CHECKS);
#else
return false;
#endif
}
/* V8's GetOptionalIntegerProperty: the descriptor's `name` property as an
* unsigned 32-bit integer within [lower, upper]. An absent or undefined
* property leaves *out unchanged, or throws "is required" when
* `required`. False with an exception pending. */
static bool scr_wasm_limit(const ScrDyn *descriptor, const char *name, bool required, const char *what,
uint64_t lower, uint64_t upper, uint64_t *out) {
ScrDyn *v = scr_wasm_member(descriptor, name, strlen(name));
if (scr_exc_pending()) return false;
char msg[240];
if (!v) {
if (!required) return true;
snprintf(msg, sizeof msg, "%s: Property '%s' is required", what, name);
scr_wasm_type_error(msg);
return false;
}
char label[120];
snprintf(label, sizeof label, "%s: Property '%s'", what, name);
uint32_t n;
bool ok = scr_wasm_u32_arg(v, label, &n);
scr_dyn_release(v);
if (!ok) return false;
/* V8 prints the lower bound in hexadecimal. */
if (n < lower) {
snprintf(msg, sizeof msg, "%s: Property '%s': value %u is below the lower bound %llx", what, name, n,
(unsigned long long)lower);
scr_wasm_range_error(msg);
return false;
}
if (n > upper) {
snprintf(msg, sizeof msg, "%s: Property '%s': value %u is above the upper bound %llu", what, name, n,
(unsigned long long)upper);
scr_wasm_range_error(msg);
return false;
}
*out = n;
return true;
}
/* ToString of a descriptor's type property (+1; NULL with an exception
* pending). An absent property reads as "undefined". */
static ScrStr *scr_wasm_type_name(const ScrDyn *descriptor, const char *name) {
ScrDyn *v = scr_wasm_member(descriptor, name, strlen(name));
if (scr_exc_pending()) return NULL;
if (!v) return scr_str_new("undefined", 9);
ScrStr *s = scr_dyn_string_coerce_js(v);
scr_dyn_release(v);
return s;
}
static bool scr_wasm_str_is(const ScrStr *s, const char *text) {
size_t n = strlen(text);
return s->len == n && !memcmp(s->data, text, n);
}
/* A descriptor's boolean property (ToBoolean; false when absent). */
static bool scr_wasm_flag(const ScrDyn *descriptor, const char *name, bool *out) {
ScrDyn *v = scr_wasm_member(descriptor, name, strlen(name));
if (scr_exc_pending()) return false;
*out = v && scr_dyn_truthy(v);
scr_dyn_release(v);
return true;
}
static bool scr_wasm_absent(const ScrDyn *v) { return !v || v->kind == SCR_DYN_UNDEF; }
ScrDyn *scr_wasm_memory_construct(ScrDyn *descriptor) {
scr_wasm_install();
const char *what = "WebAssembly.Memory()";
if (!scr_wasm_is_object(descriptor)) {
scr_wasm_type_error("WebAssembly.Memory(): Argument 0 must be a memory descriptor");
return NULL;
}
uint64_t initial = 0, maximum = UINT64_MAX;
bool shared;
if (!scr_wasm_limit(descriptor, "initial", true, what, 0, SCR_WASM_MAX_PAGES, &initial) ||
!scr_wasm_limit(descriptor, "maximum", false, what, initial, SCR_WASM_MAX_PAGES, &maximum) ||
!scr_wasm_flag(descriptor, "shared", &shared))
return NULL;
if (shared && maximum == UINT64_MAX) {
scr_wasm_type_error("WebAssembly.Memory(): If shared is true, maximum property should be defined.");
return NULL;
}
if (shared) {
scr_wasm_type_error("WebAssembly.Memory(): shared memories are not supported yet (scriptc does not translate "
"WebAssembly threads; see https://scriptc.dev/limitations)");
return NULL;
}
ScrWasmMemory *m = scr_wasm_memory_new(initial, maximum, scr_wasm_build_guarded(), what);
if (!m) return NULL;
ScrWasmObj *o = scr_wasm_obj_new(SCR_WASM_K_MEMORY);
o->memory = m;
return scr_wasm_box(o);
}
ScrDyn *scr_wasm_table_construct(ScrDyn *descriptor, ScrDyn *value) {
scr_wasm_install();
const char *what = "WebAssembly.Table()";
if (!scr_wasm_is_object(descriptor)) {
scr_wasm_type_error("WebAssembly.Table(): Argument 0 must be a table descriptor");
return NULL;
}
ScrStr *type = scr_wasm_type_name(descriptor, "element");
if (!type) return NULL;
char elem = scr_wasm_str_is(type, "anyfunc") ? 'r' : scr_wasm_str_is(type, "externref") ? 'x' : 0;
scr_str_release(type);
if (!elem) {
scr_wasm_type_error("WebAssembly.Table(): Descriptor property 'element' must be a WebAssembly reference type");
return NULL;
}
uint64_t initial = 0, maximum = UINT64_MAX;
if (!scr_wasm_limit(descriptor, "initial", true, what, 0, SCR_WASM_MAX_TABLE, &initial) ||
!scr_wasm_limit(descriptor, "maximum", false, what, initial, 4294967295u, &maximum))
return NULL;
uint64_t init = scr_wasm_default_value(elem);
if (!scr_wasm_absent(value) &&
!scr_wasm_from_js_as(elem, value, &init,
"WebAssembly.Table(): Argument 2 must be undefined or a value of type compatible with the "
"type of the new table: " SCR_WASM_FUNCREF_EXPECTED "."))
return NULL;
ScrWasmTable *t = scr_wasm_table_new(elem, initial, maximum);
for (uint32_t i = 0; i < t->size; i++) t->elems[i] = scr_wasm_ref_retain(elem, (void *)(uintptr_t)init);
ScrWasmObj *o = scr_wasm_obj_new(SCR_WASM_K_TABLE);
o->table = t;
return scr_wasm_box(o);
}
ScrDyn *scr_wasm_global_construct(ScrDyn *descriptor, ScrDyn *value) {
scr_wasm_install();
if (!scr_wasm_is_object(descriptor)) {
scr_wasm_type_error("WebAssembly.Global(): Argument 0 must be a global descriptor");
return NULL;
}
bool mutable_;
if (!scr_wasm_flag(descriptor, "mutable", &mutable_)) return NULL;
ScrStr *name = scr_wasm_type_name(descriptor, "value");
if (!name) return NULL;
static const char *const names[] = {"i32", "i64", "f32", "f64", "externref", "anyfunc"};
static const char letters[] = {'i', 'I', 'f', 'F', 'x', 'r'};
char type = 0;
for (size_t i = 0; i < sizeof letters; i++)
if (scr_wasm_str_is(name, names[i])) type = letters[i];
bool v128 = scr_wasm_str_is(name, "v128");
scr_str_release(name);
if (v128) {
scr_wasm_type_error("WebAssembly.Global(): A global of type 'v128' cannot be created in JavaScript");
return NULL;
}
if (!type) {
scr_wasm_type_error("WebAssembly.Global(): Descriptor property 'value' must be a WebAssembly type");
return NULL;
}
uint64_t v = scr_wasm_default_value(type);
if (!scr_wasm_absent(value) &&
!scr_wasm_from_js_as(type, value, &v, "WebAssembly.Global(): " SCR_WASM_FUNCREF_EXPECTED))
return NULL;
ScrWasmGlobal *g = scr_wasm_global_new(type, mutable_);
if (type == 'x') scr_wasm_global_set_ref(g, (void *)(uintptr_t)v);
else g->value = v;
ScrWasmObj *o = scr_wasm_obj_new(SCR_WASM_K_GLOBAL);
o->global = g;
return scr_wasm_box(o);
}
/* `WebAssembly.Module(...)` and the other classes called without new. */
ScrDyn *scr_wasm_construct_without_new(double kind, ScrDyn *arg0, ScrDyn *arg1) {
(void)arg0;
(void)arg1;
static const char *const names[] = {"Module", "Instance", "Memory", "Table", "Global"};
const char *name = names[(int)kind];
char msg[160];
snprintf(msg, sizeof msg, "WebAssembly.%s(): WebAssembly.%s must be invoked with 'new'", name, name);
scr_wasm_type_error(msg);
return NULL;
}
static const ScrWasmModuleDesc *scr_wasm_module_arg(const ScrDyn *v, const char *what) {
ScrWasmObj *m = scr_wasm_unbox(v, SCR_WASM_K_MODULE);
if (m) return m->desc;
char msg[160];
snprintf(msg, sizeof msg, "%s: Argument 0 must be a WebAssembly.Module", what);
scr_wasm_type_error(msg);
return NULL;
}
static void scr_wasm_set_text(ScrDyn *obj, const char *key, const char *text, size_t len) {
ScrStr *s = scr_str_new(text, len);
scr_dyn_obj_set(obj, key, strlen(key), scr_dyn_new_str(s));
scr_str_release(s);
}
static const char *const scr_wasm_kind_names[] = {"function", "table", "memory", "global"};
ScrDyn *scr_wasm_module_imports(ScrDyn *module) {
const ScrWasmModuleDesc *desc = scr_wasm_module_arg(module, "WebAssembly.Module.imports()");
if (!desc) return NULL;
ScrDyn *out = scr_dyn_new_arr();
for (uint64_t i = 0; i < desc->import_count; i++) {
const ScrWasmImportDesc *im = &desc->imports[i];
ScrDyn *row = scr_dyn_new_obj();
scr_wasm_set_text(row, "module", im->module, (size_t)im->module_len);
scr_wasm_set_text(row, "name", im->name, (size_t)im->name_len);
scr_wasm_set_text(row, "kind", scr_wasm_kind_names[im->kind], strlen(scr_wasm_kind_names[im->kind]));
scr_dyn_arr_push(out, row);
}
return out;
}
ScrDyn *scr_wasm_module_exports(ScrDyn *module) {
const ScrWasmModuleDesc *desc = scr_wasm_module_arg(module, "WebAssembly.Module.exports()");
if (!desc) return NULL;
ScrDyn *out = scr_dyn_new_arr();
for (uint64_t i = 0; i < desc->export_count; i++) {
const ScrWasmExportDesc *ex = &desc->exports[i];
ScrDyn *row = scr_dyn_new_obj();
scr_wasm_set_text(row, "name", ex->name, (size_t)ex->name_len);
scr_wasm_set_text(row, "kind", scr_wasm_kind_names[ex->kind], strlen(scr_wasm_kind_names[ex->kind]));
scr_dyn_arr_push(out, row);
}
return out;
}
/* A fresh ArrayBuffer per matching custom section, in module order. */
ScrDyn *scr_wasm_module_custom_sections(ScrDyn *module, ScrDyn *name) {
const ScrWasmModuleDesc *desc = scr_wasm_module_arg(module, "WebAssembly.Module.customSections()");
if (!desc) return NULL;
if (scr_wasm_absent(name)) {
scr_wasm_type_error("WebAssembly.Module.customSections(): Argument 1 is required");
return NULL;
}
ScrStr *wanted = scr_dyn_string_coerce_js(name);
if (!wanted) return NULL;
ScrDyn *out = scr_dyn_new_arr();
for (uint64_t i = 0; i < desc->custom_count; i++) {
const ScrWasmCustomDesc *c = &desc->customs[i];
if (c->name_len != wanted->len || memcmp(c->name, wanted->data, wanted->len) != 0) continue;
ScrBytes *bytes = scr_bytes_from_data(c->data, (size_t)c->len);
scr_dyn_arr_push(out, scr_array_buffer_from_bytes(bytes));
scr_bytes_release(bytes);
}
scr_str_release(wanted);
return out;
}
/* `v.valueOf()` on a WebAssembly-typed receiver: a Global's value; any
* other value answers what the generic valueOf answers. */
ScrDyn *scr_wasm_value_of(ScrDyn *value) {
ScrWasmObj *g = scr_wasm_unbox(value, SCR_WASM_K_GLOBAL);
if (g) return scr_wasm_global_value(g->global);
ScrStr *what = scr_str_new("valueOf", 7);
ScrDyn *out = scr_dyn_value_of(value, what);
scr_str_release(what);
return out;
}
/* ── promises ────────────────────────────────────────────────────────── */
/* A promise settled now: fulfilled with value, or rejected with the
@@ -2039,6 +2494,11 @@ static void scr_wasm_teardown(void) {
ScrWasmInst *host = scr_wasm_hosts[i];
const ScrWasmModuleDesc *desc = host->desc;
scr_dyn_release(host->exports);
scr_dyn_release(host->memory_obj);
for (uint64_t t = 0; t < desc->table_count; t++) scr_dyn_release(host->table_objs[t]);
for (uint64_t g = 0; g < desc->global_count; g++) scr_dyn_release(host->global_objs[g]);
free(host->table_objs);
free(host->global_objs);
for (uint64_t f = 0; f < desc->func_count; f++) {
scr_dyn_release(host->export_fns[f]);
scr_dyn_release(host->funcs[f]);
+28 -1
View File
@@ -35,7 +35,7 @@
#include <stddef.h>
#include <stdint.h>
#define SCR_WASM_ABI_VERSION 1
#define SCR_WASM_ABI_VERSION 2
enum { SCR_WASM_FUNC = 0, SCR_WASM_TABLE = 1, SCR_WASM_MEMORY = 2, SCR_WASM_GLOBAL = 3 };
@@ -63,6 +63,9 @@ enum {
#define SCR_WASM_DESC_BOUNDS_CHECKS 1u
typedef void (*ScrWasmThunk)(void *inst, const uint64_t *args, uint64_t *results);
/* A signature's call thunk: calls `code` (a function of that signature,
* `(inst, params...) -> results`) with the arguments in slots. */
typedef void (*ScrWasmCallThunk)(void *code, void *inst, const uint64_t *args, uint64_t *results);
typedef struct {
const char *module;
@@ -113,6 +116,16 @@ typedef struct {
const int64_t *items;
} ScrWasmElemDesc;
/* A custom section: its name and payload, in module order. Only programs
* that call WebAssembly.Module.customSections carry them (the translator
* emits none otherwise, so debug sections cost nothing). */
typedef struct {
const char *name;
uint64_t name_len;
const uint8_t *data;
uint64_t len;
} ScrWasmCustomDesc;
typedef struct ScrWasmModuleDesc {
uint8_t sha256[32];
uint64_t byte_length;
@@ -145,6 +158,12 @@ typedef struct ScrWasmModuleDesc {
* guard; then the start function `void (inst)`, or NULL. */
void (*init)(void *inst);
void (*start)(void *inst);
uint64_t custom_count;
const ScrWasmCustomDesc *customs;
/* Per function: the call thunk of its signature for referenced
* functions the module does not export (NULL otherwise), so any
* function reference can be called from JavaScript. */
const ScrWasmCallThunk *func_call_thunks;
} ScrWasmModuleDesc;
/* The registry the compiler emits in the program module of every program
@@ -165,7 +184,14 @@ typedef struct ScrWasmMemory {
bool guarded;
struct ScrDyn *buffer; /* the cached ArrayBuffer */
uint64_t buffer_size; /* the size it was created at */
uint64_t declared_max; /* pages; UINT64_MAX when the memory has no maximum */
bool buffer_exposed; /* a buffer over the storage was handed out */
/* The byte stores of every ArrayBuffer handed out over this memory's
* storage (retained). The storage outlives the memory while JavaScript
* still holds one of them or a view over it. */
struct ScrBytes **exposed;
size_t exposed_count;
struct ScrWasmMemory *next_parked; /* released, storage still exposed */
} ScrWasmMemory;
typedef struct ScrWasmFuncRef {
@@ -181,6 +207,7 @@ typedef struct ScrWasmTable {
uint32_t max;
size_t rc;
char elem; /* 'r' or 'x' */
uint64_t declared_max; /* UINT64_MAX when the table has no maximum */
} ScrWasmTable;
/* A global's cell: the value in the low bytes of `value`. */
@@ -0,0 +1,21 @@
MIT License
Copyright (c) 2026 Vercel, Inc.
Permission is hereby granted, free of charge, to any person obtaining a copy
of this software and associated documentation files (the "Software"), to deal
in the Software without restriction, including without limitation the rights
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
copies of the Software, and to permit persons to whom the Software is
furnished to do so, subject to the following conditions:
The above copyright notice and this permission notice shall be included in
all copies or substantial portions of the Software.
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
THE SOFTWARE.
@@ -0,0 +1,372 @@
/**
* WASM Dynamic Linking - Extension Loader
*
* Loads WASM shared libraries (.so) compiled with wasi-sdk as extensions
* for the QuickJS WASM runtime. Extensions can call QuickJS C API functions
* directly through dynamic linking (shared memory + symbol resolution).
*
* Key concepts:
* - Extensions are WASM shared libraries with a `dylink.0` custom section
* - They share linear memory and the indirect function table with the main module
* - Symbol imports (env.*) are resolved against the main module's exports
* - Each extension gets a unique __memory_base and __table_base
* - Extensions export an init function (e.g., `qjs_ext_url_init`)
*/
const decoder = new TextDecoder();
/**
* Parse a null-terminated "key=value\nkey=value" string from WASM memory
* into a Record<string, string>. Used to read version info from extensions.
*/
function parseVersionString(memory, ptr) {
const mem = new Uint8Array(memory.buffer);
let end = ptr;
while (mem[end] !== 0)
end++;
if (end === ptr)
return {};
const str = decoder.decode(mem.slice(ptr, end));
const result = {};
for (const line of str.split('\n')) {
const eq = line.indexOf('=');
if (eq > 0) {
result[line.slice(0, eq)] = line.slice(eq + 1);
}
}
return result;
}
/**
* Check if an extension exports a versions function and parse the result.
*/
function extractExtensionVersions(ext, memory) {
const versionsFnName = `qjs_ext_${ext.name.replace(/-/g, '_')}_versions`;
const versionsFn = ext.instance.exports[versionsFnName];
if (typeof versionsFn !== 'function')
return undefined;
const ptr = versionsFn();
if (ptr === 0)
return undefined;
return parseVersionString(memory, ptr);
}
// ---- dylink.0 parser ----
/** Read a ULEB128-encoded integer from a byte array */
function readULEB128(bytes, offset) {
let result = 0;
let shift = 0;
while (offset.value < bytes.length) {
const byte = bytes[offset.value++];
result |= (byte & 0x7f) << shift;
if ((byte & 0x80) === 0)
break;
shift += 7;
}
return result;
}
/** Parse the dylink.0 custom section from a WASM module */
export function parseDylink(module) {
const sections = WebAssembly.Module.customSections(module, 'dylink.0');
if (sections.length === 0)
return null;
const bytes = new Uint8Array(sections[0]);
const offset = { value: 0 };
const info = {
memorySize: 0,
memoryAlignment: 0,
tableSize: 0,
tableAlignment: 0,
needed: [],
};
while (offset.value < bytes.length) {
const subsectionType = readULEB128(bytes, offset);
const subsectionSize = readULEB128(bytes, offset);
const subsectionEnd = offset.value + subsectionSize;
if (subsectionType === 1) {
// WASM_DYLINK_MEM_INFO
info.memorySize = readULEB128(bytes, offset);
info.memoryAlignment = readULEB128(bytes, offset);
info.tableSize = readULEB128(bytes, offset);
info.tableAlignment = readULEB128(bytes, offset);
}
else if (subsectionType === 2) {
// WASM_DYLINK_NEEDED
const count = readULEB128(bytes, offset);
for (let i = 0; i < count; i++) {
const len = readULEB128(bytes, offset);
const name = new TextDecoder().decode(bytes.slice(offset.value, offset.value + len));
offset.value += len;
info.needed.push(name);
}
}
offset.value = subsectionEnd;
}
return info;
}
// ---- Alignment helper ----
/** Align a value up to 2^align */
function alignUp(value, align) {
if (align <= 0)
return value;
const mask = (1 << align) - 1;
return (value + mask) & ~mask;
}
// ---- Extension Loader ----
/**
* Load a WASM shared library extension and link it against the main module.
*
* @param descriptor - Extension description (name, wasm bytes, init function)
* @param mainExports - The main QuickJS WASM module's exports
* @param wasiBuiltins - Built-in WASI implementations (lowest priority)
* @param wasiUserOverrides - User-provided WASI overrides (highest priority)
* @param memoryProxy - A memory proxy for extension WASI factories
* @param allocBase - Optional fixed memory/table bases (for snapshot restore)
*/
export async function loadExtension(descriptor, mainExports, wasiBuiltins, wasiUserOverrides, memoryProxy, allocBase) {
// Compile the extension module
let module;
if (descriptor.wasm instanceof WebAssembly.Module) {
module = descriptor.wasm;
}
else {
module = await WebAssembly.compile(descriptor.wasm);
}
// Parse dylink.0 section
const dylink = parseDylink(module);
if (!dylink) {
throw new Error(`Extension "${descriptor.name}" is not a WASM shared library (missing dylink.0 section)`);
}
const memory = mainExports.memory;
const table = mainExports.__indirect_function_table;
const stackPointer = mainExports.__stack_pointer;
const mallocFn = mainExports.malloc;
if (!memory)
throw new Error('Main module does not export memory');
if (!table)
throw new Error('Main module does not export __indirect_function_table');
if (!mallocFn)
throw new Error('Main module does not export malloc');
// Allocate memory region for the extension's static data
let memoryBase;
let tableBase;
if (allocBase) {
// Restore mode: use the exact same bases as before
memoryBase = allocBase.memoryBase;
tableBase = allocBase.tableBase;
}
else {
// Fresh allocation
if (dylink.memorySize > 0) {
// Use malloc from the main module to allocate in the shared heap
memoryBase = mallocFn(dylink.memorySize);
if (memoryBase === 0) {
throw new Error(`Failed to allocate ${dylink.memorySize} bytes for extension "${descriptor.name}"`);
}
// Zero-initialize the region
new Uint8Array(memory.buffer, memoryBase, dylink.memorySize).fill(0);
}
else {
memoryBase = 0;
}
}
// Grow the function table if needed
if (allocBase) {
tableBase = allocBase.tableBase;
// Ensure table is large enough
if (tableBase + dylink.tableSize > table.length) {
table.grow(tableBase + dylink.tableSize - table.length);
}
}
else {
tableBase = table.length;
if (dylink.tableSize > 0) {
table.grow(dylink.tableSize);
}
}
// Build the import object
const extImports = WebAssembly.Module.imports(module);
const importObj = {
env: {},
'GOT.mem': {},
'GOT.func': {},
};
// Provide wasi_snapshot_preview1 imports to extensions that need them.
// Layered merge: builtins → extension-provided → user overrides.
const needsWasi = extImports.some((imp) => imp.module === 'wasi_snapshot_preview1');
if (needsWasi) {
const extWasi = (descriptor.wasi && memoryProxy) ? descriptor.wasi(memoryProxy) : undefined;
importObj['wasi_snapshot_preview1'] = {
...wasiBuiltins, // 1. Built-in defaults (lowest priority)
...extWasi, // 2. Extension-provided
...wasiUserOverrides // 3. User overrides (highest priority)
};
}
// Collect names of functions the extension exports (for self-resolution)
const extExportNames = new Set(WebAssembly.Module.exports(module)
.filter((e) => e.kind === 'function')
.map((e) => e.name));
const unresolvedFuncs = new Set();
for (const imp of extImports) {
if (imp.module === 'env') {
if (imp.name === 'memory' && imp.kind === 'memory') {
importObj.env.memory = memory;
}
else if (imp.name === '__indirect_function_table' &&
imp.kind === 'table') {
importObj.env.__indirect_function_table = table;
}
else if (imp.name === '__memory_base' && imp.kind === 'global') {
importObj.env.__memory_base = new WebAssembly.Global({ value: 'i32', mutable: false }, memoryBase);
}
else if (imp.name === '__table_base' && imp.kind === 'global') {
importObj.env.__table_base = new WebAssembly.Global({ value: 'i32', mutable: false }, tableBase);
}
else if (imp.name === '__stack_pointer' && imp.kind === 'global') {
importObj.env.__stack_pointer = stackPointer;
}
else if (imp.kind === 'function') {
// Resolve function imports from the main module's exports
const resolved = mainExports[imp.name];
if (resolved && typeof resolved === 'function') {
importObj.env[imp.name] = resolved;
}
else {
// Provide a trap stub for unresolved symbols (e.g., C++ runtime
// functions like wstring methods that are imported but never called).
// If actually called at runtime, this will throw.
unresolvedFuncs.add(imp.name);
importObj.env[imp.name] = () => {
throw new Error(`Extension "${descriptor.name}" called unresolved symbol: env.${imp.name}`);
};
}
}
else if (imp.kind === 'global') {
// Other globals - try to resolve from main exports
const resolved = mainExports[imp.name];
if (resolved instanceof WebAssembly.Global) {
importObj.env[imp.name] = resolved;
}
else {
// Create a zero-initialized mutable global as fallback
importObj.env[imp.name] = new WebAssembly.Global({ value: 'i32', mutable: true }, 0);
}
}
}
else if (imp.module === 'GOT.mem' && imp.kind === 'global') {
// GOT.mem entries are mutable globals containing memory addresses of symbols
// Try to resolve the symbol's address - for now, create mutable globals
// that can be patched by __wasm_apply_data_relocs
importObj['GOT.mem'][imp.name] = new WebAssembly.Global({ value: 'i32', mutable: true }, 0);
}
else if (imp.module === 'GOT.func' && imp.kind === 'global') {
// GOT.func entries are mutable globals containing table indices of functions.
// We resolve these by finding the function in the main module's exports,
// adding it to the indirect function table, and providing the table index.
const resolved = mainExports[imp.name];
if (resolved && typeof resolved === 'function') {
const idx = table.length;
table.grow(1);
table.set(idx, resolved);
importObj['GOT.func'][imp.name] = new WebAssembly.Global({ value: 'i32', mutable: true }, idx);
}
else {
importObj['GOT.func'][imp.name] = new WebAssembly.Global({ value: 'i32', mutable: true }, 0);
}
}
}
// Self-resolve: in WASM shared libraries, the linker with --allow-undefined
// may leave symbols as both imports and exports (e.g., C++ weak symbols from
// libc++ string.cpp.o that satisfy references from ada.o). We use indirection
// through mutable wrapper functions: first create wrappers that initially trap,
// then instantiate, then patch the wrappers to forward to the instance's own exports.
// This way all internal calls go through the wrappers which point to the final exports.
const selfResolvable = [...unresolvedFuncs].filter((name) => extExportNames.has(name));
// For self-resolvable symbols, create mutable wrappers
const wrappers = {};
for (const name of selfResolvable) {
const wrapper = { target: null };
wrappers[name] = wrapper;
// Replace the trap stub with a wrapper that forwards to the target
importObj.env[name] = (...args) => {
if (!wrapper.target) {
throw new Error(`Extension "${descriptor.name}" called unresolved symbol during init: env.${name}`);
}
return wrapper.target(...args);
};
}
const instance = await WebAssembly.instantiate(module, importObj);
const extExports = instance.exports;
// Patch wrappers to point to the instance's own exports
for (const name of selfResolvable) {
const selfExport = extExports[name];
if (typeof selfExport === 'function') {
wrappers[name].target = selfExport;
}
}
// Apply data relocations if the extension has them
if (typeof extExports.__wasm_apply_data_relocs === 'function') {
extExports.__wasm_apply_data_relocs();
}
// Call constructors if present
if (typeof extExports.__wasm_call_ctors === 'function') {
extExports.__wasm_call_ctors();
}
const initFn = descriptor.initFn ?? `qjs_ext_${descriptor.name.replace(/-/g, '_')}_init`;
const ext = {
name: descriptor.name,
module,
instance,
dylink,
memoryBase,
tableBase,
initFn,
};
// Check for version reporting
ext.versions = extractExtensionVersions(ext, memory);
return ext;
}
/**
* Call the extension's init function, passing the JSContext and JSRuntime
* pointers from the main module.
*/
export function initExtension(ext, mainExports) {
const initFunc = ext.instance.exports[ext.initFn];
if (typeof initFunc !== 'function') {
throw new Error(`Extension "${ext.name}" does not export init function "${ext.initFn}"`);
}
// Get the JSContext and JSRuntime pointers from the main module
const ctxPtr = mainExports.qjs_get_context_ptr();
const rtPtr = mainExports.qjs_get_runtime_ptr();
// Call the extension's init function: qjs_ext_xxx_init(ctx, rt)
const result = initFunc(ctxPtr, rtPtr);
if (result !== 0) {
throw new Error(`Extension "${ext.name}" init function returned error code ${result}`);
}
}
/**
* Re-instantiate extensions for snapshot restore.
*
* During restore, we need to:
* 1. Instantiate extension modules with the same memory/table bases
* 2. Let them populate the function table (via elem segments and __wasm_apply_data_relocs)
* 3. Do NOT call the init function (the state is already in the snapshot memory)
*
* This reconstructs the function table entries that extensions need, without
* modifying linear memory (which will be overwritten by the snapshot).
*/
export async function restoreExtensions(descriptors, extensionMeta, mainExports, wasiBuiltins, wasiUserOverrides, memoryProxy) {
const loaded = [];
for (const meta of extensionMeta) {
// Find the descriptor for this extension
const descriptor = descriptors.find((d) => d.name === meta.name);
if (!descriptor) {
throw new Error(`Extension "${meta.name}" required by snapshot but not provided`);
}
// Load with fixed bases
const ext = await loadExtension(descriptor, mainExports, wasiBuiltins, wasiUserOverrides, memoryProxy, {
memoryBase: meta.memoryBase,
tableBase: meta.tableBase,
});
ext.initFn = meta.initFn;
loaded.push(ext);
}
return loaded;
}
//# sourceMappingURL=extensions.js.map
+101
View File
@@ -0,0 +1,101 @@
// quickjs-wasi's WebAssembly extension loader, run through its own code:
// extensions.js is dist/extensions.js of quickjs-wasi 3.6.2, unmodified
// (MIT, LICENSE.quickjs-wasi). It parses the dylink.0 custom section
// (WebAssembly.Module.customSections), reads the extension's imports and
// exports (Module.imports, Module.exports), allocates the extension's data
// in the main module's memory, grows the shared function table and stores
// main-module functions in it (table.grow, table.set), creates
// __memory_base, __table_base and GOT globals (new WebAssembly.Global), and
// instantiates the extension against the main module's memory, table and
// stack pointer. The two modules stand in for quickjs.wasm and an
// extension .so; their text is in tests/wasm-spec/corpus/dylink-*.wat.
import { initExtension, loadExtension, parseDylink } from "./extensions.js";
const mainBytes = Buffer.from(
"AGFzbQEAAAABEANgAX8Bf2AAAX9gAn9/AX8DBwYAAQECAgAEBAFwAAQFAwEAAgYTA38BQYCABAt/AUGACAt/AUEACweOAQkGbWVtb3J5AgAZX19pbmRpcmVjdF9mdW5jdGlvbl90YWJsZQEAD19fc3RhY2tfcG9pbnRlcgMABm1hbGxvYwAAE3Fqc19nZXRfY29udGV4dF9wdHIAARNxanNfZ2V0X3J1bnRpbWVfcHRyAAILanNfcmVnaXN0ZXIAAwdqc19jYWxsAAQGaGVscGVyAAUKXAYXAQF/IwEhASMBIABqQQ9qQXBxJAEgAQsFAEHkAAsFAEHIAQsZAEGABCMCQQRsaiABNgIAIwJBAWokAiMCCxMAIAFBgAQgAEEEbGooAgARAAALCAAgAEHoB2oL",
"base64",
);
const extBytes = Buffer.from(
"AGFzbQEAAAABEwRgAX8Bf2ACf38Bf2AAAGAAAX8C0AEKA2VudgZtZW1vcnkCAAEDZW52GV9faW5kaXJlY3RfZnVuY3Rpb25fdGFibGUBcAAAA2Vudg9fX3N0YWNrX3BvaW50ZXIDfwEDZW52DV9fbWVtb3J5X2Jhc2UDfwADZW52DF9fdGFibGVfYmFzZQN/AAdHT1QubWVtB2NvdW50ZXIDfwEIR09ULmZ1bmMGaGVscGVyA38BA2Vudgtqc19yZWdpc3RlcgABA2Vudg5taXNzaW5nX3N5bWJvbAAAA2VudgtzZWxmX3N5bWJvbAAAAwwLAAACAgEDAAAAAwMHogEJGF9fd2FzbV9hcHBseV9kYXRhX3JlbG9jcwAFEV9fd2FzbV9jYWxsX2N0b3JzAAYRcWpzX2V4dF9kZW1vX2luaXQABxVxanNfZXh0X2RlbW9fdmVyc2lvbnMACAtzZWxmX3N5bWJvbAAJCHVzZV9zZWxmAAoLdXNlX21pc3NpbmcACw1zdGFja19wb2ludGVyAAwMYnVtcF9jb3VudGVyAA0JCAEAIwILAgMECnYLBwAgAEEDbAsJACAAIwQRAAALCQAjAUEgaiQDCwkAIwNBBTYCAAsZACAAIwIQABogACMCQQFqEAAaIAFByAFrCwQAIwELBwAgAEEBagsGACAAEAILBgAgABABCwQAIwALFAAjAyMDKAIAQQFqNgIAIwMoAgALCxwBACMBCxZuYW1lPWRlbW8KdmVyc2lvbj0xLjIAABoIZHlsaW5rLjABBDACAgACCQEHbGliYy5zbw==",
"base64",
);
// A module without a dylink.0 section (tests/wasm-spec/corpus/jsapi-own.wat).
const plainBytes = Buffer.from(
"AGFzbQEAAAABBgFgAX8BfwMCAQAEBQFwAQIIBQQBAQEEBhICfwFBBwt8AEQAAAAAAAD4PwsHKgcDbWVtAgAEbWVtMgIAA3RhYgEAAWcDAAJnMgMABWZpeGVkAwECaWQAAAkHAQBBAQsBAAoGAQQAIAAL",
"base64",
);
const failure = (e) => (e instanceof Error ? `${e.name}: ${e.message}` : `threw ${String(e)}`);
const { instance } = await WebAssembly.instantiate(mainBytes, {});
const mainExports = instance.exports;
const table = mainExports.__indirect_function_table;
console.log("dylink", JSON.stringify(parseDylink(new WebAssembly.Module(extBytes))));
console.log("plain dylink", parseDylink(new WebAssembly.Module(plainBytes)));
const ext = await loadExtension({ name: "demo", wasm: extBytes }, mainExports, {}, {}, null);
console.log("loaded", ext.name, ext.memoryBase, ext.tableBase, ext.initFn);
console.log("versions", JSON.stringify(ext.versions));
console.log("dylink", JSON.stringify(ext.dylink));
console.log("table length", table.length, table.get(6) === mainExports.helper);
// The extension's own functions in the shared table, called from here.
const triple = table.get(4);
console.log("table functions", triple(14), table.get(5) === table.get(5), triple.name, triple.length);
console.log("module", ext.module instanceof WebAssembly.Module, ext.instance instanceof WebAssembly.Instance);
initExtension(ext, mainExports);
console.log("js_call", mainExports.js_call(0, 7), mainExports.js_call(1, 5));
const x = ext.instance.exports;
console.log("self symbol", x.use_self(41));
try {
x.use_missing(1);
} catch (e) {
console.log("missing symbol", failure(e));
}
console.log("stack pointer", x.stack_pointer(), mainExports.__stack_pointer.value);
mainExports.__stack_pointer.value = 60000;
console.log("shared stack pointer", x.stack_pointer());
console.log("counter", x.bump_counter(), x.bump_counter());
console.log("memory", new Uint32Array(mainExports.memory.buffer, ext.memoryBase + 32, 1)[0]);
// Snapshot restore (what restoreExtensions does per extension): the same
// bases, from a compiled Module, without init.
const restored = await loadExtension(
{ name: "demo", wasm: new WebAssembly.Module(extBytes) },
mainExports,
{},
{},
null,
{ memoryBase: ext.memoryBase, tableBase: ext.tableBase },
);
console.log("restored", restored.memoryBase, restored.tableBase, table.length);
console.log("restored counter", restored.instance.exports.bump_counter());
// A second extension instance gets its own bases.
const second = await loadExtension({ name: "demo", wasm: extBytes }, mainExports, {}, {}, null);
console.log("second", second.memoryBase, second.tableBase, table.length);
initExtension(second, mainExports);
console.log("js_call", mainExports.js_call(2, 10), mainExports.js_call(3, 10));
async function tryLoad(label, wasm, exports) {
try {
await loadExtension({ name: label, wasm }, exports, {}, {}, null);
console.log(label, "loaded");
} catch (e) {
console.log(label, failure(e));
}
}
await tryLoad("plain", plainBytes, mainExports);
await tryLoad("no memory", extBytes, { ...mainExports, memory: undefined });
await tryLoad("no table", extBytes, { ...mainExports, __indirect_function_table: undefined });
try {
initExtension({ ...ext, initFn: "qjs_ext_none_init" }, mainExports);
} catch (e) {
console.log("init", failure(e));
}
try {
initExtension(ext, { ...mainExports, qjs_get_runtime_ptr: () => 201 });
} catch (e) {
console.log("init", failure(e));
}
+356
View File
@@ -0,0 +1,356 @@
// The WebAssembly JavaScript API's constructors and reflection: new
// WebAssembly.Memory, Table and Global (descriptors, defaults, conversions
// and every error), using them as imports (limits, types and mutability
// checked with Node's LinkError messages), the identity of exported
// objects, WebAssembly.Module.imports, exports and customSections, and the
// classes called without new. The modules are inline base64 literals
// (their text is in tests/wasm-spec/corpus/jsapi.wat and jsapi-own.wat).
const jsapi = Buffer.from("AGFzbQEAAAABKglgAX8Bf2ABfwBgAAF/YAF8AXxgAX4AYAABb2ABbwBgAn9/AX9gAn9/AAJbBwNlbnYDbG9nAAEDZW52Bm1lbW9yeQIBAQQDZW52BXRhYmxlAXABAggDZW52B2NvdW50ZXIDfwEDZW52BXNjYWxlA3wAA2VudgNiaWcDfgEDZW52A3JlZgNvAQMPDgAAAgMEBQYHCAACAAIBB5UBEQRidW1wAAMGc2NhbGVkAAQGYmlnQWRkAAUGZ2V0UmVmAAYGc2V0UmVmAAcFYXBwbHkACAVzdG9yZQAJBGxvYWQACgRzaXplAAsEZ3JvdwAMCXRhYmxlU2l6ZQANA3NheQAOBm1lbW9yeQIABXRhYmxlAQAHY291bnRlcgMAC2Fsc29Db3VudGVyAwAGZG91YmxlAAEJCAEAQQALAgECCnAOBwAgAEECbAsHACAAIABsCwsAIwBBAWokACMACwcAIAAjAaILCQAjAiAAfCQCCwQAIwMLBgAgACQDCwkAIAEgABEAAAsJACAAIAE6AAALBwAgAC0AAAsEAD8ACwYAIABAAAsFAPwQAAsGACAAEAALAAwIZHlsaW5rLjABAgMACgRtZXRhaGVsbG8ACwhkeWxpbmsuMAQFAAEA", "base64");
const own = Buffer.from("AGFzbQEAAAABBgFgAX8BfwMCAQAEBQFwAQIIBQQBAQEEBhICfwFBBwt8AEQAAAAAAAD4PwsHKgcDbWVtAgAEbWVtMgIAA3RhYgEAAWcDAAJnMgMABWZpeGVkAwECaWQAAAkHAQBBAQsBAAoGAQQAIAAL", "base64");
const show = (v) => {
if (typeof v === "bigint") return `${v}n`;
if (typeof v === "number") return Object.is(v, -0) ? "-0" : String(v);
if (typeof v === "function") return "function";
if (Array.isArray(v)) return `[${v.map(show).join(", ")}]`;
if (v instanceof WebAssembly.Memory) return "Memory";
if (v instanceof WebAssembly.Table) return "Table";
if (v instanceof WebAssembly.Global) return "Global";
if (v instanceof ArrayBuffer) return `ArrayBuffer(${v.byteLength})`;
if (v !== null && typeof v === "object") return JSON.stringify(v);
return String(v);
};
const attempt = (label, f) => {
try {
console.log(label, show(f()));
} catch (e) {
console.log(label, e instanceof Error ? `${e.name}: ${e.message}` : `threw ${show(e)}`);
}
};
// ── WebAssembly.Global ────────────────────────────────────────────────────
console.log("Global");
attempt("i32 default", () => new WebAssembly.Global({ value: "i32" }).value);
attempt("i64 default", () => new WebAssembly.Global({ value: "i64" }).value);
attempt("f32", () => new WebAssembly.Global({ value: "f32" }, 1.1).value);
attempt("f64", () => new WebAssembly.Global({ value: "f64" }, 1.1).value);
attempt("f64 undefined", () => new WebAssembly.Global({ value: "f64" }, undefined).value);
attempt("f64 null", () => new WebAssembly.Global({ value: "f64" }, null).value);
attempt("i32 string", () => new WebAssembly.Global({ value: "i32" }, "7").value);
attempt("i32 wraps", () => new WebAssembly.Global({ value: "i32" }, 2 ** 32 + 3).value);
attempt("i32 object", () => new WebAssembly.Global({ value: "i32" }, { valueOf: () => 9 }).value);
attempt("i64 bigint", () => new WebAssembly.Global({ value: "i64" }, 2n ** 63n).value);
attempt("i64 string", () => new WebAssembly.Global({ value: "i64" }, "12").value);
attempt("i64 number", () => new WebAssembly.Global({ value: "i64" }, 5).value);
attempt("i64 null", () => new WebAssembly.Global({ value: "i64" }, null).value);
attempt("i32 bigint", () => new WebAssembly.Global({ value: "i32" }, 5n).value);
attempt("externref default", () => new WebAssembly.Global({ value: "externref" }).value);
attempt("externref object", () => new WebAssembly.Global({ value: "externref" }, { a: 1 }).value);
attempt("anyfunc default", () => new WebAssembly.Global({ value: "anyfunc" }).value);
attempt("anyfunc arrow", () => new WebAssembly.Global({ value: "anyfunc" }, () => 1));
attempt("anyfunc number", () => new WebAssembly.Global({ value: "anyfunc" }, 1));
attempt("funcref name", () => new WebAssembly.Global({ value: "funcref" }));
attempt("v128", () => new WebAssembly.Global({ value: "v128" }));
attempt("bad type", () => new WebAssembly.Global({ value: "i16" }));
attempt("no value", () => new WebAssembly.Global({}));
attempt("no descriptor", () => new WebAssembly.Global());
attempt("number descriptor", () => new WebAssembly.Global(1));
attempt("toString type", () => new WebAssembly.Global({ value: { toString: () => "f64" } }, 2.5).value);
const order = [];
new WebAssembly.Global(
{
get value() {
order.push("value");
return { toString: () => (order.push("value toString"), "f64") };
},
get mutable() {
order.push("mutable");
return false;
},
},
{ valueOf: () => (order.push("valueOf"), 1) },
);
console.log("order", order.join(","));
const immutable = new WebAssembly.Global({ value: "i32" }, 4);
attempt("immutable set", () => {
immutable.value = 5;
});
console.log("immutable after", immutable.value, immutable.valueOf());
const mutable = new WebAssembly.Global({ value: "i32", mutable: 1 }, 4);
mutable.value = 2.9;
console.log("mutable", mutable.value, mutable.valueOf());
mutable.value = "x";
console.log("mutable NaN", mutable.value);
const mutable64 = new WebAssembly.Global({ value: "i64", mutable: true });
mutable64.value = true;
console.log("mutable64", show(mutable64.value));
attempt("i64 set number", () => {
mutable64.value = 3;
});
const mutableF32 = new WebAssembly.Global({ value: "f32", mutable: true }, 1);
mutableF32.value = undefined;
console.log("f32 set undefined", mutableF32.value);
const mutableRef = new WebAssembly.Global({ value: "externref", mutable: true }, "start");
mutableRef.value = undefined;
console.log("externref set undefined", mutableRef.value);
const mutableFunc = new WebAssembly.Global({ value: "anyfunc", mutable: true });
attempt("anyfunc set arrow", () => {
mutableFunc.value = () => 2;
});
console.log(String(immutable), Object.prototype.toString.call(immutable), immutable instanceof WebAssembly.Global);
console.log(immutable);
// @ts-ignore: called without new, as Node reports
attempt("Global()", () => WebAssembly.Global({ value: "i32" }));
// ── WebAssembly.Memory ────────────────────────────────────────────────────
console.log("Memory");
attempt("bytes", () => new WebAssembly.Memory({ initial: 1 }).buffer.byteLength);
attempt("zero", () => new WebAssembly.Memory({ initial: 0 }).buffer.byteLength);
attempt("string initial", () => new WebAssembly.Memory({ initial: "2" }).buffer.byteLength);
attempt("fractional", () => new WebAssembly.Memory({ initial: 1.9 }).buffer.byteLength);
attempt("null initial", () => new WebAssembly.Memory({ initial: null }).buffer.byteLength);
attempt("missing", () => new WebAssembly.Memory({}));
attempt("minimum only", () => new WebAssembly.Memory({ minimum: 1 }));
attempt("undefined initial", () => new WebAssembly.Memory({ initial: undefined }));
attempt("negative", () => new WebAssembly.Memory({ initial: -1 }));
attempt("negative fraction", () => new WebAssembly.Memory({ initial: -0.5 }));
attempt("NaN", () => new WebAssembly.Memory({ initial: NaN }));
attempt("Infinity", () => new WebAssembly.Memory({ initial: Infinity }));
attempt("2^32", () => new WebAssembly.Memory({ initial: 2 ** 32 }));
attempt("too many pages", () => new WebAssembly.Memory({ initial: 65537 }));
attempt("max below initial", () => new WebAssembly.Memory({ initial: 20, maximum: 1 }));
attempt("max too large", () => new WebAssembly.Memory({ initial: 1, maximum: 65537 }));
attempt("max 2^32", () => new WebAssembly.Memory({ initial: 1, maximum: 2 ** 32 }));
attempt("max null", () => new WebAssembly.Memory({ initial: 1, maximum: null }));
attempt("bigint initial", () => new WebAssembly.Memory({ initial: 1n }));
attempt("throwing initial", () =>
new WebAssembly.Memory({
initial: {
valueOf() {
throw new Error("boom");
},
},
}),
);
attempt("shared without maximum", () => new WebAssembly.Memory({ initial: 1, shared: true }));
attempt("no descriptor", () => new WebAssembly.Memory());
attempt("string descriptor", () => new WebAssembly.Memory("x"));
const memoryOrder = [];
new WebAssembly.Memory({
get initial() {
memoryOrder.push("initial");
return { valueOf: () => (memoryOrder.push("initial valueOf"), 1) };
},
get maximum() {
memoryOrder.push("maximum");
return { valueOf: () => (memoryOrder.push("maximum valueOf"), 3) };
},
get shared() {
memoryOrder.push("shared");
return false;
},
});
console.log("order", memoryOrder.join(","));
const grown = new WebAssembly.Memory({ initial: 1, maximum: 2 });
const before = grown.buffer;
new Uint8Array(before)[5] = 7;
console.log("same buffer", grown.buffer === grown.buffer);
attempt("grow", () => grown.grow(1));
console.log("after grow", grown.buffer.byteLength, new Uint8Array(grown.buffer)[5], before === grown.buffer);
attempt("grow past maximum", () => grown.grow(1));
attempt("grow negative", () => grown.grow(-1));
attempt("grow missing", () => grown.grow());
attempt("grow NaN", () => grown.grow(NaN));
attempt("grow 2^32", () => grown.grow(2 ** 32));
attempt("grow zero", () => grown.grow(0));
attempt("unbounded grow", () => new WebAssembly.Memory({ initial: 1, maximum: undefined }).grow(3));
console.log(String(grown), grown instanceof WebAssembly.Memory);
console.log(grown);
// @ts-ignore: called without new, as Node reports
attempt("Memory()", () => WebAssembly.Memory({ initial: 1 }));
// ── WebAssembly.Table ─────────────────────────────────────────────────────
console.log("Table");
attempt("anyfunc", () => new WebAssembly.Table({ initial: 2, element: "anyfunc" }).get(1));
attempt("externref", () => new WebAssembly.Table({ initial: 1, element: "externref" }).get(0));
attempt("externref init", () => new WebAssembly.Table({ initial: 2, element: "externref" }, "hi").get(1));
attempt("externref undefined", () => new WebAssembly.Table({ initial: 2, element: "externref" }, undefined).get(1));
attempt("anyfunc null", () => new WebAssembly.Table({ initial: 2, element: "anyfunc" }, null).get(1));
attempt("anyfunc arrow", () => new WebAssembly.Table({ initial: 2, element: "anyfunc" }, () => 1));
attempt("anyfunc number", () => new WebAssembly.Table({ initial: 2, element: "anyfunc" }, 5));
attempt("funcref name", () => new WebAssembly.Table({ initial: 1, element: "funcref" }));
attempt("i32 element", () => new WebAssembly.Table({ initial: 1, element: "i32" }));
attempt("no element", () => new WebAssembly.Table({ initial: 1 }));
attempt("no initial", () => new WebAssembly.Table({ element: "anyfunc" }));
// @ts-ignore: an incomplete descriptor
attempt("empty", () => new WebAssembly.Table({}));
attempt("no descriptor", () => new WebAssembly.Table());
attempt("max below initial", () => new WebAssembly.Table({ initial: 2, maximum: 1, element: "anyfunc" }));
attempt("bad init and descriptor", () => new WebAssembly.Table({ initial: 3, maximum: 2, element: "anyfunc" }, 37));
attempt("too large", () => new WebAssembly.Table({ initial: 10000001, element: "anyfunc" }));
attempt("large maximum", () => new WebAssembly.Table({ initial: 1, maximum: 10000001, element: "anyfunc" }).length);
attempt("max 2^32", () => new WebAssembly.Table({ initial: 1, maximum: 2 ** 32, element: "anyfunc" }));
attempt("toString element", () => new WebAssembly.Table({ element: { toString: () => "anyfunc" }, initial: 1 }).length);
const tableOrder = [];
new WebAssembly.Table({
get element() {
tableOrder.push("element");
return { toString: () => (tableOrder.push("element toString"), "anyfunc") };
},
get initial() {
tableOrder.push("initial");
return { valueOf: () => (tableOrder.push("initial valueOf"), 1) };
},
get maximum() {
tableOrder.push("maximum");
return { valueOf: () => (tableOrder.push("maximum valueOf"), 3) };
},
});
console.log("order", tableOrder.join(","));
const refs = new WebAssembly.Table({ initial: 1, maximum: 4, element: "externref" }, 3);
attempt("set default", () => {
refs.set(0);
return refs.get(0);
});
attempt("grow default", () => [refs.grow(1), refs.get(1), refs.length]);
attempt("grow value", () => [refs.grow(1, "z"), refs.get(2), refs.length]);
attempt("grow past maximum", () => refs.grow(2));
attempt("get negative", () => refs.get(-1));
attempt("get missing", () => refs.get());
attempt("get out of range", () => refs.get(5));
attempt("set out of range", () => refs.set(9, 1));
const funcs = new WebAssembly.Table({ initial: 1, element: "anyfunc" });
attempt("set arrow", () => funcs.set(0, () => 1));
attempt("set number", () => funcs.set(0, 7));
attempt("grow arrow", () => funcs.grow(1, () => 1));
attempt("grow negative", () => funcs.grow(-1));
console.log(String(funcs), funcs instanceof WebAssembly.Table);
console.log(funcs);
// @ts-ignore: called without new, as Node reports
attempt("Table()", () => WebAssembly.Table({ initial: 1, element: "anyfunc" }));
// ── WebAssembly.Module's static functions ────────────────────────────────
console.log("Module");
const module = new WebAssembly.Module(jsapi);
console.log(JSON.stringify(WebAssembly.Module.imports(module)));
console.log(JSON.stringify(WebAssembly.Module.exports(module)));
const firstImport = WebAssembly.Module.imports(module)[0];
console.log(Object.keys(firstImport).join(","), Array.isArray(WebAssembly.Module.imports(module)));
firstImport.module = "changed";
console.log("fresh", WebAssembly.Module.imports(module)[0].module);
const dylink = WebAssembly.Module.customSections(module, "dylink.0");
console.log(
"dylink.0",
dylink.length,
dylink.map((b) => `${show(b)} ${[...new Uint8Array(b)].join(" ")}`).join(" | "),
);
console.log("meta", new TextDecoder().decode(WebAssembly.Module.customSections(module, "meta")[0]));
console.log("empty name", WebAssembly.Module.customSections(module, "").length);
console.log("missing", WebAssembly.Module.customSections(module, "nope").length);
console.log("toString", WebAssembly.Module.customSections(module, { toString: () => "meta" }).length);
console.log("null", WebAssembly.Module.customSections(module, null).length);
const copy = WebAssembly.Module.customSections(module, "meta")[0];
new Uint8Array(copy)[0] = 0;
console.log(
"copies",
new Uint8Array(WebAssembly.Module.customSections(module, "meta")[0])[0],
copy === WebAssembly.Module.customSections(module, "meta")[0],
);
attempt("customSections missing name", () => WebAssembly.Module.customSections(module));
attempt("customSections symbol", () => WebAssembly.Module.customSections(module, Symbol("x")));
attempt("customSections number", () => WebAssembly.Module.customSections(1, "x"));
attempt("imports object", () => WebAssembly.Module.imports({}));
attempt("exports missing", () => WebAssembly.Module.exports());
attempt("imports stray", () => WebAssembly.Module.imports(module, 1).length);
console.log("own", JSON.stringify(WebAssembly.Module.imports(new WebAssembly.Module(own))));
console.log("own", JSON.stringify(WebAssembly.Module.exports(new WebAssembly.Module(own))));
// @ts-ignore: called without new, as Node reports
attempt("Module()", () => WebAssembly.Module(jsapi));
// @ts-ignore: called without new, as Node reports
attempt("Instance()", () => WebAssembly.Instance(module, {}));
// ── constructed objects as imports ────────────────────────────────────────
console.log("imports");
const logged = [];
const memory = new WebAssembly.Memory({ initial: 1, maximum: 4 });
const table = new WebAssembly.Table({ initial: 2, maximum: 8, element: "anyfunc" });
const counter = new WebAssembly.Global({ value: "i32", mutable: true }, 10);
const big = new WebAssembly.Global({ value: "i64", mutable: true }, 1n);
const ref = new WebAssembly.Global({ value: "externref", mutable: true }, "first");
const env = { log: (v) => logged.push(v), memory, table, counter, scale: 2.5, big, ref };
const instance = new WebAssembly.Instance(module, { env });
const x = instance.exports;
console.log(
"identity",
x.memory === memory,
x.table === table,
x.counter === counter,
x.alsoCounter === counter,
);
attempt("bump", () => [x.bump(), x.bump(), counter.value]);
counter.value = 100;
attempt("bump after set", () => x.bump());
attempt("scaled", () => x.scaled(4));
x.bigAdd(41n);
console.log("big", show(big.value));
attempt("getRef", () => x.getRef());
x.setRef({ hello: "world" });
console.log("ref", JSON.stringify(ref.value));
ref.value = 42;
attempt("getRef after set", () => x.getRef());
attempt("apply double", () => x.apply(0, 21));
attempt("apply square", () => x.apply(1, 12));
console.log("table get", table.get(0) === x.double);
// A function the module does not export, reached through the table.
const square = table.get(1);
console.log("unexported", square(5), square === table.get(1), square.name, square.length, typeof square);
x.store(3, 99);
console.log("memory", new Uint8Array(memory.buffer)[3], x.load(3));
new Uint8Array(memory.buffer)[4] = 55;
console.log("memory from js", x.load(4));
attempt("grow in wasm", () => [x.grow(1), x.size(), memory.buffer.byteLength]);
attempt("grow from js", () => [memory.grow(1), x.size(), memory.buffer.byteLength]);
attempt("table grow", () => [table.grow(2), x.tableSize(), table.length]);
const global = new WebAssembly.Global({ value: "anyfunc", mutable: true }, x.double);
console.log("funcref global", global.value === x.double);
global.value = null;
console.log("funcref null", global.value);
table.set(1, x.double);
attempt("apply after set", () => x.apply(1, 8));
x.say(5);
console.log("logged", logged.join(","));
// Another instance's exports as imports, and the same objects again.
const provider = new WebAssembly.Instance(new WebAssembly.Module(own)).exports;
console.log("own identity", provider.mem === provider.mem2, provider.g === provider.g2, provider.mem instanceof WebAssembly.Memory);
const second = new WebAssembly.Instance(module, {
env: { ...env, memory: provider.mem, table: provider.tab, counter: provider.g },
}).exports;
console.log("re-exported", second.memory === provider.mem, second.table === provider.tab, second.counter === provider.g);
attempt("bump provider global", () => [second.bump(), provider.g.value]);
attempt("apply provider table", () => second.apply(1, 3));
// ── link errors ───────────────────────────────────────────────────────────
console.log("link errors");
const link = (label, overrides) =>
attempt(label, () => new WebAssembly.Instance(module, { env: { ...env, ...overrides } }) && "linked");
link("memory too small", { memory: new WebAssembly.Memory({ initial: 0, maximum: 4 }) });
link("memory without maximum", { memory: new WebAssembly.Memory({ initial: 1 }) });
link("memory maximum too large", { memory: new WebAssembly.Memory({ initial: 1, maximum: 5 }) });
link("memory larger initial", { memory: new WebAssembly.Memory({ initial: 3, maximum: 4 }) });
link("memory number", { memory: 5 });
link("table too small", { table: new WebAssembly.Table({ initial: 1, maximum: 8, element: "anyfunc" }) });
link("table without maximum", { table: new WebAssembly.Table({ initial: 2, element: "anyfunc" }) });
link("table maximum too large", { table: new WebAssembly.Table({ initial: 2, maximum: 12, element: "anyfunc" }) });
link("table externref", { table: new WebAssembly.Table({ initial: 2, maximum: 8, element: "externref" }) });
link("table small externref", { table: new WebAssembly.Table({ initial: 1, maximum: 8, element: "externref" }) });
link("table number", { table: 3 });
link("global immutable", { counter: new WebAssembly.Global({ value: "i32" }, 1) });
link("global type", { counter: new WebAssembly.Global({ value: "i64", mutable: true }, 1n) });
link("global both", { counter: new WebAssembly.Global({ value: "f64" }, 1) });
link("global number", { counter: 1 });
link("immutable global object", { scale: new WebAssembly.Global({ value: "f64" }, 4) });
link("immutable global mutable", { scale: new WebAssembly.Global({ value: "f64", mutable: true }, 4) });
link("immutable global bigint", { scale: 4n });
link("i64 number", { big: 4 });
link("externref anything", { ref: new WebAssembly.Global({ value: "externref", mutable: true }, null) });
+43
View File
@@ -0,0 +1,43 @@
// Lifetimes across the WebAssembly boundary: an externref read from a table
// or global outlives the slot being cleared, a fresh import result reaches
// JavaScript from an export, a load with an explicit memory index reads its
// own offset, and a Memory's buffer outlives the Memory. The module's text
// is in tests/wasm-spec/corpus/lifetimes.wat.
const lifetimes = Buffer.from(
"AGFzbQEAAAABDQNgAW8AYAABb2AAAX8CDAEDZW52BG1ha2UAAQMHBgABAAEBAgQEAW8AAQUDAQABBgYBbwHQbwsHLAYDcHV0AAEEdGFrZQACBHB1dGcAAwV0YWtlZwAEBWZyZXNoAAUEbG9hZAAGCjsGCABBACAAJgALEgEBb0EAJQAhAEEA0G8mACAACwYAIAAkAAsIACMA0G8kAAsEABAACwgAQQAoQgAICwsKAQBBCAsEYWJjZA==",
"base64",
);
const { instance } = await WebAssembly.instantiate(lifetimes, {
env: { make: () => ({ made: [1, 2, 3] }) },
});
const x = instance.exports;
x.put({ from: "table" });
console.log(x.take());
x.putg({ from: "global" });
console.log(x.takeg());
console.log(x.fresh());
console.log(x.fresh().made.length);
console.log(x.load().toString(16));
function viewOfDroppedMemory() {
const memory = new WebAssembly.Memory({ initial: 1 });
const bytes = new Uint8Array(memory.buffer);
bytes[0] = 7;
bytes[65535] = 9;
return bytes;
}
const view = viewOfDroppedMemory();
const filler = [];
for (let i = 0; i < 32; i++) filler.push(new Uint8Array(65536).fill(1));
view[1] = 5;
console.log(view.length, view[0], view[1], view[65535], filler.length);
function bufferOfDroppedMemory() {
const memory = new WebAssembly.Memory({ initial: 1, maximum: 4 });
new Uint8Array(memory.buffer)[3] = 42;
return memory.buffer;
}
const buffer = bufferOfDroppedMemory();
console.log(buffer.byteLength, new Uint8Array(buffer)[3]);
File diff suppressed because one or more lines are too long
@@ -0,0 +1,5 @@
// Executes WebAssembly spec tests (3 files); see tests/wasm-spec/runner.mjs.
import { run } from "../../wasm-spec/runner.mjs";
import { modules, script } from "./data.mjs";
run(modules, script);
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large Load Diff
Binary file not shown.
+9
View File
@@ -8,6 +8,15 @@ export function hasModule() {
return existsSync(new URL("./add.wasm", import.meta.url).pathname);
}
// WebAssembly shared libraries (quickjs-wasi's extensions) use .so: ext.so
// is a module, native.so is not and is not reported.
export function sharedLibraries() {
return [
existsSync(new URL("./ext.so", import.meta.url).pathname),
existsSync(new URL("./native.so", import.meta.url).pathname),
];
}
export function inlineModule() {
return ANSWER;
}
+7 -6
View File
@@ -1,9 +1,10 @@
// The WebAssembly modules this program can load are visible to the build in
// four ways (wasm/discover.ts): a path the compiler resolves (glue.js), an
// inline base64 literal (glue.js), a .wasm file shipped by a package whose
// code mentions WebAssembly (wasm-fixture-pkg), and the scriptc.wasm list in
// package.json (build/generated.wasm).
import { hasModule, inlineModule } from "./glue.js";
// four ways (wasm/discover.ts): a path the compiler resolves (glue.js,
// including a .so shared library), an inline base64 literal (glue.js), a
// .wasm file shipped by a package whose code mentions WebAssembly
// (wasm-fixture-pkg), and the scriptc.wasm list in package.json
// (build/generated.wasm).
import { hasModule, inlineModule, sharedLibraries } from "./glue.js";
import { describeModule } from "wasm-fixture-pkg";
console.log(hasModule(), inlineModule().length, describeModule());
console.log(hasModule(), inlineModule().length, describeModule(), sharedLibraries());
Binary file not shown.
+6 -7
View File
@@ -1,18 +1,17 @@
// The parts of the WebAssembly API scriptc does not implement yet refuse at
// each reached use, in each spelling. compile, instantiate, validate,
// Module, Instance and the error classes compile (tests/corpus/webassembly-*).
// each reached use, in each spelling. compile, instantiate, validate, the
// Module, Instance, Memory, Table and Global constructors, Module.imports,
// exports and customSections, and the error classes compile
// (tests/corpus/webassembly-*).
async function load(bytes: Uint8Array): Promise<void> {
const module = await WebAssembly.compile(bytes);
const memory = new WebAssembly.Memory({ initial: 1 });
const table = new WebAssembly.Table({ initial: 1, element: "anyfunc" });
const global = new WebAssembly.Global({ value: "i32", mutable: true }, 1);
console.log(memory.buffer.byteLength, table.length, global.value);
console.log(WebAssembly.Module.imports(module).length);
console.log(WebAssembly.Module.customSections(module, "name").length);
const streamed = await WebAssembly.instantiateStreaming(fetch("https://example.com/x.wasm"));
console.log(streamed.instance);
const Ctor = WebAssembly.Module;
console.log(Ctor);
const Memory = WebAssembly.Memory;
console.log(new Memory({ initial: 1 }));
}
await load(new Uint8Array([0, 97, 115, 109, 1, 0, 0, 0]));
@@ -1,8 +1,8 @@
scriptc coverage tests/coverage-fixtures/wasm/main.ts
statements analyzed 8
compile statically 8 (100%)
in dynamic tier 2 (operate on values typed only at runtime; --explain-dynamic lists them)
statements analyzed 9
compile statically 9 (100%)
in dynamic tier 4 (operate on values typed only at runtime; --explain-dynamic lists them)
npm package compilation:
wasm-fixture-pkg static
@@ -14,6 +14,9 @@ scriptc coverage tests/coverage-fixtures/wasm/main.ts
tests/coverage-fixtures/wasm/add.wasm 41 bytes valid
1 function, 0 imports, 1 export
found: path at tests/coverage-fixtures/wasm/glue.js:8
tests/coverage-fixtures/wasm/ext.so 36 bytes valid
1 function, 0 imports, 1 export
found: path at tests/coverage-fixtures/wasm/glue.js:15
tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/lib/core.wasm 27 bytes invalid: type mismatch (at byte 27)
1 function, 0 imports, 0 exports
found: shipped by the package at tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg
@@ -1,71 +1,26 @@
webassembly-fences.ts:6:22 - error SC2020: 'new WebAssembly.Memory' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, Module, Instance and the error classes over modules translated at build time, but not this part of the API
webassembly-fences.ts:9:26 - error SC2020: 'WebAssembly.instantiateStreaming' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, and the error classes over modules translated at build time, but not this part of the API
5 | const module = await WebAssembly.compile(bytes);
6 | const memory = new WebAssembly.Memory({ initial: 1 });
| ^~~~~~~~~~~~~~~~~~
7 | const table = new WebAssembly.Table({ initial: 1, element: "anyfunc" });
hint: the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)
webassembly-fences.ts:7:21 - error SC2020: 'new WebAssembly.Table' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, Module, Instance and the error classes over modules translated at build time, but not this part of the API
6 | const memory = new WebAssembly.Memory({ initial: 1 });
7 | const table = new WebAssembly.Table({ initial: 1, element: "anyfunc" });
| ^~~~~~~~~~~~~~~~~
8 | const global = new WebAssembly.Global({ value: "i32", mutable: true }, 1);
hint: the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)
webassembly-fences.ts:8:22 - error SC2020: 'new WebAssembly.Global' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, Module, Instance and the error classes over modules translated at build time, but not this part of the API
7 | const table = new WebAssembly.Table({ initial: 1, element: "anyfunc" });
8 | const global = new WebAssembly.Global({ value: "i32", mutable: true }, 1);
| ^~~~~~~~~~~~~~~~~~
9 | console.log(memory.buffer.byteLength, table.length, global.value);
hint: the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)
webassembly-fences.ts:9:15 - error SC2004: uses of 'memory' inherit the blocker on its declaration
8 | const global = new WebAssembly.Global({ value: "i32", mutable: true }, 1);
9 | console.log(memory.buffer.byteLength, table.length, global.value);
| ^~~~~~~~~~~~~~~~~~~~~~~~
10 | console.log(WebAssembly.Module.imports(module).length);
hint: the declaration of 'memory' did not compile — fix the diagnostic reported there and these sites clear with it
webassembly-fences.ts:10:15 - error SC2020: 'WebAssembly.Module.imports' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, Module, Instance and the error classes over modules translated at build time, but not this part of the API
9 | console.log(memory.buffer.byteLength, table.length, global.value);
10 | console.log(WebAssembly.Module.imports(module).length);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~
11 | console.log(WebAssembly.Module.customSections(module, "name").length);
hint: the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)
webassembly-fences.ts:11:15 - error SC2020: 'WebAssembly.Module.customSections' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, Module, Instance and the error classes over modules translated at build time, but not this part of the API
10 | console.log(WebAssembly.Module.imports(module).length);
11 | console.log(WebAssembly.Module.customSections(module, "name").length);
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
12 | const streamed = await WebAssembly.instantiateStreaming(fetch("https://example.com/x.wasm"));
hint: the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)
webassembly-fences.ts:12:26 - error SC2020: 'WebAssembly.instantiateStreaming' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, Module, Instance and the error classes over modules translated at build time, but not this part of the API
11 | console.log(WebAssembly.Module.customSections(module, "name").length);
12 | const streamed = await WebAssembly.instantiateStreaming(fetch("https://example.com/x.wasm"));
8 | console.log(WebAssembly.Module.imports(module).length);
9 | const streamed = await WebAssembly.instantiateStreaming(fetch("https://example.com/x.wasm"));
| ^~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
13 | console.log(streamed.instance);
10 | console.log(streamed.instance);
hint: the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)
hint: the streaming functions, Tag and Exception, and the classes used as values other than with new and instanceof are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)
webassembly-fences.ts:14:16 - error SC2020: 'WebAssembly.Module' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, Module, Instance and the error classes over modules translated at build time, but not this part of the API
webassembly-fences.ts:11:16 - error SC2020: 'WebAssembly.Module' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, and the error classes over modules translated at build time, but not this part of the API
13 | console.log(streamed.instance);
14 | const Ctor = WebAssembly.Module;
10 | console.log(streamed.instance);
11 | const Ctor = WebAssembly.Module;
| ^~~~~~~~~~~~~~~~~~
15 | console.log(Ctor);
12 | console.log(Ctor);
hint: the Memory, Table and Global constructors, Module.imports, exports and customSections, and the streaming functions are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)
hint: the streaming functions, Tag and Exception, and the classes used as values other than with new and instanceof are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)
webassembly-fences.ts:13:18 - error SC2020: 'WebAssembly.Memory' is not supported yet: scriptc implements WebAssembly.compile, instantiate, validate, the Module, Instance, Memory, Table and Global constructors, Module.imports, exports and customSections, and the error classes over modules translated at build time, but not this part of the API
12 | console.log(Ctor);
13 | const Memory = WebAssembly.Memory;
| ^~~~~~~~~~~~~~~~~~
14 | console.log(new Memory({ initial: 1 }));
hint: the streaming functions, Tag and Exception, and the classes used as values other than with new and instanceof are planned; 'scriptc coverage' lists the modules this program loads (https://scriptc.dev/limitations)
@@ -0,0 +1 @@
{ "plain": 6843, "san": 50107 }
@@ -0,0 +1 @@
{ "plain": 7155, "san": 9278 }
@@ -0,0 +1 @@
{ "plain": 2996, "san": 7565 }
@@ -0,0 +1 @@
{ "plain": 4431, "san": 4894 }
@@ -0,0 +1 @@
{ "plain": 14016, "san": 15948 }
+5 -3
View File
@@ -44,8 +44,9 @@ test("unreached blockers report in their own group", async () => {
});
test("WebAssembly modules the build can see report with their sources", async () => {
// A resolved path and an inline literal (glue.js), a packaged .wasm
// (wasm-fixture-pkg), and package.json's scriptc.wasm list.
// A resolved path, a resolved .so shared library and an inline literal
// (glue.js), a packaged .wasm (wasm-fixture-pkg), and package.json's
// scriptc.wasm list. A resolved .so that is not a module is not reported.
await expect(report(fixture("wasm/main.ts"))).toMatchFileSnapshot(
"__snapshots__/coverage-wasm.txt",
);
@@ -61,6 +62,7 @@ test("WebAssembly modules the build can see report with their sources", async ()
).toEqual([
["inline", "valid", [], ["inline"]],
["tests/coverage-fixtures/wasm/add.wasm", "valid", [], ["path", "config"]],
["tests/coverage-fixtures/wasm/ext.so", "valid", [], ["path"]],
[
"tests/coverage-fixtures/wasm/node_modules/wasm-fixture-pkg/lib/core.wasm",
"invalid",
@@ -70,7 +72,7 @@ test("WebAssembly modules the build can see report with their sources", async ()
["tests/coverage-fixtures/wasm/build/generated.wasm", "unsupported", ["simd"], ["config"]],
]);
const envelope = coverageEnvelope(coverage, { compilerVersion: "test" });
expect(envelope.wasmModules?.map((m) => m.sha256.length)).toEqual([64, 64, 64, 64]);
expect(envelope.wasmModules?.map((m) => m.sha256.length)).toEqual([64, 64, 64, 64, 64]);
});
test("divergences report as warnings, never as blockers", async () => {
+42
View File
@@ -0,0 +1,42 @@
;; tests/corpus/webassembly-dylink: an extension shaped like quickjs-wasi's
;; WebAssembly shared libraries (wasi-sdk -shared): it shares the main
;; module's memory and table, places its data and functions at the
;; __memory_base and __table_base the loader assigns, reaches the main
;; module through GOT.mem and GOT.func globals and env imports, and leaves
;; one symbol unresolved and one resolved by its own export. The program's
;; base64 copy appends the dylink.0 section: memory size 48 (alignment 2^2),
;; table size 2, needed "libc.so".
(module
(type $i_i (func (param i32) (result i32)))
(import "env" "memory" (memory 1))
(import "env" "__indirect_function_table" (table 0 funcref))
(import "env" "__stack_pointer" (global $sp (mut i32)))
(import "env" "__memory_base" (global $mb i32))
(import "env" "__table_base" (global $tb i32))
(import "GOT.mem" "counter" (global $got_counter (mut i32)))
(import "GOT.func" "helper" (global $got_helper (mut i32)))
(import "env" "js_register" (func $js_register (param i32 i32) (result i32)))
(import "env" "missing_symbol" (func $missing (param i32) (result i32)))
(import "env" "self_symbol" (func $self (param i32) (result i32)))
(data (global.get $mb) "name=demo\0aversion=1.2\00")
(elem (global.get $tb) $triple $via_helper)
(func $triple (type $i_i) (i32.mul (local.get 0) (i32.const 3)))
(func $via_helper (type $i_i)
(call_indirect (type $i_i) (local.get 0) (global.get $got_helper)))
(func (export "__wasm_apply_data_relocs")
(global.set $got_counter (i32.add (global.get $mb) (i32.const 32))))
(func (export "__wasm_call_ctors")
(i32.store (global.get $got_counter) (i32.const 5)))
(func (export "qjs_ext_demo_init") (param i32 i32) (result i32)
(drop (call $js_register (local.get 0) (global.get $tb)))
(drop (call $js_register (local.get 0) (i32.add (global.get $tb) (i32.const 1))))
(i32.sub (local.get 1) (i32.const 200)))
(func (export "qjs_ext_demo_versions") (result i32) (global.get $mb))
(func (export "self_symbol") (param i32) (result i32) (i32.add (local.get 0) (i32.const 1)))
(func (export "use_self") (param i32) (result i32) (call $self (local.get 0)))
(func (export "use_missing") (param i32) (result i32) (call $missing (local.get 0)))
(func (export "stack_pointer") (result i32) (global.get $sp))
(func (export "bump_counter") (result i32)
(i32.store (global.get $got_counter) (i32.add (i32.load (global.get $got_counter)) (i32.const 1)))
(i32.load (global.get $got_counter)))
)
+33
View File
@@ -0,0 +1,33 @@
;; tests/corpus/webassembly-dylink: the main module of a dynamically linked
;; program, shaped like quickjs-wasi's quickjs.wasm: it exports its memory,
;; its function table, __stack_pointer, malloc and the entry points an
;; extension's init function calls.
(module
(type $i_i (func (param i32) (result i32)))
(memory (export "memory") 2)
(table (export "__indirect_function_table") 4 funcref)
(global $sp (export "__stack_pointer") (mut i32) (i32.const 65536))
(global $heap (mut i32) (i32.const 1024))
(global $registered (mut i32) (i32.const 0))
(func (export "malloc") (param i32) (result i32)
(local i32)
(local.set 1 (global.get $heap))
(global.set $heap
(i32.and (i32.add (i32.add (global.get $heap) (local.get 0)) (i32.const 15)) (i32.const -16)))
(local.get 1))
(func (export "qjs_get_context_ptr") (result i32) (i32.const 100))
(func (export "qjs_get_runtime_ptr") (result i32) (i32.const 200))
;; js_register(ctx, tableIndex): remembers a function an extension adds.
(func (export "js_register") (param i32 i32) (result i32)
(i32.store
(i32.add (i32.const 512) (i32.mul (global.get $registered) (i32.const 4)))
(local.get 1))
(global.set $registered (i32.add (global.get $registered) (i32.const 1)))
(global.get $registered))
;; js_call(n, x): calls the n-th registered function through the table.
(func (export "js_call") (param i32 i32) (result i32)
(call_indirect (type $i_i)
(local.get 1)
(i32.load (i32.add (i32.const 512) (i32.mul (local.get 0) (i32.const 4))))))
(func (export "helper") (param i32) (result i32) (i32.add (local.get 0) (i32.const 1000)))
)
+11
View File
@@ -0,0 +1,11 @@
;; tests/corpus/webassembly-jsapi.mjs: a module that defines and exports a
;; memory, a table and globals, exported twice each, for identity and for
;; importing what one instance exports into another.
(module
(memory (export "mem") (export "mem2") 1 4)
(table (export "tab") 2 8 funcref)
(global (export "g") (export "g2") (mut i32) (i32.const 7))
(global (export "fixed") f64 (f64.const 1.5))
(func (export "id") (param i32) (result i32) (local.get 0))
(elem (i32.const 1) 0)
)
+37
View File
@@ -0,0 +1,37 @@
;; tests/corpus/webassembly-jsapi.mjs: imports of every kind, filled by the
;; Memory, Table and Global constructors. The program's base64 copy also
;; carries custom sections ("dylink.0" [1 2 3], "meta" "hello", "dylink.0"
;; [4 5] and "" []), appended after wat2wasm.
(module
(import "env" "log" (func $log (param i32)))
(import "env" "memory" (memory 1 4))
(import "env" "table" (table 2 8 funcref))
(import "env" "counter" (global $counter (mut i32)))
(import "env" "scale" (global $scale f64))
(import "env" "big" (global $big (mut i64)))
(import "env" "ref" (global $ref (mut externref)))
(type $i_i (func (param i32) (result i32)))
(func $double (type $i_i) (i32.mul (local.get 0) (i32.const 2)))
(func $square (type $i_i) (i32.mul (local.get 0) (local.get 0)))
(elem (i32.const 0) $double $square)
(func (export "bump") (result i32)
(global.set $counter (i32.add (global.get $counter) (i32.const 1)))
(global.get $counter))
(func (export "scaled") (param f64) (result f64) (f64.mul (local.get 0) (global.get $scale)))
(func (export "bigAdd") (param i64) (global.set $big (i64.add (global.get $big) (local.get 0))))
(func (export "getRef") (result externref) (global.get $ref))
(func (export "setRef") (param externref) (global.set $ref (local.get 0)))
(func (export "apply") (param i32 i32) (result i32)
(call_indirect (type $i_i) (local.get 1) (local.get 0)))
(func (export "store") (param i32 i32) (i32.store8 (local.get 0) (local.get 1)))
(func (export "load") (param i32) (result i32) (i32.load8_u (local.get 0)))
(func (export "size") (result i32) (memory.size))
(func (export "grow") (param i32) (result i32) (memory.grow (local.get 0)))
(func (export "tableSize") (result i32) (table.size 0))
(func (export "say") (param i32) (call $log (local.get 0)))
(export "memory" (memory 0))
(export "table" (table 0))
(export "counter" (global $counter))
(export "alsoCounter" (global $counter))
(export "double" (func $double))
)
+23
View File
@@ -0,0 +1,23 @@
(module
(import "env" "make" (func $make (result externref)))
(table $slots 1 externref)
(global $slot (mut externref) (ref.null extern))
(memory 1)
(func (export "put") (param externref)
(table.set $slots (i32.const 0) (local.get 0)))
(func (export "take") (result externref) (local externref)
(local.set 0 (table.get $slots (i32.const 0)))
(table.set $slots (i32.const 0) (ref.null extern))
(local.get 0))
(func (export "putg") (param externref)
(global.set $slot (local.get 0)))
(func (export "takeg") (result externref)
(global.get $slot)
(global.set $slot (ref.null extern)))
(func (export "fresh") (result externref)
(call $make))
;; i32.load with the memory index written out (memarg flags 0x42, memory
;; 0, offset 8).
(func (export "load") (result i32)
(i32.load 0 offset=8 (i32.const 0)))
(data (i32.const 8) "abcd"))
+6 -11
View File
@@ -10,10 +10,10 @@
* --spec <WebAssembly/spec at tag wg-2.0>/test/core
*
* Left out: modules given as text, modules the decoder does not accept
* within the translatable feature level, modules that import a spectest
* memory or table (the Memory and Table constructors are not supported
* yet) and every command that targets them, and commands with v128
* values. Also left out, because they test NaN payloads, which neither
* within the translatable feature level and every command that targets
* them, and commands with v128 values. The runner's spectest module
* provides the harness's memory and table (new WebAssembly.Memory and
* Table), so modules that import them run. Also left out, because they test NaN payloads, which neither
* side preserves the way the other does: actions whose f32 or f64
* arguments are NaNs other than the canonical quiet NaN (V8 and scriptc
* carry NaN payloads through JavaScript numbers differently), and
@@ -113,6 +113,7 @@ export const GROUPS = {
"exports",
"linking",
],
imports: ["imports", "table", "names"],
};
const MAX_ACTIONS = 400;
@@ -204,13 +205,7 @@ for (const [group, names] of Object.entries(GROUPS)) {
}
const bytes = readFileSync(join(dir, c.filename));
const summary = analyzeWasm(new Uint8Array(bytes));
const imports = WebAssembly.Module.imports(new WebAssembly.Module(bytes));
const usable =
summary.status === "valid" &&
!imports.some(
(i) => i.module === "spectest" && (i.kind === "memory" || i.kind === "table"),
);
if (!usable) {
if (summary.status !== "valid") {
if (c.type === "module") {
currentSkipped = true;
if (c.name) skipped.add(c.name);
+4 -1
View File
@@ -5,7 +5,8 @@
* error. Module bytes are inline base64 literals in each program's
* data.mjs, which the build translates. */
/** The spec harness's `spectest` module, without its memory and table. */
/** The spec harness's `spectest` module (the reference interpreter's JS
* harness), with its memory and table. */
function spectestImports(log) {
return {
print() {
@@ -33,6 +34,8 @@ function spectestImports(log) {
global_i64: 666n,
global_f32: 666.6,
global_f64: 666.6,
table: new WebAssembly.Table({ initial: 10, maximum: 20, element: "anyfunc" }),
memory: new WebAssembly.Memory({ initial: 1, maximum: 2 }),
};
}