wasi: add reactor support

PR-URL: https://github.com/nodejs/node/pull/34046
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
This commit is contained in:
Gus Caplan 2020-06-24 19:45:03 -05:00
parent da8a1ef21b
commit 9e135accbb
No known key found for this signature in database
GPG Key ID: F00BD11880E82F0E
4 changed files with 279 additions and 34 deletions

View File

@ -132,6 +132,23 @@ Attempt to begin execution of `instance` as a WASI command by invoking its
If `start()` is called more than once, an exception is thrown.
### `wasi.initialize(instance)`
<!-- YAML
added:
- REPLACEME
-->
* `instance` {WebAssembly.Instance}
Attempt to initialize `instance` as a WASI reactor by invoking its
`_initialize()` export, if it is present. If `instance` contains a `_start()`
export, then an exception is thrown.
`initialize()` requires that `instance` exports a [`WebAssembly.Memory`][] named
`memory`. If `instance` does not have a `memory` export an exception is thrown.
If `initialize()` is called more than once, an exception is thrown.
### `wasi.wasiImport`
<!-- YAML
added:

View File

@ -20,13 +20,36 @@ const {
validateObject,
} = require('internal/validators');
const { WASI: _WASI } = internalBinding('wasi');
const kExitCode = Symbol('exitCode');
const kSetMemory = Symbol('setMemory');
const kStarted = Symbol('started');
const kExitCode = Symbol('kExitCode');
const kSetMemory = Symbol('kSetMemory');
const kStarted = Symbol('kStarted');
const kInstance = Symbol('kInstance');
emitExperimentalWarning('WASI');
function setupInstance(self, instance) {
validateObject(instance, 'instance');
validateObject(instance.exports, 'instance.exports');
// WASI::_SetMemory() in src/node_wasi.cc only expects that |memory| is
// an object. It will try to look up the .buffer property when needed
// and fail with UVWASI_EINVAL when the property is missing or is not
// an ArrayBuffer. Long story short, we don't need much validation here
// but we type-check anyway because it helps catch bugs in the user's
// code early.
validateObject(instance.exports.memory, 'instance.exports.memory');
if (!isArrayBuffer(instance.exports.memory.buffer)) {
throw new ERR_INVALID_ARG_TYPE(
'instance.exports.memory.buffer',
['WebAssembly.Memory'],
instance.exports.memory.buffer);
}
self[kInstance] = instance;
self[kSetMemory](instance.exports.memory);
}
class WASI {
constructor(options = {}) {
validateObject(options, 'options');
@ -75,51 +98,31 @@ class WASI {
this.wasiImport = wrap;
this[kStarted] = false;
this[kExitCode] = 0;
this[kInstance] = undefined;
}
// Must not export _initialize, must export _start
start(instance) {
validateObject(instance, 'instance');
if (this[kStarted]) {
throw new ERR_WASI_ALREADY_STARTED();
}
this[kStarted] = true;
const exports = instance.exports;
setupInstance(this, instance);
validateObject(exports, 'instance.exports');
const { _initialize, _start, memory } = exports;
const { _start, _initialize } = this[kInstance].exports;
if (typeof _start !== 'function') {
throw new ERR_INVALID_ARG_TYPE(
'instance.exports._start', 'function', _start);
}
if (_initialize !== undefined) {
throw new ERR_INVALID_ARG_TYPE(
'instance.exports._initialize', 'undefined', _initialize);
}
// WASI::_SetMemory() in src/node_wasi.cc only expects that |memory| is
// an object. It will try to look up the .buffer property when needed
// and fail with UVWASI_EINVAL when the property is missing or is not
// an ArrayBuffer. Long story short, we don't need much validation here
// but we type-check anyway because it helps catch bugs in the user's
// code early.
validateObject(memory, 'instance.exports.memory');
if (!isArrayBuffer(memory.buffer)) {
throw new ERR_INVALID_ARG_TYPE(
'instance.exports.memory.buffer',
['WebAssembly.Memory'],
memory.buffer);
}
if (this[kStarted]) {
throw new ERR_WASI_ALREADY_STARTED();
}
this[kStarted] = true;
this[kSetMemory](memory);
try {
exports._start();
_start();
} catch (err) {
if (err !== kExitCode) {
throw err;
@ -128,6 +131,31 @@ class WASI {
return this[kExitCode];
}
// Must not export _start, may optionally export _initialize
initialize(instance) {
if (this[kStarted]) {
throw new ERR_WASI_ALREADY_STARTED();
}
this[kStarted] = true;
setupInstance(this, instance);
const { _start, _initialize } = this[kInstance].exports;
if (typeof _initialize !== 'function' && _initialize !== undefined) {
throw new ERR_INVALID_ARG_TYPE(
'instance.exports._initialize', 'function', _initialize);
}
if (_start !== undefined) {
throw new ERR_INVALID_ARG_TYPE(
'instance.exports._start', 'undefined', _initialize);
}
if (_initialize !== undefined) {
_initialize();
}
}
}

View File

@ -0,0 +1,195 @@
// Flags: --experimental-wasi-unstable-preview1
'use strict';
const common = require('../common');
const assert = require('assert');
const vm = require('vm');
const { WASI } = require('wasi');
const fixtures = require('../common/fixtures');
const bufferSource = fixtures.readSync('simple.wasm');
(async () => {
{
// Verify that a WebAssembly.Instance is passed in.
const wasi = new WASI();
assert.throws(
() => { wasi.initialize(); },
{
code: 'ERR_INVALID_ARG_TYPE',
message: /"instance" argument must be of type object/
}
);
}
{
// Verify that the passed instance has an exports objects.
const wasi = new WASI({});
const wasm = await WebAssembly.compile(bufferSource);
const instance = await WebAssembly.instantiate(wasm);
Object.defineProperty(instance, 'exports', { get() { return null; } });
assert.throws(
() => { wasi.initialize(instance); },
{
code: 'ERR_INVALID_ARG_TYPE',
message: /"instance\.exports" property must be of type object/
}
);
}
{
// Verify that a _initialize() export was passed.
const wasi = new WASI({});
const wasm = await WebAssembly.compile(bufferSource);
const instance = await WebAssembly.instantiate(wasm);
Object.defineProperty(instance, 'exports', {
get() {
return { _initialize: 5, memory: new Uint8Array() };
},
});
assert.throws(
() => { wasi.initialize(instance); },
{
code: 'ERR_INVALID_ARG_TYPE',
message: /"instance\.exports\._initialize" property must be of type function/
}
);
}
{
// Verify that a _start export was not passed.
const wasi = new WASI({});
const wasm = await WebAssembly.compile(bufferSource);
const instance = await WebAssembly.instantiate(wasm);
Object.defineProperty(instance, 'exports', {
get() {
return {
_start() {},
_initialize() {},
memory: new Uint8Array(),
};
}
});
assert.throws(
() => { wasi.initialize(instance); },
{
code: 'ERR_INVALID_ARG_TYPE',
message: /"instance\.exports\._start" property must be undefined/
}
);
}
{
// Verify that a memory export was passed.
const wasi = new WASI({});
const wasm = await WebAssembly.compile(bufferSource);
const instance = await WebAssembly.instantiate(wasm);
Object.defineProperty(instance, 'exports', {
get() { return { _initialize() {} }; }
});
assert.throws(
() => { wasi.initialize(instance); },
{
code: 'ERR_INVALID_ARG_TYPE',
message: /"instance\.exports\.memory" property must be of type object/
}
);
}
{
// Verify that a non-ArrayBuffer memory.buffer is rejected.
const wasi = new WASI({});
const wasm = await WebAssembly.compile(bufferSource);
const instance = await WebAssembly.instantiate(wasm);
Object.defineProperty(instance, 'exports', {
get() {
return {
_initialize() {},
memory: {},
};
}
});
// The error message is a little white lie because any object
// with a .buffer property of type ArrayBuffer is accepted,
// but 99% of the time a WebAssembly.Memory object is used.
assert.throws(
() => { wasi.initialize(instance); },
{
code: 'ERR_INVALID_ARG_TYPE',
message: /"instance\.exports\.memory\.buffer" property must be an WebAssembly\.Memory/
}
);
}
{
// Verify that an argument that duck-types as a WebAssembly.Instance
// is accepted.
const wasi = new WASI({});
const wasm = await WebAssembly.compile(bufferSource);
const instance = await WebAssembly.instantiate(wasm);
Object.defineProperty(instance, 'exports', {
get() {
return {
_initialize() {},
memory: { buffer: new ArrayBuffer(0) },
};
}
});
wasi.initialize(instance);
}
{
// Verify that a WebAssembly.Instance from another VM context is accepted.
const wasi = new WASI({});
const instance = await vm.runInNewContext(`
(async () => {
const wasm = await WebAssembly.compile(bufferSource);
const instance = await WebAssembly.instantiate(wasm);
Object.defineProperty(instance, 'exports', {
get() {
return {
_initialize() {},
memory: new WebAssembly.Memory({ initial: 1 })
};
}
});
return instance;
})()
`, { bufferSource });
wasi.initialize(instance);
}
{
// Verify that initialize() can only be called once.
const wasi = new WASI({});
const wasm = await WebAssembly.compile(bufferSource);
const instance = await WebAssembly.instantiate(wasm);
Object.defineProperty(instance, 'exports', {
get() {
return {
_initialize() {},
memory: new WebAssembly.Memory({ initial: 1 })
};
}
});
wasi.initialize(instance);
assert.throws(
() => { wasi.initialize(instance); },
{
code: 'ERR_WASI_ALREADY_STARTED',
message: /^WASI instance has already started$/
}
);
}
})().then(common.mustCall());

View File

@ -45,7 +45,11 @@ const bufferSource = fixtures.readSync('simple.wasm');
const wasm = await WebAssembly.compile(bufferSource);
const instance = await WebAssembly.instantiate(wasm);
Object.defineProperty(instance, 'exports', { get() { return {}; } });
Object.defineProperty(instance, 'exports', {
get() {
return { memory: new Uint8Array() };
},
});
assert.throws(
() => { wasi.start(instance); },
{
@ -65,7 +69,8 @@ const bufferSource = fixtures.readSync('simple.wasm');
get() {
return {
_start() {},
_initialize() {}
_initialize() {},
memory: new Uint8Array(),
};
}
});