Skip to content

Commit 6be685a

Browse files
devsnekMylesBorins
authored andcommitted
wasi: add reactor support
PR-URL: #34046 Reviewed-By: Guy Bedford <guybedford@gmail.com> Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
1 parent 105d560 commit 6be685a

File tree

4 files changed

+278
-33
lines changed

4 files changed

+278
-33
lines changed

doc/api/wasi.md

+17
Original file line numberDiff line numberDiff line change
@@ -132,6 +132,23 @@ Attempt to begin execution of `instance` as a WASI command by invoking its
132132

133133
If `start()` is called more than once, an exception is thrown.
134134

135+
### `wasi.initialize(instance)`
136+
<!-- YAML
137+
added:
138+
- REPLACEME
139+
-->
140+
141+
* `instance` {WebAssembly.Instance}
142+
143+
Attempt to initialize `instance` as a WASI reactor by invoking its
144+
`_initialize()` export, if it is present. If `instance` contains a `_start()`
145+
export, then an exception is thrown.
146+
147+
`initialize()` requires that `instance` exports a [`WebAssembly.Memory`][] named
148+
`memory`. If `instance` does not have a `memory` export an exception is thrown.
149+
150+
If `initialize()` is called more than once, an exception is thrown.
151+
135152
### `wasi.wasiImport`
136153
<!-- YAML
137154
added:

lib/wasi.js

+59-31
Original file line numberDiff line numberDiff line change
@@ -20,13 +20,36 @@ const {
2020
validateObject,
2121
} = require('internal/validators');
2222
const { WASI: _WASI } = internalBinding('wasi');
23-
const kExitCode = Symbol('exitCode');
24-
const kSetMemory = Symbol('setMemory');
25-
const kStarted = Symbol('started');
23+
const kExitCode = Symbol('kExitCode');
24+
const kSetMemory = Symbol('kSetMemory');
25+
const kStarted = Symbol('kStarted');
26+
const kInstance = Symbol('kInstance');
2627

2728
emitExperimentalWarning('WASI');
2829

2930

