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

4 files changed

Lines changed: 278 additions & 33 deletions

File tree

‎doc/api/wasi.md‎

Lines changed: 17 additions & 0 deletions
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‎

Lines changed: 59 additions & 31 deletions
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-
constkExitCode=Symbol('exitCode');
24-
constkSetMemory=Symbol('setMemory');
25-
constkStarted=Symbol('started');
23+
constkExitCode=Symbol('kExitCode');
24+
constkSetMemory=Symbol('kSetMemory');
25+
constkStarted=Symbol('kStarted');
26+
constkInstance=Symbol('kInstance');
2627

2728
emitExperimentalWarning('WASI');
2829

2930

31+
functionsetupInstance(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+
thrownewERR_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
classWASI{
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-
constexports=instance.exports;
106+
if(this[kStarted]){
107+
thrownewERR_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
thrownewERR_INVALID_ARG_TYPE(
91117
'instance.exports._start','function',_start);
92118
}
93-
94119
if(_initialize!==undefined){
95120
thrownewERR_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-
thrownewERR_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+
throwerr;
129+
}
112130
}
113131

132+
returnthis[kExitCode];
133+
}
134+
135+
// Must not export _start, may optionally export _initialize
136+
initialize(instance){
114137
if(this[kStarted]){
115138
thrownewERR_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-
throwerr;
126-
}
142+
setupInstance(this,instance);
143+
144+
const{ _start, _initialize }=this[kInstance].exports;
145+
146+
if(typeof_initialize!=='function'&&_initialize!==undefined){
147+
thrownewERR_INVALID_ARG_TYPE(
148+
'instance.exports._initialize','function',_initialize);
149+
}
150+
if(_start!==undefined){
151+
thrownewERR_INVALID_ARG_TYPE(
152+
'instance.exports._start','undefined',_initialize);
127153
}
128154

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

Lines changed: 195 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,195 @@
1+
// Flags: --experimental-wasi-unstable-preview1
2+
'use strict';
3+
4+
constcommon=require('../common');
5+
constassert=require('assert');
6+
constvm=require('vm');
7+
const{WASI}=require('wasi');
8+
9+
constfixtures=require('../common/fixtures');
10+
constbufferSource=fixtures.readSync('simple.wasm');
11+
12+
(async()=>{
13+
{
14+
// Verify that a WebAssembly.Instance is passed in.
15+
constwasi=newWASI();
16+
17+
assert.throws(
18+
()=>{wasi.initialize();},
19+
{
20+
code: 'ERR_INVALID_ARG_TYPE',
21+
message: /"instance"argumentmustbeoftypeobject/
22+
}
23+
);
24+
}
25+
26+
{
27+
// Verify that the passed instance has an exports objects.
28+
constwasi=newWASI({});
29+
constwasm=awaitWebAssembly.compile(bufferSource);
30+
constinstance=awaitWebAssembly.instantiate(wasm);
31+
32+
Object.defineProperty(instance,'exports',{get(){returnnull;}});
33+
assert.throws(
34+
()=>{wasi.initialize(instance);},
35+
{
36+
code: 'ERR_INVALID_ARG_TYPE',
37+
message: /"instance\.exports"propertymustbeoftypeobject/
38+
}
39+
);
40+
}
41+
42+
{
43+
// Verify that a _initialize() export was passed.
44+
constwasi=newWASI({});
45+
constwasm=awaitWebAssembly.compile(bufferSource);
46+
constinstance=awaitWebAssembly.instantiate(wasm);
47+
48+
Object.defineProperty(instance,'exports',{
49+
get(){
50+
return{_initialize: 5,memory: newUint8Array()};
51+
},
52+
});
53+
assert.throws(
54+
()=>{wasi.initialize(instance);},
55+
{
56+
code: 'ERR_INVALID_ARG_TYPE',
57+
message: /"instance\.exports\._initialize"propertymustbeoftypefunction/
58+
}
59+
);
60+
}
61+
62+
{
63+
// Verify that a _start export was not passed.
64+
constwasi=newWASI({});
65+
constwasm=awaitWebAssembly.compile(bufferSource);
66+
constinstance=awaitWebAssembly.instantiate(wasm);
67+
68+
Object.defineProperty(instance,'exports',{
69+
get(){
70+
return{
71+
_start(){},
72+
_initialize(){},
73+
memory: newUint8Array(),
74+
};
75+
}
76+
});
77+
assert.throws(
78+
()=>{wasi.initialize(instance);},
79+
{
80+
code: 'ERR_INVALID_ARG_TYPE',
81+
message: /"instance\.exports\._start"propertymustbeundefined/
82+
}
83+
);
84+
}
85+
86+
{
87+
// Verify that a memory export was passed.
88+
constwasi=newWASI({});
89+
constwasm=awaitWebAssembly.compile(bufferSource);
90+
constinstance=awaitWebAssembly.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"propertymustbeoftypeobject/
100+
}
101+
);
102+
}
103+
104+
{
105+
// Verify that a non-ArrayBuffer memory.buffer is rejected.
106+
constwasi=newWASI({});
107+
constwasm=awaitWebAssembly.compile(bufferSource);
108+
constinstance=awaitWebAssembly.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"propertymustbeanWebAssembly\.Memory/
126+
}
127+
);
128+
}
129+
130+
{
131+
// Verify that an argument that duck-types as a WebAssembly.Instance
132+
// is accepted.
133+
constwasi=newWASI({});
134+
constwasm=awaitWebAssembly.compile(bufferSource);
135+
constinstance=awaitWebAssembly.instantiate(wasm);
136+
137+
Object.defineProperty(instance,'exports',{
138+
get(){
139+
return{
140+
_initialize(){},
141+
memory: {buffer: newArrayBuffer(0)},
142+
};
143+
}
144+
});
145+
wasi.initialize(instance);
146+
}
147+
148+
{
149+
// Verify that a WebAssembly.Instance from another VM context is accepted.
150+
constwasi=newWASI({});
151+
constinstance=awaitvm.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+
constwasi=newWASI({});
175+
constwasm=awaitWebAssembly.compile(bufferSource);
176+
constinstance=awaitWebAssembly.instantiate(wasm);
177+
178+
Object.defineProperty(instance,'exports',{
179+
get(){
180+
return{
181+
_initialize(){},
182+
memory: newWebAssembly.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: /^WASIinstancehasalreadystarted$/
192+
}
193+
);
194+
}
195+
})().then(common.mustCall());

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

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -45,7 +45,11 @@ const bufferSource = fixtures.readSync('simple.wasm');
4545
constwasm=awaitWebAssembly.compile(bufferSource);
4646
constinstance=awaitWebAssembly.instantiate(wasm);
4747

48-
Object.defineProperty(instance,'exports',{get(){return{};}});
48+
Object.defineProperty(instance,'exports',{
49+
get(){
50+
return{memory: newUint8Array()};
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: newUint8Array(),
6974
};
7075
}
7176
});

0 commit comments

Comments
 (0)