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 support for version when creating WASI #46469

Closed
wants to merge 12 commits into from
42 changes: 32 additions & 10 deletions doc/api/wasi.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,21 +16,18 @@ import { WASI } from 'wasi';
import { argv, env } from 'node:process';

const wasi = new WASI({
version: 'preview1',
args: argv,
env,
preopens: {
'/sandbox': '/some/real/path/that/wasm/can/access',
},
});

// Some WASI binaries require:
// const importObject = { wasi_unstable: wasi.wasiImport };
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };

Comment on lines -26 to -29
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We appear to be losing this information, which could be important to some users. I think we either need to keep this somewhere in the docs, or support passing in "wasi_unstable" as a version which would return the same API as wasi_snapshot_preview1, but with a different version string.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm happy to either add it back to the doc or add to "wasi_unstable" to what can be passed in as a version depending on what people think is best.

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It seems like a pretty easy thing to support since it would be an alias for preview1, so I'd suggest supporting it and removing this note from the docs (but we would need to document that "wasi_unstable" is a valid version).

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

will do

const wasm = await WebAssembly.compile(
await readFile(new URL('./demo.wasm', import.meta.url)),
);
const instance = await WebAssembly.instantiate(wasm, importObject);
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());

wasi.start(instance);
```
Expand All @@ -43,22 +40,19 @@ const { argv, env } = require('node:process');
const { join } = require('node:path');

const wasi = new WASI({
version: 'preview1',
args: argv,
env,
preopens: {
'/sandbox': '/some/real/path/that/wasm/can/access',
},
});

// Some WASI binaries require:
// const importObject = { wasi_unstable: wasi.wasiImport };
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };

(async () => {
const wasm = await WebAssembly.compile(
await readFile(join(__dirname, 'demo.wasm')),
);
const instance = await WebAssembly.instantiate(wasm, importObject);
const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());

wasi.start(instance);
})();
Expand Down Expand Up @@ -126,6 +120,10 @@ sandbox directory structure configured explicitly.
added:
- v13.3.0
- v12.16.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/46469
description: version field added to options.
-->

* `options` {Object}
Expand All @@ -148,6 +146,30 @@ added:
WebAssembly application. **Default:** `1`.
* `stderr` {integer} The file descriptor used as standard error in the
WebAssembly application. **Default:** `2`.
* `version` {string} The version of WASI requested. Currently the only
supported versions are `unstable` and `preview1`. **Default:** `preview1`.

### `wasi.getImportObject()`

<!-- YAML
added: REPLACEME
-->

Return an import object that can be passed to `WebAssembly.instantiate()` if
no other WASM imports are needed beyond those provided by WASI.

If version `unstable` was passed into the constructor it will return:

```json
{ wasi_unstable: wasi.wasiImport }
```

If version `preview1` was passed into the constructor or no version was
specified it will return:

```json
{ wasi_snapshot_preview1: wasi.wasiImport }
```

### `wasi.start(instance)`

Expand Down
34 changes: 32 additions & 2 deletions lib/wasi.js
Original file line number Diff line number Diff line change
Expand Up @@ -10,6 +10,7 @@ const {
} = primordials;

const {
ERR_INVALID_ARG_VALUE,
ERR_WASI_ALREADY_STARTED
} = require('internal/errors').codes;
const {
Expand All @@ -22,13 +23,14 @@ const {
validateFunction,
validateInt32,
validateObject,
validateString,
validateUndefined,
} = require('internal/validators');
const { WASI: _WASI } = internalBinding('wasi');
const kExitCode = Symbol('kExitCode');
const kSetMemory = Symbol('kSetMemory');
const kStarted = Symbol('kStarted');
const kInstance = Symbol('kInstance');
const kBindingName = Symbol('kBindingName');

emitExperimentalWarning('WASI');

Expand All @@ -45,6 +47,31 @@ class WASI {
constructor(options = kEmptyObject) {
validateObject(options, 'options');

let _WASI;
if (options.version !== undefined) {
validateString(options.version, 'options.version');
switch (options.version) {
case 'unstable':
({ WASI: _WASI } = internalBinding('wasi'));
this[kBindingName] = 'wasi_unstable';
break;
// When adding support for additional wasi versions add case here
case 'preview1':
({ WASI: _WASI } = internalBinding('wasi'));
this[kBindingName] = 'wasi_snapshot_preview1';
break;
// When adding support for additional wasi versions add case here
default:
throw new ERR_INVALID_ARG_VALUE('options.version',
options.version,
'unsupported WASI version');
}
} else {
// TODO(mdawson): Remove this in a SemVer major PR before Node.js 20
({ WASI: _WASI } = internalBinding('wasi'));
mhdawson marked this conversation as resolved.
Show resolved Hide resolved
this[kBindingName] = 'wasi_snapshot_preview1';
}

if (options.args !== undefined)
validateArray(options.args, 'options.args');
const args = ArrayPrototypeMap(options.args || [], String);
Expand Down Expand Up @@ -138,8 +165,11 @@ class WASI {
_initialize();
}
}
}

getImportObject() {
return { [this[kBindingName]]: this.wasiImport };
}
}

module.exports = { WASI };

Expand Down
11 changes: 5 additions & 6 deletions src/node_wasi.cc
Original file line number Diff line number Diff line change
Expand Up @@ -1247,10 +1247,10 @@ void WASI::_SetMemory(const FunctionCallbackInfo<Value>& args) {
wasi->memory_.Reset(wasi->env()->isolate(), args[0].As<WasmMemoryObject>());
}

static void Initialize(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
static void InitializePreview1(Local<Object> target,
Local<Value> unused,
Local<Context> context,
void* priv) {
Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate();

Expand Down Expand Up @@ -1313,8 +1313,7 @@ static void Initialize(Local<Object> target,
SetConstructorFunction(context, target, "WASI", tmpl);
}


} // namespace wasi
} // namespace node

NODE_BINDING_CONTEXT_AWARE_INTERNAL(wasi, node::wasi::Initialize)
NODE_BINDING_CONTEXT_AWARE_INTERNAL(wasi, node::wasi::InitializePreview1)
9 changes: 9 additions & 0 deletions test/wasi/test-wasi-options-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -47,3 +47,12 @@ assert.throws(() => { new WASI({ stderr: 'fhqwhgads' }); },
assert.throws(() => {
new WASI({ preopens: { '/sandbox': '__/not/real/path' } });
}, { code: 'UVWASI_ENOENT', message: /uvwasi_init/ });

// If version is not a string, it should throw
assert.throws(() => { new WASI({ version: { x: 'y' } }); },
{ code: 'ERR_INVALID_ARG_TYPE', message: /\bversion\b/ });


// If version is an unsupported version, it should throw
assert.throws(() => { new WASI({ version: 'not_a_version' }); },
{ code: 'ERR_INVALID_ARG_VALUE', message: /\bversion\b/ });
45 changes: 42 additions & 3 deletions test/wasi/test-wasi.js
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
'use strict';
const common = require('../common');

if (process.argv[2] === 'wasi-child') {
if (process.argv[2] === 'wasi-child-default') {
// test default case
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const fs = require('fs');
Expand Down Expand Up @@ -30,12 +31,49 @@ if (process.argv[2] === 'wasi-child') {

wasi.start(instance);
})().then(common.mustCall());
} else if (process.argv[2] === 'wasi-child-preview1') {
// Test version set to preview1
const assert = require('assert');
const fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir');
const fs = require('fs');
const path = require('path');

common.expectWarning('ExperimentalWarning',
'WASI is an experimental feature and might change at any time');

const { WASI } = require('wasi');
tmpdir.refresh();
const wasmDir = path.join(__dirname, 'wasm');
const wasiPreview1 = new WASI({
version: 'preview1',
args: ['foo', '-bar', '--baz=value'],
env: process.env,
preopens: {
'/sandbox': fixtures.path('wasi'),
'/tmp': tmpdir.path,
},
});

// Validate the getImportObject helper
assert.strictEqual(wasiPreview1.wasiImport,
wasiPreview1.getImportObject().wasi_snapshot_preview1);
const modulePathPreview1 = path.join(wasmDir, `${process.argv[3]}.wasm`);
const bufferPreview1 = fs.readFileSync(modulePathPreview1);

(async () => {
const { instance: instancePreview1 } =
await WebAssembly.instantiate(bufferPreview1,
wasiPreview1.getImportObject());

wasiPreview1.start(instancePreview1);
})().then(common.mustCall());
} else {
const assert = require('assert');
const cp = require('child_process');
const { checkoutEOL } = common;

function innerRunWASI(options, args) {
function innerRunWASI(options, args, flavor = 'default') {
console.log('executing', options.test);
const opts = {
env: {
Expand All @@ -52,7 +90,7 @@ if (process.argv[2] === 'wasi-child') {
...args,
'--experimental-wasi-unstable-preview1',
__filename,
'wasi-child',
'wasi-child-' + flavor,
options.test,
], opts);
console.log(child.stderr.toString());
Expand All @@ -64,6 +102,7 @@ if (process.argv[2] === 'wasi-child') {
function runWASI(options) {
innerRunWASI(options, ['--no-turbo-fast-api-calls']);
innerRunWASI(options, ['--turbo-fast-api-calls']);
innerRunWASI(options, ['--turbo-fast-api-calls'], 'preview1');
}

runWASI({ test: 'cant_dotdot' });
Expand Down