/ concept-collection / stan-web-ide
concept-collection / stan-web-ide
stan-web-ide / src / stan / wasi.ts
129 lines · 4.5 KBBlameHistoryRaw
1// Minimal WASI preview1 host for the compiled Stan models: the compile
2// server (stan-wasm-wasi) produces pure command modules — exported _start
3// and memory, importing only wasi_snapshot_preview1 — that read argv, write
4// stdio, and read the clock. No filesystem, no environment, no threads.
5//
6// The full import surface of a server-compiled model (verified):
7// args_get, args_sizes_get, environ_get, environ_sizes_get,
8// clock_time_get, fd_write, fd_read, fd_close, fd_seek, proc_exit
9// Anything else the toolchain might add in the future is stubbed to ENOSYS
10// so it fails with a readable error instead of a link error.
12export interface WasiRunOptions {
13 module: WebAssembly.Module;
14 /** argv, excluding argv[0] (the module name). */
15 args: string[];
16 /** Raw bytes written to stdout (fd 1). */
17 onStdout: (bytes: Uint8Array) => void;
18 /** Raw bytes written to stderr (fd 2). */
19 onStderr: (bytes: Uint8Array) => void;
22const ERRNO_SUCCESS = 0;
23const ERRNO_BADF = 8;
24const ERRNO_NOSYS = 52;
25const ERRNO_SPIPE = 70;
27/** Thrown by proc_exit to unwind out of _start. */
28class ProcExit {
29 constructor(readonly code: number) {}
32/** Instantiates the command module and runs it to completion (this blocks
33 * the calling thread — run it in a worker). Resolves with the exit code. */
34export async function runWasiModule({ module, args, onStdout, onStderr }: WasiRunOptions): Promise<number> {
35 let memory: WebAssembly.Memory;
36 const view = () => new DataView(memory.buffer);
37 const mem = () => new Uint8Array(memory.buffer);
39 const encoder = new TextEncoder();
40 const argv = ['main.wasm', ...args].map((arg) => encoder.encode(arg + '\0'));
42 const wasi: Record<string, (...args: never[]) => unknown> = {
43 args_sizes_get(argcPtr: number, bufSizePtr: number): number {
44 view().setUint32(argcPtr, argv.length, true);
45 view().setUint32(bufSizePtr, argv.reduce((size, arg) => size + arg.length, 0), true);
46 return ERRNO_SUCCESS;
47 },
48 args_get(argvPtr: number, bufPtr: number): number {
49 for (const arg of argv) {
50 view().setUint32(argvPtr, bufPtr, true);
51 mem().set(arg, bufPtr);
52 argvPtr += 4;
53 bufPtr += arg.length;
54 }
55 return ERRNO_SUCCESS;
56 },
57 environ_sizes_get(countPtr: number, bufSizePtr: number): number {
58 view().setUint32(countPtr, 0, true);
59 view().setUint32(bufSizePtr, 0, true);
60 return ERRNO_SUCCESS;
61 },
62 environ_get(): number {
63 return ERRNO_SUCCESS;
64 },
65 clock_time_get(id: number, _precision: bigint, timePtr: number): number {
66 // 0 = realtime, 1 = monotonic; nanoseconds as u64
67 const nanos = id === 0
68 ? BigInt(Date.now()) * 1_000_000n
69 : BigInt(Math.round(performance.now() * 1e6));
70 view().setBigUint64(timePtr, nanos, true);
71 return ERRNO_SUCCESS;
72 },
73 fd_write(fd: number, iovsPtr: number, iovsLen: number, nwrittenPtr: number): number {
74 if (fd !== 1 && fd !== 2) {
75 return ERRNO_BADF;
76 }
77 let written = 0;
78 for (let i = 0; i < iovsLen; i++) {
79 const ptr = view().getUint32(iovsPtr + i * 8, true);
80 const len = view().getUint32(iovsPtr + i * 8 + 4, true);
81 if (len > 0) {
82 // slice (not subarray): the receiver keeps the bytes
83 (fd === 1 ? onStdout : onStderr)(mem().slice(ptr, ptr + len));
84 written += len;
85 }
86 }
87 view().setUint32(nwrittenPtr, written, true);
88 return ERRNO_SUCCESS;
89 },
90 fd_read(_fd: number, _iovsPtr: number, _iovsLen: number, nreadPtr: number): number {
91 view().setUint32(nreadPtr, 0, true); // EOF
92 return ERRNO_SUCCESS;
93 },
94 fd_close(_fd: number): number {
95 return ERRNO_SUCCESS;
96 },
97 fd_seek(_fd: number, _offset: bigint, _whence: number, _newOffsetPtr: number): number {
98 return ERRNO_SPIPE; // stdio is not seekable
99 },
100 proc_exit(code: number): never {
101 throw new ProcExit(code);
102 },
103 };
105 for (const imported of WebAssembly.Module.imports(module)) {
106 if (imported.module === 'wasi_snapshot_preview1' && !(imported.name in wasi)) {
107 const name = imported.name;
108 wasi[name] = () => {
109 onStderr(encoder.encode(`wasi: unimplemented syscall ${name}\n`));
110 return ERRNO_NOSYS;
111 };
112 }
113 }
115 // modules built with -sALLOW_MEMORY_GROWTH import this one benign
116 // notification hook; everything else stays pure WASI preview1
117 const env = { emscripten_notify_memory_growth: (_index: number) => {} };
118 const instance = await WebAssembly.instantiate(module, { wasi_snapshot_preview1: wasi, env });
119 memory = instance.exports.memory as WebAssembly.Memory;
120 try {
121 (instance.exports._start as () => void)();
122 return 0;
123 } catch (error) {
124 if (error instanceof ProcExit) {
125 return error.code;
126 }
127 throw error;
128 }