V2: WASI, -r flag, CI pipeline, examples & tests cleanup
Some checks failed
CI / build-test-release (pull_request) Failing after 44s
Some checks failed
CI / build-test-release (pull_request) Failing after 44s
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>
This commit is contained in:
parent
cdfdb976c8
commit
eaee502e8c
102 changed files with 2211 additions and 686 deletions
10
wasi-runtime/index.html.in
Normal file
10
wasi-runtime/index.html.in
Normal file
|
|
@ -0,0 +1,10 @@
|
|||
<!DOCTYPE html>
|
||||
<html>
|
||||
<head>
|
||||
<meta charset="utf-8">
|
||||
<title>{{WASM}}</title>
|
||||
<script>window.CRAFTER_WASM_URL = "{{WASM}}";</script>
|
||||
<script src="runtime.js" type="module"></script>
|
||||
</head>
|
||||
<body style="margin:0;"></body>
|
||||
</html>
|
||||
167
wasi-runtime/runtime.js
Normal file
167
wasi-runtime/runtime.js
Normal file
|
|
@ -0,0 +1,167 @@
|
|||
// 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();
|
||||
|
||||
class Wasi {
|
||||
#encodedStdin;
|
||||
#envEncodedStrings;
|
||||
#argEncodedStrings;
|
||||
instance;
|
||||
|
||||
constructor({ env, stdin, args }) {
|
||||
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"));
|
||||
this.bind();
|
||||
}
|
||||
|
||||
bind() {
|
||||
this.args_get = this.args_get.bind(this);
|
||||
this.args_sizes_get = this.args_sizes_get.bind(this);
|
||||
this.environ_get = this.environ_get.bind(this);
|
||||
this.environ_sizes_get = this.environ_sizes_get.bind(this);
|
||||
this.fd_read = this.fd_read.bind(this);
|
||||
this.fd_write = this.fd_write.bind(this);
|
||||
}
|
||||
|
||||
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) {
|
||||
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;
|
||||
}
|
||||
const dataView = new DataView(this.instance.exports.memory.buffer);
|
||||
dataView.setInt32(bytesWrittenPtr, totalBytesWritten, true);
|
||||
(fd === 2 ? console.error : console.log)(text);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
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);
|
||||
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;
|
||||
}
|
||||
const dataView = new DataView(this.instance.exports.memory.buffer);
|
||||
dataView.setInt32(bytesReadPtr, totalBytesRead, true);
|
||||
}
|
||||
return 0;
|
||||
}
|
||||
fd_advise() { return 0; }
|
||||
fd_close() { return 0; }
|
||||
fd_fdstat_get() { return 0; }
|
||||
fd_prestat_get() { return 0; }
|
||||
fd_prestat_dir_name() { return 0; }
|
||||
clock_res_get() { return 0; }
|
||||
clock_time_get() { return 0; }
|
||||
fd_seek() { return 0; }
|
||||
fd_allocate() { return 0; }
|
||||
fd_datasync() { return 0; }
|
||||
fd_fdstat_set_flags() { return 0; }
|
||||
fd_fdstat_set_rights() { return 0; }
|
||||
fd_filestat_get() { return 0; }
|
||||
fd_filestat_set_size() { return 0; }
|
||||
fd_filestat_set_times() { return 0; }
|
||||
fd_pread() { return 0; }
|
||||
fd_pwrite() { return 0; }
|
||||
fd_readdir() { return 0; }
|
||||
fd_renumber() { return 0; }
|
||||
fd_sync() { return 0; }
|
||||
fd_tell() { return 0; }
|
||||
path_create_directory() { return 0; }
|
||||
path_filestat_get() { return 0; }
|
||||
path_filestat_set_times() { return 0; }
|
||||
path_link() { return 0; }
|
||||
path_open() { return 0; }
|
||||
path_readlink() { return 0; }
|
||||
path_remove_directory() { return 0; }
|
||||
path_rename() { return 0; }
|
||||
path_symlink() { return 0; }
|
||||
path_unlink_file() { return 0; }
|
||||
poll_oneoff() { return 0; }
|
||||
sched_yield() { return 0; }
|
||||
random_get() { return 0; }
|
||||
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) {
|
||||
console.log(`[wasi] proc_exit(${code})`);
|
||||
}
|
||||
}
|
||||
|
||||
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)");
|
||||
}
|
||||
|
||||
const wasi = new Wasi({ stdin: "", env: {}, args: [] });
|
||||
|
||||
const { instance } = await WebAssembly.instantiateStreaming(fetch(wasmUrl), {
|
||||
wasi_snapshot_preview1: wasi,
|
||||
});
|
||||
wasi.instance = instance;
|
||||
window.crafter_wasi = wasi;
|
||||
|
||||
instance.exports._start();
|
||||
Loading…
Add table
Add a link
Reference in a new issue