31+
function setupInstance(self, instance) {
32+
validateObject(instance, 'instance');
33+
validateObject(instance.exports, 'instance.exports');
34+
35+
// WASI::_SetMemory() in src/node_wasi.cc only expects that |memory| is
36+
// an object. It will try to look up the .buffer property when needed
37+
// and fail with UVWASI_EINVAL when the property is missing or is not
38+
// an ArrayBuffer. Long story short, we don't need much validation here
39+
// but we type-check anyway because it helps catch bugs in the user's
40+
// code early.
41+
validateObject(instance.exports.memory, 'instance.exports.memory');
42+
if (!isArrayBuffer(instance.exports.memory.buffer)) {
43+
throw new ERR_INVALID_ARG_TYPE(
44+
'instance.exports.memory.buffer',
45+
['WebAssembly.Memory'],
46+
instance.exports.memory.buffer);
47+
}
48+
49+
self[kInstance] = instance;
50+
self[kSetMemory](instance.exports.memory);
51+
}
52+
3053
class WASI {
3154
constructor(options = {}) {
3255
validateObject(options, 'options');
@@ -75,58 +98,63 @@ class WASI {
7598
this.wasiImport = wrap;
7699
this[kStarted] = false;
77100
this[kExitCode] = 0;
101+
this[kInstance] = undefined;
78102
}
79103

104+
// Must not export _initialize, must export _start
80105
start(instance) {
81-
validateObject(instance, 'instance');
82-
83-
const exports = instance.exports;
106+
if (this[kStarted]) {
107+
throw new ERR_WASI_ALREADY_STARTED();
108+
}
109+
this[kStarted] = true;
84110

85-
validateObject(exports, 'instance.exports');
111+
setupInstance(this, instance);
86112

87-
const { _initialize, _start, memory } = exports;
113+
const { _start, _initialize } = this[kInstance].exports;
88114

89115
if (typeof _start !== 'function') {
90116
throw new ERR_INVALID_ARG_TYPE(
91117
'instance.exports._start', 'function', _start);
92118
}
93-
94119
if (_initialize !== undefined) {
95120
throw new ERR_INVALID_ARG_TYPE(
96121
'instance.exports._initialize', 'undefined', _initialize);
97122
}
98123

99-
// WASI::_SetMemory() in src/node_wasi.cc only expects that |memory| is
100-
// an object. It will try to look up the .buffer property when needed
101-
// and fail with UVWASI_EINVAL when the property is missing or is not
102-
// an ArrayBuffer. Long story short, we don't need much validation here
103-
// but we type-check anyway because it helps catch bugs in the user's
104-
// code early.
105-
validateObject(memory, 'instance.exports.memory');
106-
107-
if (!isArrayBuffer(memory.buffer)) {
108-
throw new ERR_INVALID_ARG_TYPE(
109-
'instance.exports.memory.buffer',
110-
['WebAssembly.Memory'],
111-
memory.buffer);
124+
try {
125+
_start();
126+
} catch (err) {
127+
if (err !== kExitCode) {
128+
throw err;
129+
}
112130
}
113131

132+
return this[kExitCode];
133+
}
134+
135+
// Must not export _start, may optionally export _initialize
136+
initialize(instance) {
114137
if (this[kStarted]) {
115138
throw new ERR_WASI_ALREADY_STARTED();
116139
}
117-
118140
this[kStarted] = true;
119-
this[kSetMemory](memory);
120141

121-
try {
122-
exports._start();
123-
} catch (err) {
124-
if (err !== kExitCode) {
125-
throw err;
126-
}
142+
setupInstance(this, instance);
143+
144+
const { _start, _initialize } = this[kInstance].exports;
145+
146+
if (typeof _initialize !== 'function' && _initialize !== undefined) {
147+
throw new ERR_INVALID_ARG_TYPE(
148+
'instance.exports._initialize', 'function', _initialize);
149+
}
150+
if (_start !== undefined) {
151+
throw new ERR_INVALID_ARG_TYPE(
152+
'instance.exports._start', 'undefined', _initialize);
127153
}
128154

129-
return this[kExitCode];
155+
if (_initialize !== undefined) {
156+
_initialize();
157+
}
130158
}
131159
}
132160

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Flags: --experimental-wasi-unstable-preview1
2+
'use strict';
3+
4+
const common = require('../common');
5+
const assert = require('assert');
6+
const vm = require('vm');
7+
const { WASI } = require('wasi');
8+
9+
const fixtures = require('../common/fixtures');
10+
const bufferSource = fixtures.readSync('simple.wasm');
11+
12+
(async () => {
13+
{
14+
// Verify that a WebAssembly.Instance is passed in.
15+
const wasi = new WASI();
16+
17+
assert.throws(
18+
() => { wasi.initialize(); },
19+
{
20+
code: 'ERR_INVALID_ARG_TYPE',
21+
message: /"instance" argument must be of type object/
22+
}
23+
);
24+
}
25+
26+
{
27+
// Verify that the passed instance has an exports objects.
28+
const wasi = new WASI({});
29+
const wasm = await WebAssembly.compile(bufferSource);
30+
const instance = await WebAssembly.instantiate(wasm);
31+
32+
Object.defineProperty(instance, 'exports', { get() { return null; } });
33+
assert.throws(
34+
() => { wasi.initialize(instance); },
35+
{
36+
code: 'ERR_INVALID_ARG_TYPE',
37+
message: /"instance\.exports" property must be of type object/
38+
}
39+
);
40+
}
41+
42+
{
43+
// Verify that a _initialize() export was passed.
44+
const wasi = new WASI({});
45+
const wasm = await WebAssembly.compile(bufferSource);
46+
const instance = await WebAssembly.instantiate(wasm);
47+
48+
Object.defineProperty(instance, 'exports', {
49+
get() {
50+
return { _initialize: 5, memory: new Uint8Array() };
51+
},
52+
});
53+
assert.throws(
54+
() => { wasi.initialize(instance); },
55+
{
56+
code: 'ERR_INVALID_ARG_TYPE',
57+
message: /"instance\.exports\._initialize" property must be of type function/
58+
}
59+
);
60+
}
61+
62+
{
63+
// Verify that a _start export was not passed.
64+
const wasi = new WASI({});
65+
const wasm = await WebAssembly.compile(bufferSource);
66+
const instance = await WebAssembly.instantiate(wasm);
67+
68+
Object.defineProperty(instance, 'exports', {
69+
get() {
70+
return {
71+
_start() {},
72+
_initialize() {},
73+
memory: new Uint8Array(),
74+
};
75+
}
76+
});
77+
assert.throws(
78+
() => { wasi.initialize(instance); },
79+
{
80+
code: 'ERR_INVALID_ARG_TYPE',
81+
message: /"instance\.exports\._start" property must be undefined/
82+
}
83+
);
84+
}
85+
86+
{
87+
// Verify that a memory export was passed.
88+
const wasi = new WASI({});
89+
const wasm = await WebAssembly.compile(bufferSource);
90+
const instance = await WebAssembly.instantiate(wasm);
91+
92+
Object.defineProperty(instance, 'exports', {
93+
get() { return { _initialize() {} }; }
94+
});
95+
assert.throws(
96+
() => { wasi.initialize(instance); },
97+
{
98+
code: 'ERR_INVALID_ARG_TYPE',
99+
message: /"instance\.exports\.memory" property must be of type object/
100+
}
101+
);
102+
}
103+
104+
{
105+
// Verify that a non-ArrayBuffer memory.buffer is rejected.
106+
const wasi = new WASI({});
107+
const wasm = await WebAssembly.compile(bufferSource);
108+
const instance = await WebAssembly.instantiate(wasm);
109+
110+
Object.defineProperty(instance, 'exports', {
111+
get() {
112+
return {
113+
_initialize() {},
114+
memory: {},
115+
};
116+
}
117+
});
118+
// The error message is a little white lie because any object
119+
// with a .buffer property of type ArrayBuffer is accepted,
120+
// but 99% of the time a WebAssembly.Memory object is used.
121+
assert.throws(
122+
() => { wasi.initialize(instance); },
123+
{
124+
code: 'ERR_INVALID_ARG_TYPE',
125+
message: /"instance\.exports\.memory\.buffer" property must be an WebAssembly\.Memory/
126+
}
127+
);
128+
}
129+
130+
{
131+
// Verify that an argument that duck-types as a WebAssembly.Instance
132+
// is accepted.
133+
const wasi = new WASI({});
134+
const wasm = await WebAssembly.compile(bufferSource);
135+
const instance = await WebAssembly.instantiate(wasm);
136+
137+
Object.defineProperty(instance, 'exports', {
138+
get() {
139+
return {
140+
_initialize() {},
141+
memory: { buffer: new ArrayBuffer(0) },
142+
};
143+
}
144+
});
145+
wasi.initialize(instance);
146+
}
147+
148+
{
149+
// Verify that a WebAssembly.Instance from another VM context is accepted.
150+
const wasi = new WASI({});
151+
const instance = await vm.runInNewContext(`
152+
(async () => {
153+
const wasm = await WebAssembly.compile(bufferSource);
154+
const instance = await WebAssembly.instantiate(wasm);
155+
156+
Object.defineProperty(instance, 'exports', {
157+
get() {
158+
return {
159+
_initialize() {},
160+
memory: new WebAssembly.Memory({ initial: 1 })
161+
};
162+
}
163+
});
164+
165+
return instance;
166+
})()
167+
`, { bufferSource });
168+
169+
wasi.initialize(instance);
170+
}
171+
172+
{
173+
// Verify that initialize() can only be called once.
174+
const wasi = new WASI({});
175+
const wasm = await WebAssembly.compile(bufferSource);
176+
const instance = await WebAssembly.instantiate(wasm);
177+
178+
Object.defineProperty(instance, 'exports', {
179+
get() {
180+
return {
181+
_initialize() {},
182+
memory: new WebAssembly.Memory({ initial: 1 })
183+
};
184+
}
185+
});
186+
wasi.initialize(instance);
187+
assert.throws(
188+
() => { wasi.initialize(instance); },
189+
{
190+
code: 'ERR_WASI_ALREADY_STARTED',
191+
message: /^WASI instance has already started$/
192+
}
193+
);
194+
}
195+
})().then(common.mustCall());

test/wasi/test-wasi-start-validation.js

+7-2
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ const bufferSource = fixtures.readSync('simple.wasm');
4545
const wasm = await WebAssembly.compile(bufferSource);
4646
const instance = await WebAssembly.instantiate(wasm);
4747

48-
Object.defineProperty(instance, 'exports', { get() { return {}; } });
48+
Object.defineProperty(instance, 'exports', {
49+
get() {
50+
return { memory: new Uint8Array() };
51+
},
52+
});
4953
assert.throws(
5054
() => { wasi.start(instance); },
5155
{
@@ -65,7 +69,8 @@ const bufferSource = fixtures.readSync('simple.wasm');
6569
get() {
6670
return {
6771
_start() {},
68-
_initialize() {}
72+
_initialize() {},
73+
memory: new Uint8Array(),
6974
};
7075
}
7176
});

0 commit comments

Comments
 (0)