wasi: add support for version when creating WASI

Refs: https://github.com/nodejs/node/issues/46254

- add version to options when creating WASI object
- add convenience function to return importObject

Signed-off-by: Michael Dawson <mdawson@devrus.com>

PR-URL: https://github.com/nodejs/node/pull/46469
Reviewed-By: Guy Bedford <guybedford@gmail.com>
Reviewed-By: James M Snell <jasnell@gmail.com>
Reviewed-By: Colin Ihrig <cjihrig@gmail.com>
This commit is contained in:
Michael Dawson 2023-01-31 18:46:55 -05:00
parent fda0de4c78
commit 2d35f669ae
5 changed files with 120 additions and 21 deletions

View File

@ -16,6 +16,7 @@ import { WASI } from 'wasi';
import { argv, env } from 'node:process'; import { argv, env } from 'node:process';
const wasi = new WASI({ const wasi = new WASI({
version: 'preview1',
args: argv, args: argv,
env, env,
preopens: { preopens: {
@ -23,14 +24,10 @@ const wasi = new WASI({
}, },
}); });
// Some WASI binaries require:
// const importObject = { wasi_unstable: wasi.wasiImport };
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };
const wasm = await WebAssembly.compile( const wasm = await WebAssembly.compile(
await readFile(new URL('./demo.wasm', import.meta.url)), 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); wasi.start(instance);
``` ```
@ -43,6 +40,7 @@ const { argv, env } = require('node:process');
const { join } = require('node:path'); const { join } = require('node:path');
const wasi = new WASI({ const wasi = new WASI({
version: 'preview1',
args: argv, args: argv,
env, env,
preopens: { preopens: {
@ -50,15 +48,11 @@ const wasi = new WASI({
}, },
}); });
// Some WASI binaries require:
// const importObject = { wasi_unstable: wasi.wasiImport };
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };
(async () => { (async () => {
const wasm = await WebAssembly.compile( const wasm = await WebAssembly.compile(
await readFile(join(__dirname, 'demo.wasm')), await readFile(join(__dirname, 'demo.wasm')),
); );
const instance = await WebAssembly.instantiate(wasm, importObject); const instance = await WebAssembly.instantiate(wasm, wasi.getImportObject());
wasi.start(instance); wasi.start(instance);
})(); })();
@ -126,6 +120,10 @@ sandbox directory structure configured explicitly.
added: added:
- v13.3.0 - v13.3.0
- v12.16.0 - v12.16.0
changes:
- version: REPLACEME
pr-url: https://github.com/nodejs/node/pull/46469
description: version field added to options.
--> -->
* `options` {Object} * `options` {Object}
@ -148,6 +146,30 @@ added:
WebAssembly application. **Default:** `1`. WebAssembly application. **Default:** `1`.
* `stderr` {integer} The file descriptor used as standard error in the * `stderr` {integer} The file descriptor used as standard error in the
WebAssembly application. **Default:** `2`. 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)` ### `wasi.start(instance)`

View File

@ -10,6 +10,7 @@ const {
} = primordials; } = primordials;
const { const {
ERR_INVALID_ARG_VALUE,
ERR_WASI_ALREADY_STARTED ERR_WASI_ALREADY_STARTED
} = require('internal/errors').codes; } = require('internal/errors').codes;
const { const {
@ -22,13 +23,14 @@ const {
validateFunction, validateFunction,
validateInt32, validateInt32,
validateObject, validateObject,
validateString,
validateUndefined, validateUndefined,
} = require('internal/validators'); } = require('internal/validators');
const { WASI: _WASI } = internalBinding('wasi');
const kExitCode = Symbol('kExitCode'); const kExitCode = Symbol('kExitCode');
const kSetMemory = Symbol('kSetMemory'); const kSetMemory = Symbol('kSetMemory');
const kStarted = Symbol('kStarted'); const kStarted = Symbol('kStarted');
const kInstance = Symbol('kInstance'); const kInstance = Symbol('kInstance');
const kBindingName = Symbol('kBindingName');
emitExperimentalWarning('WASI'); emitExperimentalWarning('WASI');
@ -45,6 +47,31 @@ class WASI {
constructor(options = kEmptyObject) { constructor(options = kEmptyObject) {
validateObject(options, 'options'); 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'));
this[kBindingName] = 'wasi_snapshot_preview1';
}
if (options.args !== undefined) if (options.args !== undefined)
validateArray(options.args, 'options.args'); validateArray(options.args, 'options.args');
const args = ArrayPrototypeMap(options.args || [], String); const args = ArrayPrototypeMap(options.args || [], String);
@ -138,8 +165,11 @@ class WASI {
_initialize(); _initialize();
} }
} }
}
getImportObject() {
return { [this[kBindingName]]: this.wasiImport };
}
}
module.exports = { WASI }; module.exports = { WASI };

View File

@ -1247,10 +1247,10 @@ void WASI::_SetMemory(const FunctionCallbackInfo<Value>& args) {
wasi->memory_.Reset(wasi->env()->isolate(), args[0].As<WasmMemoryObject>()); wasi->memory_.Reset(wasi->env()->isolate(), args[0].As<WasmMemoryObject>());
} }
static void Initialize(Local<Object> target, static void InitializePreview1(Local<Object> target,
Local<Value> unused, Local<Value> unused,
Local<Context> context, Local<Context> context,
void* priv) { void* priv) {
Environment* env = Environment::GetCurrent(context); Environment* env = Environment::GetCurrent(context);
Isolate* isolate = env->isolate(); Isolate* isolate = env->isolate();
@ -1313,8 +1313,7 @@ static void Initialize(Local<Object> target,
SetConstructorFunction(context, target, "WASI", tmpl); SetConstructorFunction(context, target, "WASI", tmpl);
} }
} // namespace wasi } // namespace wasi
} // namespace node } // namespace node
NODE_BINDING_CONTEXT_AWARE_INTERNAL(wasi, node::wasi::Initialize) NODE_BINDING_CONTEXT_AWARE_INTERNAL(wasi, node::wasi::InitializePreview1)

View File

@ -47,3 +47,12 @@ assert.throws(() => { new WASI({ stderr: 'fhqwhgads' }); },
assert.throws(() => { assert.throws(() => {
new WASI({ preopens: { '/sandbox': '__/not/real/path' } }); new WASI({ preopens: { '/sandbox': '__/not/real/path' } });
}, { code: 'UVWASI_ENOENT', message: /uvwasi_init/ }); }, { 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/ });

View File

@ -1,7 +1,8 @@
'use strict'; 'use strict';
const common = require('../common'); 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 fixtures = require('../common/fixtures');
const tmpdir = require('../common/tmpdir'); const tmpdir = require('../common/tmpdir');
const fs = require('fs'); const fs = require('fs');
@ -30,12 +31,49 @@ if (process.argv[2] === 'wasi-child') {
wasi.start(instance); wasi.start(instance);
})().then(common.mustCall()); })().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 { } else {
const assert = require('assert'); const assert = require('assert');
const cp = require('child_process'); const cp = require('child_process');
const { checkoutEOL } = common; const { checkoutEOL } = common;
function innerRunWASI(options, args) { function innerRunWASI(options, args, flavor = 'default') {
console.log('executing', options.test); console.log('executing', options.test);
const opts = { const opts = {
env: { env: {
@ -52,7 +90,7 @@ if (process.argv[2] === 'wasi-child') {
...args, ...args,
'--experimental-wasi-unstable-preview1', '--experimental-wasi-unstable-preview1',
__filename, __filename,
'wasi-child', 'wasi-child-' + flavor,
options.test, options.test,
], opts); ], opts);
console.log(child.stderr.toString()); console.log(child.stderr.toString());
@ -64,6 +102,7 @@ if (process.argv[2] === 'wasi-child') {
function runWASI(options) { function runWASI(options) {
innerRunWASI(options, ['--no-turbo-fast-api-calls']); innerRunWASI(options, ['--no-turbo-fast-api-calls']);
innerRunWASI(options, ['--turbo-fast-api-calls']); innerRunWASI(options, ['--turbo-fast-api-calls']);
innerRunWASI(options, ['--turbo-fast-api-calls'], 'preview1');
} }
runWASI({ test: 'cant_dotdot' }); runWASI({ test: 'cant_dotdot' });