Crafter.Build/wasi-runtime/runtime.js

926 lines
40 KiB
JavaScript
Raw Normal View History

V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
// Minimal browser WASI shim. Loaded by index.html, which sets
// window.CRAFTER_WASM_URL before this script runs so a single runtime.js
// can serve any output name. Most syscalls return 0 — enough to host a
// hello-world that uses fd_write (stdout) and the args/environ probes
// libc invokes during startup. Extend as needed.
const textEncoder = new TextEncoder();
2026-05-19 17:20:20 +02:00
// ─── Two VFS roots ─────────────────────────────────────────────────────
// fd=3 ⟶ read-only baked layer at "/". Populated from files.json at
// startup, keyed by basename — the bin-dir layout `cfg.assets`
// produces is flat enough that basename collapse doesn't
// collide for legitimate assets. Writes are rejected.
// fd=4 ⟶ read/write persistent layer at "/persistent". Backed by OPFS
// (Origin Private File System), keyed by relative path so
// nested directories work. The wasm sees an in-memory map
// (writes are synchronous from its POV); a background task
// mirrors dirty entries back to OPFS so they survive reload.
//
// wasi-libc walks preopens starting at fd=3 and resolves every open()
// against the longest matching preopen. "/persistent/foo.json" hits fd=4
// with relpath "foo.json"; "/Inter.ttf" hits fd=3 with relpath "Inter.ttf".
const readonlyVfs = new Map(); // basename → Uint8Array (immutable)
const persistentVfs = new Map(); // relpath → Uint8Array (mutable)
// Paths the wasm has mkdir'd. Our backing store is flat-keyed so dirs
// don't otherwise exist on their own — but libstdc++'s
// `create_directories` mkdir's, then re-stats each component, and
// expects the stat to report "directory". Tracking explicit mkdir'd
// paths here keeps that loop convergent.
const persistentDirs = new Set();
const ROOTS = {
3: { kind: "ro", map: readonlyVfs, name: "/" },
4: { kind: "rw", map: persistentVfs, name: "/persistent" },
};
2026-05-26 22:50:08 +02:00
function resolveKey(_root, raw) {
// Both VFS layers key by relative path now. We did keep the read-
// only side basename-collapsed up to Phase 1, but that broke as
// soon as two bundled dirs shipped a file with the same basename
// (e.g. maps/A/map.json vs maps/B/map.json — only one survives the
// preload). Full-path keying matches what files.json actually
// serves and what cfg.assets actually deploys.
2026-05-19 17:20:20 +02:00
let key = raw.replace(/\\/g, "/");
while (key.startsWith("./")) key = key.slice(2);
while (key.startsWith("/")) key = key.slice(1);
return key;
}
2026-05-26 22:50:08 +02:00
// Directory tree for the read-only baked layer. Built after files.json
// preload completes — each path like "maps/A/map.json" registers as:
// "" → { "maps": "dir" }
// "maps" → { "A": "dir" }
// "maps/A" → { "map.json":"file" }
// Used by path_open (recognize directory paths) and fd_readdir
// (iterate entries).
const readonlyDirs = new Map();
function roBuildDirTree() {
readonlyDirs.clear();
const addChild = (parent, name, kind) => {
let entries = readonlyDirs.get(parent);
if (!entries) {
entries = new Map();
readonlyDirs.set(parent, entries);
}
if (!entries.has(name)) entries.set(name, kind);
};
for (const path of readonlyVfs.keys()) {
const parts = path.split("/").filter(Boolean);
for (let i = 0; i < parts.length; ++i) {
const parent = parts.slice(0, i).join("/");
const name = parts[i];
const kind = (i === parts.length - 1) ? "file" : "dir";
addChild(parent, name, kind);
}
}
}
2026-05-19 17:20:20 +02:00
// ─── OPFS mirror for the persistent layer ──────────────────────────────
let opfsRoot = null;
const dirtyPaths = new Set();
let flushTimer = null;
const FLUSH_DEBOUNCE_MS = 750;
async function opfsResolveDir(parts, create) {
if (!opfsRoot) return null;
let dir = opfsRoot;
for (const part of parts) {
if (!part) continue;
dir = await dir.getDirectoryHandle(part, { create });
}
return dir;
}
async function opfsHydrate() {
if (!navigator.storage || !navigator.storage.getDirectory) {
console.warn("[wasi] OPFS unavailable — persistent writes will not survive reload");
return;
}
try {
opfsRoot = await navigator.storage.getDirectory();
} catch (e) {
console.warn("[wasi] OPFS getDirectory failed:", e?.message);
return;
}
async function walk(dir, prefix) {
for await (const [name, handle] of dir.entries()) {
const full = prefix ? `${prefix}/${name}` : name;
if (handle.kind === "file") {
try {
const file = await handle.getFile();
persistentVfs.set(full, new Uint8Array(await file.arrayBuffer()));
} catch (e) {
console.warn(`[wasi] OPFS read failed for ${full}:`, e?.message);
}
} else if (handle.kind === "directory") {
await walk(handle, full);
}
}
}
await walk(opfsRoot, "");
}
async function opfsFlushOne(path) {
if (!opfsRoot) return;
const parts = path.split("/").filter(Boolean);
if (parts.length === 0) return;
const data = persistentVfs.get(path);
if (data === undefined) {
// Removed entry — delete from OPFS, walking parents.
try {
const parent = await opfsResolveDir(parts.slice(0, -1), false);
await parent.removeEntry(parts[parts.length - 1]);
} catch (e) {
// Missing entry is fine; other errors get reported.
if (e && e.name !== "NotFoundError") {
console.warn(`[wasi] OPFS remove failed for ${path}:`, e?.message);
}
}
return;
}
try {
const dir = await opfsResolveDir(parts.slice(0, -1), true);
const fh = await dir.getFileHandle(parts[parts.length - 1], { create: true });
const w = await fh.createWritable();
await w.write(data);
await w.close();
} catch (e) {
console.warn(`[wasi] OPFS write failed for ${path}:`, e?.message);
}
}
async function opfsFlushAll() {
if (!opfsRoot) return;
const batch = Array.from(dirtyPaths);
dirtyPaths.clear();
for (const path of batch) {
await opfsFlushOne(path);
}
}
function scheduleFlush() {
if (flushTimer !== null) return;
flushTimer = setTimeout(() => {
flushTimer = null;
opfsFlushAll();
}, FLUSH_DEBOUNCE_MS);
}
function markDirty(path) {
dirtyPaths.add(path);
scheduleFlush();
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
class Wasi {
#encodedStdin;
#envEncodedStrings;
#argEncodedStrings;
instance;
2026-05-18 05:23:11 +02:00
#fdTable;
#nextFd;
#decoder;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
2026-05-19 17:20:20 +02:00
constructor({ env, stdin, args }) {
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
this.#encodedStdin = textEncoder.encode(stdin);
const envStrings = Object.entries(env).map(([k, v]) => `${k}=${v}`);
this.#envEncodedStrings = envStrings.map(s => textEncoder.encode(s + "\0"));
this.#argEncodedStrings = args.map(s => textEncoder.encode(s + "\0"));
2026-05-18 05:23:11 +02:00
this.#fdTable = new Map();
2026-05-19 17:20:20 +02:00
this.#nextFd = 100; // stay clear of stdio + preopens
2026-05-18 05:23:11 +02:00
this.#decoder = new TextDecoder();
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
this.bind();
}
bind() {
2026-05-18 05:23:11 +02:00
// wasi imports are looked up as plain function references at
// instantiate time, so any method that touches `this` MUST be
// explicitly bound here. Anything purely no-op (returning 0) can
// stay unbound.
const m = [
"args_get", "args_sizes_get",
"environ_get", "environ_sizes_get",
2026-05-26 22:50:08 +02:00
"fd_read", "fd_write", "fd_pwrite", "fd_readdir",
2026-05-19 17:20:20 +02:00
"fd_close", "fd_seek", "fd_tell",
2026-05-18 05:23:11 +02:00
"fd_filestat_get", "fd_fdstat_get",
"fd_prestat_get", "fd_prestat_dir_name",
2026-05-19 17:20:20 +02:00
"path_open", "path_create_directory", "path_remove_directory",
"path_unlink_file", "path_rename", "path_filestat_get",
2026-05-26 22:50:08 +02:00
// clock_res_get + clock_time_get touch this.instance.exports
// .memory; the old stubs were no-ops and didn't need binding.
"clock_res_get", "clock_time_get",
// random_get now writes into this.instance.exports.memory too.
"random_get",
2026-05-18 05:23:11 +02:00
];
for (const name of m) this[name] = this[name].bind(this);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
}
args_sizes_get(argCountPtr, argBufferSizePtr) {
const argByteLength = this.#argEncodedStrings.reduce((sum, val) => sum + val.byteLength, 0);
const countPointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, argCountPtr, 1);
const sizePointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, argBufferSizePtr, 1);
countPointerBuffer[0] = this.#argEncodedStrings.length;
sizePointerBuffer[0] = argByteLength;
return 0;
}
args_get(argsPtr, argBufferPtr) {
const argsByteLength = this.#argEncodedStrings.reduce((sum, val) => sum + val.byteLength, 0);
const argsPointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, argsPtr, this.#argEncodedStrings.length);
const argsBuffer = new Uint8Array(this.instance.exports.memory.buffer, argBufferPtr, argsByteLength);
let pointerOffset = 0;
for (let i = 0; i < this.#argEncodedStrings.length; i++) {
argsPointerBuffer[i] = argBufferPtr + pointerOffset;
argsBuffer.set(this.#argEncodedStrings[i], pointerOffset);
pointerOffset += this.#argEncodedStrings[i].byteLength;
}
return 0;
}
fd_write(fd, iovsPtr, iovsLength, bytesWrittenPtr) {
2026-05-19 17:20:20 +02:00
const dv = new DataView(this.instance.exports.memory.buffer);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
const iovs = new Uint32Array(this.instance.exports.memory.buffer, iovsPtr, iovsLength * 2);
if (fd === 1 || fd === 2) {
let text = "";
let totalBytesWritten = 0;
const decoder = new TextDecoder();
for (let i = 0; i < iovsLength * 2; i += 2) {
const offset = iovs[i];
const length = iovs[i + 1];
text += decoder.decode(new Int8Array(this.instance.exports.memory.buffer, offset, length));
totalBytesWritten += length;
}
2026-05-19 17:20:20 +02:00
dv.setInt32(bytesWrittenPtr, totalBytesWritten, true);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
(fd === 2 ? console.error : console.log)(text);
2026-05-19 17:20:20 +02:00
return 0;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
}
2026-05-19 17:20:20 +02:00
const entry = this.#fdTable.get(fd);
if (!entry) {
dv.setInt32(bytesWrittenPtr, 0, true);
return 8; // EBADF
}
const root = ROOTS[entry.rootFd];
if (!root || root.kind !== "rw") {
dv.setInt32(bytesWrittenPtr, 0, true);
return 76; // EROFS — close enough; libc surfaces this as "no permission"
}
// Sum iov lengths so we can grow the entry once.
let total = 0;
for (let i = 0; i < iovsLength * 2; i += 2) total += iovs[i + 1];
let data = root.map.get(entry.name) ?? new Uint8Array(0);
const end = entry.offset + total;
if (data.byteLength < end) {
const grown = new Uint8Array(end);
grown.set(data, 0);
data = grown;
root.map.set(entry.name, data);
}
const memory = new Uint8Array(this.instance.exports.memory.buffer);
let written = 0;
for (let i = 0; i < iovsLength * 2; i += 2) {
const offset = iovs[i];
const length = iovs[i + 1];
data.set(memory.subarray(offset, offset + length), entry.offset + written);
written += length;
}
entry.offset += written;
dv.setInt32(bytesWrittenPtr, written, true);
markDirty(entry.name);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
return 0;
}
2026-05-19 17:20:20 +02:00
fd_pwrite(fd, iovsPtr, iovsLength, offsetArg, bytesWrittenPtr) {
// pwrite is positioned write — same as fd_write but the seek pos
// isn't advanced. We just temporarily override entry.offset.
const entry = this.#fdTable.get(fd);
if (!entry) {
new DataView(this.instance.exports.memory.buffer)
.setInt32(bytesWrittenPtr, 0, true);
return 8;
}
const saved = entry.offset;
entry.offset = typeof offsetArg === "bigint" ? Number(offsetArg) : Number(offsetArg);
const rc = this.fd_write(fd, iovsPtr, iovsLength, bytesWrittenPtr);
entry.offset = saved;
return rc;
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
fd_read(fd, iovsPtr, iovsLength, bytesReadPtr) {
const memory = new Uint8Array(this.instance.exports.memory.buffer);
const iovs = new Uint32Array(this.instance.exports.memory.buffer, iovsPtr, iovsLength * 2);
2026-05-18 05:23:11 +02:00
const dataView = new DataView(this.instance.exports.memory.buffer);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
let totalBytesRead = 0;
if (fd === 0) {
for (let i = 0; i < iovsLength * 2; i += 2) {
const offset = iovs[i];
const length = iovs[i + 1];
const chunk = this.#encodedStdin.slice(0, length);
this.#encodedStdin = this.#encodedStdin.slice(length);
memory.set(chunk, offset);
totalBytesRead += chunk.byteLength;
if (this.#encodedStdin.length === 0) break;
}
dataView.setInt32(bytesReadPtr, totalBytesRead, true);
2026-05-18 05:23:11 +02:00
return 0;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
}
2026-05-18 05:23:11 +02:00
const entry = this.#fdTable.get(fd);
if (!entry) {
dataView.setInt32(bytesReadPtr, 0, true);
return 8; // EBADF
}
2026-05-19 17:20:20 +02:00
const root = ROOTS[entry.rootFd];
const file = root ? root.map.get(entry.name) : undefined;
if (!file) {
dataView.setInt32(bytesReadPtr, 0, true);
return 0; // EOF
}
2026-05-18 05:23:11 +02:00
for (let i = 0; i < iovsLength * 2; i += 2) {
const offset = iovs[i];
const length = iovs[i + 1];
const remaining = file.byteLength - entry.offset;
if (remaining <= 0) break;
const n = Math.min(length, remaining);
memory.set(file.subarray(entry.offset, entry.offset + n), offset);
entry.offset += n;
totalBytesRead += n;
if (n < length) break;
}
dataView.setInt32(bytesReadPtr, totalBytesRead, true);
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
return 0;
}
fd_advise() { return 0; }
2026-05-18 05:23:11 +02:00
fd_close(fd) {
this.#fdTable.delete(fd);
return 0;
}
fd_fdstat_get(fd, statPtr) {
// 24 bytes: filetype(1) + flags(2) + padding + rights_base(8) + rights_inheriting(8).
const dv = new DataView(this.instance.exports.memory.buffer);
2026-05-19 17:20:20 +02:00
let filetype;
2026-05-26 22:50:08 +02:00
if (ROOTS[fd]) {
filetype = 3; // directory — preopen mount
} else {
const entry = this.#fdTable.get(fd);
if (entry) filetype = entry.isDir ? 3 : 4;
else filetype = 0;
}
2026-05-18 05:23:11 +02:00
dv.setUint8(statPtr + 0, filetype);
dv.setUint16(statPtr + 2, 0, true);
dv.setBigUint64(statPtr + 8, 0xFFFFFFFFFFFFFFFFn, true);
dv.setBigUint64(statPtr + 16, 0xFFFFFFFFFFFFFFFFn, true);
return 0;
}
// wasi-libc walks preopens starting at fd=3 until fd_prestat_get returns
// EBADF, then resolves every relative open against one of the discovered
2026-05-19 17:20:20 +02:00
// dirs. Two preopens: fd=3 ("/") for baked assets, fd=4 ("/persistent")
// for OPFS-backed mutable state.
2026-05-18 05:23:11 +02:00
fd_prestat_get(fd, prestatPtr) {
const dv = new DataView(this.instance.exports.memory.buffer);
2026-05-19 17:20:20 +02:00
const root = ROOTS[fd];
if (!root) return 8; // EBADF — terminates the walk
// prestat_t: tag(u8) + padding to 4 + u.dir.pr_name_len(u32). 8 bytes.
dv.setUint8(prestatPtr + 0, 0); // tag = preopentype_dir
dv.setUint32(prestatPtr + 4, root.name.length, true);
return 0;
2026-05-18 05:23:11 +02:00
}
fd_prestat_dir_name(fd, pathPtr, pathLen) {
2026-05-19 17:20:20 +02:00
const root = ROOTS[fd];
if (!root) return 8;
2026-05-18 05:23:11 +02:00
const memory = new Uint8Array(this.instance.exports.memory.buffer);
2026-05-19 17:20:20 +02:00
const name = textEncoder.encode(root.name);
2026-05-18 05:23:11 +02:00
memory.set(name.subarray(0, Math.min(name.byteLength, pathLen)), pathPtr);
return 0;
}
2026-05-26 22:50:08 +02:00
// WASI clock_res_get(clock_id, resPtr): writes a u64 nanosecond
// resolution to *resPtr. We back every clock with performance.now()
// which the platform documents as ≥5µs resolution on modern browsers
// — round-trip via Math.round() loses sub-µs precision but anything
// more aggressive would mis-represent the actual host resolution.
clock_res_get(_clockId, resPtr) {
const dv = new DataView(this.instance.exports.memory.buffer);
dv.setBigUint64(resPtr, 1000n, /*littleEndian*/ true); // 1µs in ns
return 0;
}
// WASI clock_time_get(clock_id, precision, timePtr): writes a u64
// nanosecond timestamp. The stub used to return 0 unconditionally,
// which made every std::chrono::high_resolution_clock::now() in wasm
// return the same value — Physics::Update read delta=0 each frame
// and never advanced. Use performance.now() for MONOTONIC and a
// Date.now()-relative reference for REALTIME so std::chrono actually
// works.
clock_time_get(clockId, _precision, timePtr) {
const dv = new DataView(this.instance.exports.memory.buffer);
// clock IDs per wasi-snapshot-preview1:
// 0 = realtime, 1 = monotonic, 2 = process_cputime_id, 3 = thread_cputime_id
let nanos;
if (clockId === 0) {
// Date.now() returns ms since the Unix epoch.
nanos = BigInt(Date.now()) * 1000000n;
} else {
// performance.now() returns ms since the page navigation
// start with sub-ms precision. Anchor against
// performance.timeOrigin so caller sees a clean monotonic
// walltime equivalent.
const ms = performance.now();
nanos = BigInt(Math.round(ms * 1e6));
}
dv.setBigUint64(timePtr, nanos, /*littleEndian*/ true);
return 0;
}
2026-05-18 05:23:11 +02:00
fd_seek(fd, offsetLow, whence, newOffsetPtr) {
// offsetLow is a BigInt under wasi-snapshot-preview1's two-i32 ABI?
// No — wasi-snapshot-preview1 fd_seek's signature is (fd, filedelta:i64,
// whence:u8, newoffset:ptr<filesize>). JS receives the i64 as BigInt.
const entry = this.#fdTable.get(fd);
const dv = new DataView(this.instance.exports.memory.buffer);
if (!entry) {
dv.setBigUint64(newOffsetPtr, 0n, true);
return 8;
}
const delta = typeof offsetLow === "bigint" ? Number(offsetLow) : Number(offsetLow);
2026-05-19 17:20:20 +02:00
const root = ROOTS[entry.rootFd];
const file = root ? root.map.get(entry.name) : undefined;
const size = file ? file.byteLength : 0;
2026-05-18 05:23:11 +02:00
let newOff;
switch (whence) {
case 0: newOff = delta; break; // SEEK_SET
case 1: newOff = entry.offset + delta; break; // SEEK_CUR
2026-05-19 17:20:20 +02:00
case 2: newOff = size + delta; break; // SEEK_END
2026-05-18 05:23:11 +02:00
default: return 28; // EINVAL
}
if (newOff < 0) newOff = 0;
2026-05-19 17:20:20 +02:00
// Writeable fds may seek past EOF (the next write extends the
// backing buffer); read-only fds clamp to EOF.
if (!root || root.kind !== "rw") {
if (newOff > size) newOff = size;
}
2026-05-18 05:23:11 +02:00
entry.offset = newOff;
dv.setBigUint64(newOffsetPtr, BigInt(newOff), true);
return 0;
}
fd_tell(fd, offsetPtr) {
const entry = this.#fdTable.get(fd);
const dv = new DataView(this.instance.exports.memory.buffer);
if (!entry) { dv.setBigUint64(offsetPtr, 0n, true); return 8; }
dv.setBigUint64(offsetPtr, BigInt(entry.offset), true);
return 0;
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
fd_allocate() { return 0; }
fd_datasync() { return 0; }
fd_fdstat_set_flags() { return 0; }
fd_fdstat_set_rights() { return 0; }
2026-05-18 05:23:11 +02:00
fd_filestat_get(fd, statPtr) {
// Layout: dev(8) ino(8) filetype(1) +pad nlink(8) size(8) atim(8) mtim(8) ctim(8) = 64 bytes.
const dv = new DataView(this.instance.exports.memory.buffer);
2026-05-19 17:20:20 +02:00
let filetype = 4; // regular_file (default)
let size = 0;
if (ROOTS[fd]) {
filetype = 3; // directory — preopen mount itself
} else {
const entry = this.#fdTable.get(fd);
if (!entry) return 8;
2026-05-26 22:50:08 +02:00
if (entry.isDir) {
filetype = 3;
} else {
const root = ROOTS[entry.rootFd];
const file = root ? root.map.get(entry.name) : undefined;
size = file ? file.byteLength : 0;
}
2026-05-19 17:20:20 +02:00
}
2026-05-18 05:23:11 +02:00
dv.setBigUint64(statPtr + 0, 0n, true);
dv.setBigUint64(statPtr + 8, 0n, true);
2026-05-19 17:20:20 +02:00
dv.setUint8(statPtr + 16, filetype);
2026-05-18 05:23:11 +02:00
dv.setBigUint64(statPtr + 24, 1n, true);
2026-05-19 17:20:20 +02:00
dv.setBigUint64(statPtr + 32, BigInt(size), true);
2026-05-18 05:23:11 +02:00
dv.setBigUint64(statPtr + 40, 0n, true);
dv.setBigUint64(statPtr + 48, 0n, true);
dv.setBigUint64(statPtr + 56, 0n, true);
return 0;
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
fd_filestat_set_size() { return 0; }
fd_filestat_set_times() { return 0; }
fd_pread() { return 0; }
2026-05-26 22:50:08 +02:00
fd_readdir(fd, bufPtr, bufLen, cookieArg, bytesUsedPtr) {
// wasi dirent: dircookie(8) + inode(8) + namlen(4) + filetype(1)
// = 24 bytes header (the 3-byte tail is implicit padding before
// the variable-length name). The cookie returned in each entry
// is the value the caller passes in to read the *next* entry.
// We return at most bufLen bytes; libc reads, processes, calls
// back with the last entry's cookie, repeats until short read.
const dv = new DataView(this.instance.exports.memory.buffer);
const mem = new Uint8Array(this.instance.exports.memory.buffer);
const entry = this.#fdTable.get(fd);
if (!entry || !entry.isDir) {
dv.setInt32(bytesUsedPtr, 0, true);
return 8; // EBADF
}
const root = ROOTS[entry.rootFd];
if (!root) {
dv.setInt32(bytesUsedPtr, 0, true);
return 8;
}
// Gather the immediate children of `entry.name`.
let children;
if (root.kind === "ro") {
const dirMap = readonlyDirs.get(entry.name);
children = dirMap ? Array.from(dirMap.entries()) : [];
} else {
// Persistent: derive children from the keys themselves —
// strip the prefix, take the first remaining component,
// dedupe (a subdir contributes one entry per child file).
const prefix = entry.name === "" ? "" : entry.name + "/";
const seen = new Map();
for (const k of root.map.keys()) {
if (!k.startsWith(prefix)) continue;
const rest = k.slice(prefix.length);
if (rest === "") continue;
const slash = rest.indexOf("/");
if (slash < 0) {
if (!seen.has(rest)) seen.set(rest, "file");
} else {
const name = rest.slice(0, slash);
if (!seen.has(name)) seen.set(name, "dir");
}
}
// Surface mkdir'd dirs that have no files yet too.
const dirPrefix = entry.name === "" ? "" : entry.name + "/";
for (const d of persistentDirs) {
if (!d.startsWith(dirPrefix)) continue;
const rest = d.slice(dirPrefix.length);
const slash = rest.indexOf("/");
const name = slash < 0 ? rest : rest.slice(0, slash);
if (name && !seen.has(name)) seen.set(name, "dir");
}
children = Array.from(seen.entries());
}
const startCookie = typeof cookieArg === "bigint" ? Number(cookieArg) : Number(cookieArg);
let cursor = bufPtr;
let used = 0;
const textEnc = new TextEncoder();
for (let i = startCookie; i < children.length; ++i) {
const [name, kind] = children[i];
const nameBytes = textEnc.encode(name);
const recSize = 24 + nameBytes.byteLength;
if (used + 24 > bufLen) break;
const nextCookie = BigInt(i + 1);
dv.setBigUint64(cursor + 0, nextCookie, true); // d_next
dv.setBigUint64(cursor + 8, BigInt(i + 1), true); // d_ino (synthetic)
dv.setUint32(cursor + 16, nameBytes.byteLength, true); // d_namlen
dv.setUint8(cursor + 20, kind === "dir" ? 3 : 4); // d_type
mem.fill(0, cursor + 21, cursor + 24); // padding
const writeName = Math.min(nameBytes.byteLength, bufLen - used - 24);
mem.set(nameBytes.subarray(0, writeName), cursor + 24);
used += 24 + writeName;
cursor += 24 + writeName;
if (writeName < nameBytes.byteLength) break; // truncated name; libc will retry
}
dv.setInt32(bytesUsedPtr, used, true);
return 0;
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
fd_renumber() { return 0; }
fd_sync() { return 0; }
2026-05-19 17:20:20 +02:00
path_filestat_get(dirfd, _dirflags, pathPtr, pathLen, statPtr) {
const root = ROOTS[dirfd];
if (!root) return 8;
const memory = new Uint8Array(this.instance.exports.memory.buffer);
const raw = this.#decoder.decode(memory.subarray(pathPtr, pathPtr + pathLen));
const key = resolveKey(root, raw);
const file = root.map.get(key);
const dv = new DataView(this.instance.exports.memory.buffer);
// Resolve filetype: 4 = regular_file, 3 = directory. The
2026-05-26 22:50:08 +02:00
// preopen mount itself (empty key) is always a directory. On
// the read-only side we built an explicit directory tree at
// startup (readonlyDirs) — anything that key-matches a node is
// a directory. On the rw side, dirs are remembered explicitly
// via persistentDirs (path_create_directory) OR implicitly by
// being a prefix of any persistent file key (so libstdc++'s
// create_directories stat→mkdir→stat loop converges).
2026-05-19 17:20:20 +02:00
let filetype;
let size = 0n;
if (file !== undefined) {
filetype = 4;
size = BigInt(file.byteLength);
2026-05-26 22:50:08 +02:00
} else if (key === "") {
2026-05-19 17:20:20 +02:00
filetype = 3;
2026-05-26 22:50:08 +02:00
} else if (root.kind === "ro" && readonlyDirs.has(key)) {
filetype = 3;
} else if (root.kind === "rw" && persistentDirs.has(key)) {
filetype = 3;
} else if (root.kind === "rw") {
2026-05-19 17:20:20 +02:00
const prefix = key + "/";
let isDir = false;
for (const k of root.map.keys()) {
if (k.startsWith(prefix)) { isDir = true; break; }
}
2026-05-26 22:50:08 +02:00
if (!isDir) return 44;
2026-05-19 17:20:20 +02:00
filetype = 3;
2026-05-26 22:50:08 +02:00
} else {
return 44; // ENOENT
2026-05-19 17:20:20 +02:00
}
dv.setBigUint64(statPtr + 0, 0n, true);
dv.setBigUint64(statPtr + 8, 0n, true);
dv.setUint8(statPtr + 16, filetype);
dv.setBigUint64(statPtr + 24, 1n, true);
dv.setBigUint64(statPtr + 32, size, true);
dv.setBigUint64(statPtr + 40, 0n, true);
dv.setBigUint64(statPtr + 48, 0n, true);
dv.setBigUint64(statPtr + 56, 0n, true);
return 0;
}
path_create_directory(dirfd, pathPtr, pathLen) {
const root = ROOTS[dirfd];
if (!root) return 8;
if (root.kind !== "rw") return 76;
// Flat-keyed Map: directories don't need to be "created" for
// files to exist under them. Track the path in persistentDirs
// so a subsequent path_filestat_get reports it as a directory
// — that's what libstdc++'s create_directories looks for to
// confirm the mkdir landed.
const memory = new Uint8Array(this.instance.exports.memory.buffer);
const raw = this.#decoder.decode(memory.subarray(pathPtr, pathPtr + pathLen));
const key = resolveKey(root, raw);
if (key) persistentDirs.add(key);
return 0;
}
path_remove_directory(dirfd, pathPtr, pathLen) {
const root = ROOTS[dirfd];
if (!root || root.kind !== "rw") return 8;
const memory = new Uint8Array(this.instance.exports.memory.buffer);
const raw = this.#decoder.decode(memory.subarray(pathPtr, pathPtr + pathLen));
const key = resolveKey(root, raw);
// Delete every entry under this prefix and mark them for OPFS removal.
const prefix = key.endsWith("/") ? key : key + "/";
for (const k of [...root.map.keys()]) {
if (k === key || k.startsWith(prefix)) {
root.map.delete(k);
markDirty(k);
}
}
return 0;
}
path_unlink_file(dirfd, pathPtr, pathLen) {
const root = ROOTS[dirfd];
if (!root || root.kind !== "rw") return 8;
const memory = new Uint8Array(this.instance.exports.memory.buffer);
const raw = this.#decoder.decode(memory.subarray(pathPtr, pathPtr + pathLen));
const key = resolveKey(root, raw);
if (!root.map.has(key)) return 44; // ENOENT
root.map.delete(key);
markDirty(key);
return 0;
}
path_rename(oldDirfd, oldPathPtr, oldPathLen,
newDirfd, newPathPtr, newPathLen) {
const oldRoot = ROOTS[oldDirfd];
const newRoot = ROOTS[newDirfd];
if (!oldRoot || !newRoot) return 8;
if (oldRoot.kind !== "rw" || newRoot.kind !== "rw") return 76;
const memory = new Uint8Array(this.instance.exports.memory.buffer);
const oldRaw = this.#decoder.decode(memory.subarray(oldPathPtr, oldPathPtr + oldPathLen));
const newRaw = this.#decoder.decode(memory.subarray(newPathPtr, newPathPtr + newPathLen));
const oldKey = resolveKey(oldRoot, oldRaw);
const newKey = resolveKey(newRoot, newRaw);
const data = oldRoot.map.get(oldKey);
if (data === undefined) return 44;
oldRoot.map.delete(oldKey);
newRoot.map.set(newKey, data);
markDirty(oldKey);
markDirty(newKey);
return 0;
}
path_open(dirfd, _dirflags, pathPtr, pathLen, oflags, _rightsBase, _rightsInh, _fdflags, openedFdPtr) {
2026-05-18 05:23:11 +02:00
const memory = new Uint8Array(this.instance.exports.memory.buffer);
const dv = new DataView(this.instance.exports.memory.buffer);
2026-05-19 17:20:20 +02:00
const root = ROOTS[dirfd];
if (!root) {
2026-05-18 05:23:11 +02:00
dv.setInt32(openedFdPtr, -1, true);
2026-05-19 17:20:20 +02:00
return 8; // EBADF
2026-05-18 05:23:11 +02:00
}
2026-05-19 17:20:20 +02:00
const raw = this.#decoder.decode(memory.subarray(pathPtr, pathPtr + pathLen));
const key = resolveKey(root, raw);
2026-05-26 22:50:08 +02:00
const O_CREAT = 0x1;
const O_DIRECTORY = 0x2;
const O_TRUNC = 0x8;
2026-05-19 17:20:20 +02:00
const create = (oflags & O_CREAT) !== 0;
2026-05-26 22:50:08 +02:00
const wantDir = (oflags & O_DIRECTORY) !== 0;
2026-05-19 17:20:20 +02:00
const trunc = (oflags & O_TRUNC) !== 0;
2026-05-26 22:50:08 +02:00
// Directory open path. wasi-libc passes O_DIRECTORY for
// `fs::directory_iterator`; we also treat "empty key + ro" as
// the preopen root.
const isRoDir = root.kind === "ro" && (key === "" || readonlyDirs.has(key));
const isRwDir = root.kind === "rw" && (key === "" || persistentDirs.has(key) ||
(() => {
const prefix = key + "/";
for (const k of root.map.keys()) if (k.startsWith(prefix)) return true;
return false;
})());
if (wantDir || (isRoDir && !root.map.has(key)) || (isRwDir && !root.map.has(key))) {
if (!isRoDir && !isRwDir) {
dv.setInt32(openedFdPtr, -1, true);
return 54; // ENOTDIR
}
const fd = this.#nextFd++;
this.#fdTable.set(fd, { rootFd: dirfd, name: key, offset: 0, isDir: true });
dv.setInt32(openedFdPtr, fd, true);
return 0;
}
2026-05-19 17:20:20 +02:00
let present = root.map.has(key);
if (!present) {
if (!create || root.kind !== "rw") {
dv.setInt32(openedFdPtr, -1, true);
return 44; // ENOENT
}
root.map.set(key, new Uint8Array(0));
markDirty(key);
present = true;
} else if (trunc && root.kind === "rw") {
root.map.set(key, new Uint8Array(0));
markDirty(key);
}
2026-05-18 05:23:11 +02:00
const fd = this.#nextFd++;
2026-05-26 22:50:08 +02:00
this.#fdTable.set(fd, { rootFd: dirfd, name: key, offset: 0, isDir: false });
2026-05-18 05:23:11 +02:00
dv.setInt32(openedFdPtr, fd, true);
return 0;
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
path_readlink() { return 0; }
path_symlink() { return 0; }
2026-05-19 17:20:20 +02:00
path_link() { return 0; }
path_filestat_set_times() { return 0; }
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
poll_oneoff() { return 0; }
sched_yield() { return 0; }
// WASI random_get(buf, buf_len): fill buf_len bytes at *buf with
// high-quality randomness. The old stub returned success without
// writing anything, so the destination kept its prior contents (zero
// in practice) — every std::random_device user got all-zero
// "randomness". That collided WebRTC peer ids across tabs in 3DForts
// (Catcrafts/3DForts#50). Back it with crypto.getRandomValues, which
// is a CSPRNG. crypto.getRandomValues rejects views longer than 65536
// bytes, so chunk for buffers above that bound.
random_get(ptr, len) {
const MAX = 65536; // QuotaExceededError above this per the WebCrypto spec
for (let off = 0; off < len; off += MAX) {
const chunk = Math.min(MAX, len - off);
// Re-view per chunk: the buffer can detach/grow between calls,
// and a fresh view also keeps each slice within the 65536 cap.
const view = new Uint8Array(this.instance.exports.memory.buffer, ptr + off, chunk);
crypto.getRandomValues(view);
}
return 0;
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
sock_accept() { return 0; }
sock_recv() { return 0; }
sock_send() { return 0; }
sock_shutdown() { return 0; }
environ_get(environPtr, environBufferPtr) {
const envByteLength = this.#envEncodedStrings.reduce((sum, val) => sum + val.byteLength, 0);
const environsPointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, environPtr, this.#envEncodedStrings.length);
const environsBuffer = new Uint8Array(this.instance.exports.memory.buffer, environBufferPtr, envByteLength);
let pointerOffset = 0;
for (let i = 0; i < this.#envEncodedStrings.length; i++) {
environsPointerBuffer[i] = environBufferPtr + pointerOffset;
environsBuffer.set(this.#envEncodedStrings[i], pointerOffset);
pointerOffset += this.#envEncodedStrings[i].byteLength;
}
return 0;
}
environ_sizes_get(environCountPtr, environBufferSizePtr) {
const envByteLength = this.#envEncodedStrings.reduce((sum, val) => sum + val.byteLength, 0);
const countPointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, environCountPtr, 1);
const sizePointerBuffer = new Uint32Array(this.instance.exports.memory.buffer, environBufferSizePtr, 1);
countPointerBuffer[0] = this.#envEncodedStrings.length;
sizePointerBuffer[0] = envByteLength;
return 0;
}
proc_exit(code) {
2026-05-18 05:23:11 +02:00
// Throw a sentinel so the wasm stack unwinds back to runtime.js
// WITHOUT executing the `unreachable` instruction wasi-libc emits
// after __wasi_proc_exit (which is declared noreturn). Trapping
// there would mark the wasm call as crashed and may interrupt any
// browser-side render loop that called _Exit on purpose (e.g.
// Crafter::Window::StartSync on DOM, which hands the loop to rAF
// and then exits main without running static destructors).
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
console.log(`[wasi] proc_exit(${code})`);
2026-05-19 17:20:20 +02:00
// Trigger a final flush so anything written this session has the
// best chance of reaching OPFS before the page is torn down.
opfsFlushAll();
2026-05-18 05:23:11 +02:00
const e = new Error(`wasi proc_exit(${code})`);
e.crafterWasiExit = code;
throw e;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
}
}
const wasmUrl = window.CRAFTER_WASM_URL;
if (!wasmUrl) {
throw new Error("runtime.js: window.CRAFTER_WASM_URL is not set (set it in index.html before loading runtime.js)");
}
2026-05-18 05:23:11 +02:00
// Preload asset files listed in files.json (emitted by
2026-05-19 17:20:20 +02:00
// EnableWasiBrowserRuntime) into the read-only baked layer so wasi-libc's
// file syscalls work against them. Browser builds otherwise can't open
// assets shipped alongside the .wasm — sync XHR is too deprecated to rely
// on.
2026-05-19 03:28:27 +02:00
//
// Manifest entries are relative paths under the bin dir (the layout
// `cfg.assets` produces — e.g. "assets/Inter.ttf",
2026-05-26 22:50:08 +02:00
// "mods/3DForts_Base/foo.cmesh"). We fetch each at its full path and
// key the read-only map by that same relative path so directory
// iteration + nested dirs work; path_open's resolveKey normalises
// caller-side variations (leading "./" or "/").
2026-05-18 05:23:11 +02:00
try {
const manifestResp = await fetch("files.json");
if (manifestResp.ok) {
const names = await manifestResp.json();
await Promise.all(names.map(async (name) => {
const r = await fetch(name);
2026-05-19 03:28:27 +02:00
if (!r.ok) {
console.warn(`[wasi] failed to preload ${name}: HTTP ${r.status}`);
return;
}
2026-05-26 22:50:08 +02:00
readonlyVfs.set(name, new Uint8Array(await r.arrayBuffer()));
2026-05-18 05:23:11 +02:00
}));
}
} catch (e) {
console.warn("[wasi] no files.json manifest (or fetch failed); file I/O syscalls will return ENOENT:", e.message);
}
2026-05-26 22:50:08 +02:00
roBuildDirTree();
2026-05-18 05:23:11 +02:00
2026-05-19 17:20:20 +02:00
// Hydrate the persistent layer from OPFS. Must complete before main()
// runs so any std::ifstream on /persistent/* sees the on-disk bytes.
await opfsHydrate();
// Best-effort final flush as the page is torn down. `pagehide` is the
// modern equivalent of `unload`; it fires on tab close + bfcache eviction
// + cross-document navigation. The browser MAY kill the page before the
// async writes complete — the periodic FLUSH_DEBOUNCE_MS timer is the
// real durability guarantee for writes that happen close to shutdown.
window.addEventListener("pagehide", () => { opfsFlushAll(); });
const wasi = new Wasi({ stdin: "", env: {}, args: [] });
2026-05-18 05:23:11 +02:00
// Modules that need env imports (Crafter.Graphics DOM mode, etc.) ship a
// co-located env.js that sets `window.crafter_webbuild_env`. The
// `EnableWasiBrowserRuntime` injects each one as a regular <script> tag
// before this module script, so by the time we instantiate they've all
// populated the global. Pure-WASI builds leave the global undefined and
// we pass an empty object — extra imports on the JS side are harmless
// (WebAssembly only complains about *missing* declared imports).
if (!window.crafter_webbuild_env) {
window.crafter_webbuild_env = {};
}
// Some env.js bridges (notably the Crafter.CppDOM one) expect an indirect
// function table they can push function references onto. Set it up so we
// don't break compatibility with that pattern.
window.crafter_webbuild_env.table = new WebAssembly.Table({ initial: 4, element: "anyfunc" });
// Async env bridges (notably the WebGPU one in Crafter.Graphics) set
// `window.crafter_webbuild_env_ready` to a Promise that resolves once
// their init (adapter/device requests, pipeline compilation, etc.) is
// finished AND env.* has been swapped from sync stubs to real impls.
// We MUST await this BEFORE WebAssembly.instantiateStreaming — the wasm
// snapshots its import functions at instantiate time, so stubs captured
// then are baked in. Sibling <script type="module"> top-level awaits
// aren't reliably serialized cross-browser, so we can't rely on dom-*
// scripts' own TLAs to block us here.
if (window.crafter_webbuild_env_ready) {
try {
await window.crafter_webbuild_env_ready;
} catch (e) {
console.error("[crafter] env bridge init failed:", e);
// Continue to instantiation anyway — the bridge's stubs throw a
// clearer error from their own call sites.
}
}
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), {
wasi_snapshot_preview1: wasi,
2026-05-18 05:23:11 +02:00
env: window.crafter_webbuild_env,
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
});
wasi.instance = instance;
2026-05-18 05:23:11 +02:00
// Alias under both names — `crafter_wasi` is the canonical, but the older
// CppDOM env.js reads from `crafter_webbuild_wasi`. Keep both wired so
// either generation of env.js works against this runtime.
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
window.crafter_wasi = wasi;
2026-05-18 05:23:11 +02:00
window.crafter_webbuild_wasi = wasi;
V2: WASI, -r flag, CI pipeline, examples & tests cleanup WASI / wasm32 target support - Auto-detect /usr/share/wasi-sysroot on Linux when target starts_with("wasm32") - Skip -march/-mtune for wasm (clang rejects them) - Apply -fno-exceptions -fno-c++-static-destructors -mllvm -wasm-enable-sjlj -D_WASI_EMULATED_SIGNAL to wasm builds (compile + std PCM, kept in sync) - .wasm output extension in expectedOutputFor and link command - EnableWasiBrowserRuntime(cfg): opt-in helper that drops index.html + runtime.js next to the .wasm; runtime.js reads window.CRAFTER_WASM_URL set in the templated index.html so a single shim handles any output name -r run flag in the CLI: build then exec the artifact (host targets only; rejects libraries; auto .exe/.wasm extension handling) CI pipeline (.forgejo/workflows/ci.yaml) - Triggers: PR/push to master + manual dispatch - Single arch-latest container job: install deps, bootstrap, self-rebuild, run tests, cross-compile mingw, package both archives, upload artifacts - Rolling 'latest' release published only on push/dispatch to master mingw cross-compile from Linux now works end-to-end: - ExternalDependency cache key includes target so per-target glslang builds don't collide; CMAKE_BUILD_TYPE=Release pinned (otherwise glslang appends 'd' to lib names and breaks linking); cross-compile cmake flags (CMAKE_SYSTEM_NAME=Windows, CMAKE_*_COMPILER_TARGET=...) - project.cpp accepts --target=<triple>; Linux-only -Wl,--export-dynamic and -ldl are gated; mingw glslang skips the standalone exe (its libgcc_eh link pulls pthread which mingw doesn't link by default) - mingw compile uses -femulated-tls so std::__once_callable etc reference the same emutls symbols libstdc++ provides - mingw link auto-adds -lstdc++exp -lpthread GetCrafterBuildHome() exposed from the Platform module; LoadProject (Linux + Windows) now both use it instead of duplicating the resolution. Examples reorg: hello-world, library, with-module, wasi, tests — each with its own README. Tests reorg: per-test directory with inner/ fixture, no shared tests/fixtures/ tree. New Wasi test verifies .wasm magic bytes. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
2026-04-28 23:24:46 +02:00
2026-05-18 05:23:11 +02:00
try {
instance.exports._start();
} catch (e) {
if (e && typeof e.crafterWasiExit === "number") {
// Clean exit from std::_Exit / __wasi_proc_exit. The wasm instance
// remains valid; any rAF callback registered before exit keeps
// running. (See Crafter::Window::StartSync DOM impl.)
} else {
throw e;
}
}