Skip to content

Commit

Permalink
wasi: allow WASI stdio to be configured
Browse files Browse the repository at this point in the history
This commit adds stdin, stderr, and stdout options to WASI, which
allow the stdio streams to be configured.

PR-URL: #33544
Reviewed-By: Anna Henningsen <[email protected]>
Reviewed-By: David Carlier <[email protected]>
Reviewed-By: Juan José Arboleda <[email protected]>
Reviewed-By: Zeyu Yang <[email protected]>
Reviewed-By: Ben Noordhuis <[email protected]>
Reviewed-By: James M Snell <[email protected]>
  • Loading branch information
cjihrig authored and codebytere committed Jun 18, 2020
1 parent 6d25b57 commit af14c1f
Show file tree
Hide file tree
Showing 5 changed files with 71 additions and 5 deletions.
6 changes: 6 additions & 0 deletions doc/api/wasi.md
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,12 @@ added:
process via the `__wasi_proc_exit()` function. Setting this option to `true`
causes `wasi.start()` to return the exit code rather than terminate the
process. **Default:** `false`.
* `stdin` {integer} The file descriptor used as standard input in the
WebAssembly application. **Default:** `0`.
* `stdout` {integer} The file descriptor used as standard output in the
WebAssembly application. **Default:** `1`.
* `stderr` {integer} The file descriptor used as standard error in the
WebAssembly application. **Default:** `2`.

### `wasi.start(instance)`
<!-- YAML
Expand Down
9 changes: 8 additions & 1 deletion lib/wasi.js
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ const { isArrayBuffer } = require('internal/util/types');
const {
validateArray,
validateBoolean,
validateInt32,
validateObject,
} = require('internal/validators');
const { WASI: _WASI } = internalBinding('wasi');
Expand Down Expand Up @@ -51,7 +52,13 @@ class WASI {
}
}

const wrap = new _WASI(args, env, preopens);
const { stdin = 0, stdout = 1, stderr = 2 } = options;
validateInt32(stdin, 'options.stdin', 0);
validateInt32(stdout, 'options.stdout', 0);
validateInt32(stderr, 'options.stderr', 0);
const stdio = [stdin, stdout, stderr];

const wrap = new _WASI(args, env, preopens, stdio);

for (const prop in wrap) {
wrap[prop] = FunctionPrototypeBind(wrap[prop], wrap);
Expand Down
15 changes: 11 additions & 4 deletions src/node_wasi.cc
Original file line number Diff line number Diff line change
Expand Up @@ -163,20 +163,27 @@ void WASI::DecreaseAllocatedSize(size_t size) {

void WASI::New(const FunctionCallbackInfo<Value>& args) {
CHECK(args.IsConstructCall());
CHECK_EQ(args.Length(), 3);
CHECK_EQ(args.Length(), 4);
CHECK(args[0]->IsArray());
CHECK(args[1]->IsArray());
CHECK(args[2]->IsArray());
CHECK(args[3]->IsArray());

Environment* env = Environment::GetCurrent(args);
Local<Context> context = env->context();
Local<Array> argv = args[0].As<Array>();
const uint32_t argc = argv->Length();
uvwasi_options_t options;

options.in = 0;
options.out = 1;
options.err = 2;
Local<Array> stdio = args[3].As<Array>();
CHECK_EQ(stdio->Length(), 3);
options.in = stdio->Get(context, 0).ToLocalChecked()->
Int32Value(context).FromJust();
options.out = stdio->Get(context, 1).ToLocalChecked()->
Int32Value(context).FromJust();
options.err = stdio->Get(context, 2).ToLocalChecked()->
Int32Value(context).FromJust();

options.fd_table_size = 3;
options.argc = argc;
options.argv =
Expand Down
12 changes: 12 additions & 0 deletions test/wasi/test-wasi-options-validation.js
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,18 @@ assert.throws(() => { new WASI({ preopens: 'fhqwhgads' }); },
assert.throws(() => { new WASI({ returnOnExit: 'fhqwhgads' }); },
{ code: 'ERR_INVALID_ARG_TYPE', message: /\breturnOnExit\b/ });

// If stdin is not an int32 and not undefined, it should throw.
assert.throws(() => { new WASI({ stdin: 'fhqwhgads' }); },
{ code: 'ERR_INVALID_ARG_TYPE', message: /\bstdin\b/ });

// If stdout is not an int32 and not undefined, it should throw.
assert.throws(() => { new WASI({ stdout: 'fhqwhgads' }); },
{ code: 'ERR_INVALID_ARG_TYPE', message: /\bstdout\b/ });

// If stderr is not an int32 and not undefined, it should throw.
assert.throws(() => { new WASI({ stderr: 'fhqwhgads' }); },
{ code: 'ERR_INVALID_ARG_TYPE', message: /\bstderr\b/ });

// If options is provided, but not an object, the constructor should throw.
[null, 'foo', '', 0, NaN, Symbol(), true, false, () => {}].forEach((value) => {
assert.throws(() => { new WASI(value); },
Expand Down
34 changes: 34 additions & 0 deletions test/wasi/test-wasi-stdio.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
// Flags: --experimental-wasi-unstable-preview1 --experimental-wasm-bigint
'use strict';
require('../common');
const tmpdir = require('../common/tmpdir');
const { strictEqual } = require('assert');
const { closeSync, openSync, readFileSync, writeFileSync } = require('fs');
const { join } = require('path');
const { WASI } = require('wasi');
const modulePath = join(__dirname, 'wasm', 'stdin.wasm');
const buffer = readFileSync(modulePath);
const stdinFile = join(tmpdir.path, 'stdin.txt');
const stdoutFile = join(tmpdir.path, 'stdout.txt');
const stderrFile = join(tmpdir.path, 'stderr.txt');

tmpdir.refresh();
// Write 33 x's. The test's buffer only holds 31 x's + a terminator.
writeFileSync(stdinFile, 'x'.repeat(33));

const stdin = openSync(stdinFile, 'r');
const stdout = openSync(stdoutFile, 'a');
const stderr = openSync(stderrFile, 'a');
const wasi = new WASI({ stdin, stdout, stderr, returnOnExit: true });
const importObject = { wasi_snapshot_preview1: wasi.wasiImport };

(async () => {
const { instance } = await WebAssembly.instantiate(buffer, importObject);

strictEqual(wasi.start(instance), 0);
closeSync(stdin);
closeSync(stdout);
closeSync(stderr);
strictEqual(readFileSync(stdoutFile, 'utf8').trim(), 'x'.repeat(31));
strictEqual(readFileSync(stderrFile, 'utf8').trim(), '');
})();

0 comments on commit af14c1f

Please sign in to comment.