Skip to content

Commit 839b25e

Browse files
toyobayashiRafaelGSS
authored andcommitted
lib: expose setupInstance method on WASI class
PR-URL: #57214 Reviewed-By: Guy Bedford <[email protected]>
1 parent ed966a0 commit 839b25e

File tree

8 files changed

+255
-24
lines changed

8 files changed

+255
-24
lines changed

doc/api/wasi.md

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -243,6 +243,28 @@ export, then an exception is thrown.
243243
244244
If `initialize()` is called more than once, an exception is thrown.
245245
246+
### `wasi.finalizeBindings(instance[, options])`
247+
248+
<!-- YAML
249+
added: REPLACEME
250+
-->
251+
252+
* `instance` {WebAssembly.Instance}
253+
* `options` {Object}
254+
* `memory` {WebAssembly.Memory} **Default:** `instance.exports.memory`.
255+
256+
Set up WASI host bindings to `instance` without calling `initialize()`
257+
or `start()`. This method is useful when the WASI module is instantiated in
258+
child threads for sharing the memory across threads.
259+
260+
`finalizeBindings()` requires that either `instance` exports a
261+
[`WebAssembly.Memory`][] named `memory` or user specify a
262+
[`WebAssembly.Memory`][] object in `options.memory`. If the `memory` is invalid
263+
an exception is thrown.
264+
265+
`start()` and `initialize()` will call `finalizeBindings()` internally.
266+
If `finalizeBindings()` is called more than once, an exception is thrown.
267+
246268
### `wasi.wasiImport`
247269
248270
<!-- YAML

lib/wasi.js

Lines changed: 15 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -34,15 +34,6 @@ const kBindingName = Symbol('kBindingName');
3434

3535
emitExperimentalWarning('WASI');
3636

