Skip to content

Commit

Permalink
wasi: add reactor support
Browse files Browse the repository at this point in the history
PR-URL: #34046
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
  • Loading branch information
devsnek authored and MylesBorins committed Jul 16, 2020
1 parent 105d560 commit 6be685a
Show file tree
Hide file tree
Showing 4 changed files with 278 additions and 33 deletions.
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

0 comments on commit 6be685a

Please sign in to comment.