From 60e5f45141494882a50e484e691d6ea89af43d39 Mon Sep 17 00:00:00 2001 From: Michael Dawson Date: Tue, 31 Jan 2023 18:46:55 -0500 Subject: [PATCH] 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 PR-URL: https://github.com/nodejs/node/pull/46469 Reviewed-By: Guy Bedford Reviewed-By: James M Snell Reviewed-By: Colin Ihrig --- doc/api/wasi.md | 42 ++++++++++++++++----- lib/wasi.js | 34 ++++++++++++++++- src/node_wasi.cc | 11 +++--- test/wasi/test-wasi-options-validation.js | 9 +++++ test/wasi/test-wasi.js | 45 +++++++++++++++++++++-- 5 files changed, 120 insertions(+), 21 deletions(-) diff --git a/doc/api/wasi.md b/doc/api/wasi.md index c0b07c63bb3426..a57b85bb0d3c6d 100644 --- a/doc/api/wasi.md +++ b/doc/api/wasi.md @@ -16,6 +16,7 @@ import { WASI } from 'wasi'; import { argv, env } from 'node:process'; const wasi = new WASI({ + version: 'preview1', args: argv, env, 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( 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); ``` @@ -43,6 +40,7 @@ const { argv, env } = require('node:process'); const { join } = require('node:path'); const wasi = new WASI({ + version: 'preview1', args: argv, env, 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 () => { 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); })(); @@ -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} @@ -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()` + + + +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)` diff --git a/lib/wasi.js b/lib/wasi.js index 54786a4eb1b556..6c023e9d6bd644 100644 --- a/lib/wasi.js +++ b/lib/wasi.js @@ -10,6 +10,7 @@ const { } = primordials; const { + ERR_INVALID_ARG_VALUE, ERR_WASI_ALREADY_STARTED } = require('internal/errors').codes; const { @@ -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'); @@ -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')); + this[kBindingName] = 'wasi_snapshot_preview1'; + } + if (options.args !== undefined) validateArray(options.args, 'options.args'); const args = ArrayPrototypeMap(options.args || [], String); @@ -138,8 +165,11 @@ class WASI { _initialize(); } } -} + getImportObject() { + return { [this[kBindingName]]: this.wasiImport }; + } +} module.exports = { WASI }; diff --git a/src/node_wasi.cc b/src/node_wasi.cc index 5d7f5a4dff50ec..6f8ceb3bb67308 100644 --- a/src/node_wasi.cc +++ b/src/node_wasi.cc @@ -1247,10 +1247,10 @@ void WASI::_SetMemory(const FunctionCallbackInfo& args) { wasi->memory_.Reset(wasi->env()->isolate(), args[0].As()); } -static void Initialize(Local target, - Local unused, - Local context, - void* priv) { +static void InitializePreview1(Local target, + Local unused, + Local context, + void* priv) { Environment* env = Environment::GetCurrent(context); Isolate* isolate = env->isolate(); @@ -1313,8 +1313,7 @@ static void Initialize(Local 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) diff --git a/test/wasi/test-wasi-options-validation.js b/test/wasi/test-wasi-options-validation.js index 913095631aa22f..20d7663c0d0c5f 100644 --- a/test/wasi/test-wasi-options-validation.js +++ b/test/wasi/test-wasi-options-validation.js @@ -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/ }); diff --git a/test/wasi/test-wasi.js b/test/wasi/test-wasi.js index e262d4a45c3718..15ca9d6c5cc47b 100644 --- a/test/wasi/test-wasi.js +++ b/test/wasi/test-wasi.js @@ -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'); @@ -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: { @@ -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()); @@ -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' });