37-
38-
function setupInstance(self, instance) {
39-
validateObject(instance, 'instance');
40-
validateObject(instance.exports, 'instance.exports');
41-
42-
self[kInstance] = instance;
43-
self[kSetMemory](instance.exports.memory);
44-
}
45-
4637
class WASI {
4738
constructor(options = kEmptyObject) {
4839
validateObject(options, 'options');
@@ -118,14 +109,25 @@ class WASI {
118109
this[kInstance] = undefined;
119110
}
120111

121-
// Must not export _initialize, must export _start
122-
start(instance) {
112+
finalizeBindings(instance, {
113+
memory = instance?.exports?.memory,
114+
} = {}) {
123115
if (this[kStarted]) {
124116
throw new ERR_WASI_ALREADY_STARTED();
125117
}
118+
119+
validateObject(instance, 'instance');
120+
validateObject(instance.exports, 'instance.exports');
121+
122+
this[kSetMemory](memory);
123+
124+
this[kInstance] = instance;
126125
this[kStarted] = true;
126+
}
127127

128-
setupInstance(this, instance);
128+
// Must not export _initialize, must export _start
129+
start(instance) {
130+
this.finalizeBindings(instance);
129131

130132
const { _start, _initialize } = this[kInstance].exports;
131133

@@ -145,12 +147,7 @@ class WASI {
145147

146148
// Must not export _start, may optionally export _initialize
147149
initialize(instance) {
148-
if (this[kStarted]) {
149-
throw new ERR_WASI_ALREADY_STARTED();
150-
}
151-
this[kStarted] = true;
152-
153-
setupInstance(this, instance);
150+
this.finalizeBindings(instance);
154151

155152
const { _start, _initialize } = this[kInstance].exports;
156153

test/fixtures/wasi-preview-1.js

Lines changed: 188 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,21 @@ const fixtures = require('../common/fixtures');
55
const tmpdir = require('../common/tmpdir');
66
const fs = require('fs');
77
const path = require('path');
8+
const { parseArgs } = require('util');
89
const common = require('../common');
910
const { WASI } = require('wasi');
11+
const { Worker, isMainThread, parentPort, workerData } = require('worker_threads');
12+
13+
const args = parseArgs({
14+
allowPositionals: true,
15+
options: {
16+
target: {
17+
type: 'string',
18+
default: 'wasm32-wasip1',
19+
},
20+
},
21+
strict: false,
22+
});
1023

1124
function returnOnExitEnvToValue(env) {
1225
const envValue = env.RETURN_ON_EXIT;
@@ -36,13 +49,182 @@ const wasiPreview1 = new WASI({
3649
// Validate the getImportObject helper
3750
assert.strictEqual(wasiPreview1.wasiImport,
3851
wasiPreview1.getImportObject().wasi_snapshot_preview1);
39-
const modulePathPreview1 = path.join(wasmDir, `${process.argv[2]}.wasm`);
40-
const bufferPreview1 = fs.readFileSync(modulePathPreview1);
4152

4253
(async () => {
43-
const { instance: instancePreview1 } =
44-
await WebAssembly.instantiate(bufferPreview1,
45-
wasiPreview1.getImportObject());
54+
const importObject = { ...wasiPreview1.getImportObject() };
55+
if (args.values.target === 'wasm32-wasip1-threads') {
56+
let nextTid = 43;
57+
const workers = [];
58+
const terminateAllThreads = () => {
59+
workers.forEach((w) => w.terminate());
60+
};
61+
const proc_exit = importObject.wasi_snapshot_preview1.proc_exit;
62+
importObject.wasi_snapshot_preview1.proc_exit = function(code) {
63+
terminateAllThreads();
64+
return proc_exit.call(this, code);
65+
};
66+
const spawn = (startArg, threadId) => {
67+
const tid = nextTid++;
68+
const name = `pthread-${tid}`;
69+
const sab = new SharedArrayBuffer(8 + 8192);
70+
const result = new Int32Array(sab);
71+
72+
const workerData = {
73+
name,
74+
startArg,
75+
tid,
76+
wasmModule,
77+
memory: importObject.env.memory,
78+
result,
79+
};
80+
81+
const worker = new Worker(__filename, {
82+
name,
83+
argv: process.argv.slice(2),
84+
execArgv: [
85+
'--experimental-wasi-unstable-preview1',
86+
],
87+
workerData,
88+
});
89+
workers[tid] = worker;
90+
91+
worker.on('message', ({ cmd, startArg, threadId, tid }) => {
92+
if (cmd === 'loaded') {
93+
worker.unref();
94+
} else if (cmd === 'thread-spawn') {
95+
spawn(startArg, threadId);
96+
} else if (cmd === 'cleanup-thread') {
97+
workers[tid].terminate();
98+
delete workers[tid];
99+
} else if (cmd === 'terminate-all-threads') {
100+
terminateAllThreads();
101+
}
102+
});
46103

47-
wasiPreview1.start(instancePreview1);
104+
worker.on('error', (e) => {
105+
terminateAllThreads();
106+
throw new Error(e);
107+
});
108+
109+
const r = Atomics.wait(result, 0, 0, 1000);
110+
if (r === 'timed-out') {
111+
workers[tid].terminate();
112+
delete workers[tid];
113+
if (threadId) {
114+
Atomics.store(threadId, 0, -6);
115+
Atomics.notify(threadId, 0);
116+
}
117+
return -6;
118+
}
119+
if (Atomics.load(result, 0) !== 0) {
120+
const decoder = new TextDecoder();
121+
const nameLength = Atomics.load(result, 1);
122+
const messageLength = Atomics.load(result, 2);
123+
const stackLength = Atomics.load(result, 3);
124+
const name = decoder.decode(sab.slice(16, 16 + nameLength));
125+
const message = decoder.decode(sab.slice(16 + nameLength, 16 + nameLength + messageLength));
126+
const stack = decoder.decode(
127+
sab.slice(16 + nameLength + messageLength,
128+
16 + nameLength + messageLength + stackLength));
129+
const ErrorConstructor = globalThis[name] ?? (
130+
name === 'RuntimeError' ? (WebAssembly.RuntimeError ?? Error) : Error);
131+
const error = new ErrorConstructor(message);
132+
Object.defineProperty(error, 'stack', {
133+
value: stack,
134+
writable: true,
135+
enumerable: false,
136+
configurable: true,
137+
});
138+
Object.defineProperty(error, 'name', {
139+
value: name,
140+
writable: true,
141+
enumerable: false,
142+
configurable: true,
143+
});
144+
throw error;
145+
}
146+
if (threadId) {
147+
Atomics.store(threadId, 0, tid);
148+
Atomics.notify(threadId, 0);
149+
}
150+
return tid;
151+
};
152+
const memory = isMainThread ? new WebAssembly.Memory({
153+
initial: 16777216 / 65536,
154+
maximum: 2147483648 / 65536,
155+
shared: true,
156+
}) : workerData.memory;
157+
importObject.env ??= {};
158+
importObject.env.memory = memory;
159+
importObject.wasi = {
160+
'thread-spawn': (startArg) => {
161+
if (isMainThread) {
162+
return spawn(startArg);
163+
}
164+
const threadIdBuffer = new SharedArrayBuffer(4);
165+
const id = new Int32Array(threadIdBuffer);
166+
Atomics.store(id, 0, -1);
167+
parentPort.postMessage({
168+
cmd: 'thread-spawn',
169+
startArg,
170+
threadId: id,
171+
});
172+
Atomics.wait(id, 0, -1);
173+
const tid = Atomics.load(id, 0);
174+
return tid;
175+
},
176+
};
177+
}
178+
let wasmModule;
179+
let instancePreview1;
180+
try {
181+
if (isMainThread) {
182+
const modulePathPreview1 = path.join(wasmDir, `${args.positionals[0]}.wasm`);
183+
const bufferPreview1 = fs.readFileSync(modulePathPreview1);
184+
wasmModule = await WebAssembly.compile(bufferPreview1);
185+
} else {
186+
wasmModule = workerData.wasmModule;
187+
}
188+
instancePreview1 = await WebAssembly.instantiate(wasmModule, importObject);
189+
190+
if (isMainThread) {
191+
wasiPreview1.start(instancePreview1);
192+
} else {
193+
wasiPreview1.finalizeBindings(instancePreview1, {
194+
memory: importObject.env.memory,
195+
});
196+
parentPort.postMessage({ cmd: 'loaded' });
197+
Atomics.store(workerData.result, 0, 0);
198+
Atomics.notify(workerData.result, 0);
199+
}
200+
} catch (e) {
201+
if (!isMainThread) {
202+
const encoder = new TextEncoder();
203+
const nameBuffer = encoder.encode(e.name);
204+
const messageBuffer = encoder.encode(e.message);
205+
const stackBuffer = encoder.encode(e.stack);
206+
Atomics.store(workerData.result, 0, 1);
207+
Atomics.store(workerData.result, 1, nameBuffer.length);
208+
Atomics.store(workerData.result, 2, messageBuffer.length);
209+
Atomics.store(workerData.result, 3, stackBuffer.length);
210+
const u8arr = new Uint8Array(workerData.result.buffer);
211+
u8arr.set(nameBuffer, 16);
212+
u8arr.set(messageBuffer, 16 + nameBuffer.length);
213+
u8arr.set(stackBuffer, 16 + nameBuffer.length + messageBuffer.length);
214+
Atomics.notify(workerData.result, 0);
215+
}
216+
throw e;
217+
}
218+
if (!isMainThread) {
219+
try {
220+
instancePreview1.exports.wasi_thread_start(workerData.tid, workerData.startArg);
221+
} catch (err) {
222+
if (err instanceof WebAssembly.RuntimeError) {
223+
parentPort.postMessage({ cmd: 'terminate-all-threads' });
224+
}
225+
throw err
226+
}
227+
228+
parentPort.postMessage({ cmd: 'cleanup-thread', tid: workerData.tid });
229+
}
48230
})().then(common.mustCall());

test/wasi/Makefile

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,9 @@ CFLAGS = -D_WASI_EMULATED_PROCESS_CLOCKS -lwasi-emulated-process-clocks
66
OBJ = $(patsubst c/%.c, wasm/%.wasm, $(wildcard c/*.c))
77
all: $(OBJ)
88

9+
wasm/pthread.wasm : c/pthread.c
10+
$(CC) $< $(CFLAGS) --target=wasm32-wasi-threads -pthread -matomics -mbulk-memory -Wl,--import-memory,--export-memory,--shared-memory,--max-memory=2147483648 --sysroot=$(SYSROOT) -s -o $@
11+
912
wasm/%.wasm : c/%.c
1013
$(CC) $< $(CFLAGS) --target=$(TARGET) --sysroot=$(SYSROOT) -s -o $@
1114

test/wasi/c/pthread.c

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
#include <assert.h>
2+
#include <pthread.h>
3+
#include <unistd.h>
4+
5+
void* worker(void* data) {
6+
int* result = (int*) data;
7+
sleep(1);
8+
*result = 42;
9+
return NULL;
10+
}
11+
12+
int main() {
13+
pthread_t thread = NULL;
14+
int result = 0;
15+
16+
int r = pthread_create(&thread, NULL, worker, &result);
17+
assert(r == 0);
18+
19+
r = pthread_join(thread, NULL);
20+
assert(r == 0);
21+
22+
assert(result == 42);
23+
return 0;
24+
}

test/wasi/test-wasi.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,3 +26,4 @@ testWasiPreview1(['preopen_populates']);
2626
testWasiPreview1(['stat']);
2727
testWasiPreview1(['sock']);
2828
testWasiPreview1(['write_file']);
29+
testWasiPreview1(['--target=wasm32-wasip1-threads', 'pthread']);

test/wasi/wasm/pthread.wasm

32.7 KB
Binary file not shown.

tools/doc/type-parser.mjs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,8 @@ const customTypesMap = {
4646
'bigint': `${jsDocPrefix}Reference/Global_Objects/BigInt`,
4747
'WebAssembly.Instance':
4848
`${jsDocPrefix}Reference/Global_Objects/WebAssembly/Instance`,
49+
'WebAssembly.Memory':
50+
`${jsDocPrefix}Reference/Global_Objects/WebAssembly/Memory`,
4951

5052
'Blob': 'buffer.html#class-blob',
5153
'File': 'buffer.html#class-file',

0 commit comments

Comments
 (0)