Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

wasi: add reactor support #34046

Merged
merged 1 commit into from Jun 29, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
17 changes: 17 additions & 0 deletions doc/api/wasi.md
Expand Up @@ -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:
Expand Down
90 changes: 59 additions & 31 deletions lib/wasi.js
Expand Up @@ -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');
Expand Down Expand Up @@ -75,58 +98,63 @@ 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');

const exports = instance.exports;
if (this[kStarted]) {
throw new ERR_WASI_ALREADY_STARTED();
}
this[kStarted] = true;

validateObject(exports, 'instance.exports');
setupInstance(this, instance);

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);
try {
_start();
} catch (err) {
if (err !== kExitCode) {
throw err;
}
}

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;
this[kSetMemory](memory);

try {
exports._start();
} catch (err) {
if (err !== kExitCode) {
throw err;
}
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);
}

return this[kExitCode];
if (_initialize !== undefined) {
_initialize();
}
}
}

Expand Down
195 changes: 195 additions & 0 deletions test/wasi/test-wasi-initialize-validation.js
@@ -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());
9 changes: 7 additions & 2 deletions test/wasi/test-wasi-start-validation.js
Expand Up @@ -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); },
{
Expand All @@ -65,7 +69,8 @@ const bufferSource = fixtures.readSync('simple.wasm');
get() {
return {
_start() {},
_initialize() {}
_initialize() {},
memory: new Uint8Array(),
};
}
});
Expand Down