Skip to content

Commit 44063d2

Browse files
add stdin support
1 parent c3a0a99 commit 44063d2

File tree

4 files changed

+127
-36
lines changed

4 files changed

+127
-36
lines changed

src/abi.ts

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -97,6 +97,27 @@ export class WASIAbi {
9797
byteLength(value: string): number {
9898
return this.encoder.encode(value).length;
9999
}
100+
101+
102+
private static readonly iovec_t = {
103+
size: 8,
104+
bufferOffset: 0,
105+
lengthOffset: 4,
106+
}
107+
108+
iovViews(memory: DataView, iovs: number, iovsLen: number): Uint8Array[] {
109+
const iovsBuffers: Uint8Array[] = [];
110+
let iovsOffset = iovs;
111+
112+
for (let i = 0; i < iovsLen; i++) {
113+
const offset = memory.getUint32(iovsOffset + WASIAbi.iovec_t.bufferOffset, true);
114+
const len = memory.getUint32(iovsOffset + WASIAbi.iovec_t.lengthOffset, true);
115+
116+
iovsBuffers.push(new Uint8Array(memory.buffer, offset, len));
117+
iovsOffset += WASIAbi.iovec_t.size;
118+
}
119+
return iovsBuffers;
120+
}
100121
}
101122

102123
export class WASIProcExit {

src/features/fd.ts

Lines changed: 95 additions & 34 deletions
Original file line numberDiff line numberDiff line change
@@ -1,10 +1,79 @@
11
import { WASIAbi } from "../abi";
22
import { WASIFeatureProvider, WASIOptions } from "../options";
33

4-
const iovec_t = {
5-
size: 8,
6-
bufferOffset: 0,
7-
lengthOffset: 4,
4+
interface FdEntry {
5+
writev(iovs: Uint8Array[]): number
6+
readv(iovs: Uint8Array[]): number
7+
close(): void
8+
}
9+
10+
class WritableTextProxy implements FdEntry {
11+
private decoder = new TextDecoder('utf-8');
12+
constructor(private readonly handler: (lines: string) => void) { }
13+
14+
writev(iovs: Uint8Array[]): number {
15+
const totalBufferSize = iovs.reduce((acc, iov) => acc + iov.byteLength, 0);
16+
let offset = 0;
17+
const concatBuffer = new Uint8Array(totalBufferSize);
18+
for (const buffer of iovs) {
19+
concatBuffer.set(buffer, offset);
20+
offset += buffer.length;
21+
}
22+
23+
const lines = this.decoder.decode(concatBuffer);
24+
this.handler(lines);
25+
return concatBuffer.length;
26+
}
27+
readv(_iovs: Uint8Array[]): number {
28+
return 0;
29+
}
30+
close(): void {}
31+
}
32+
33+
class ReadableTextProxy implements FdEntry {
34+
private encoder = new TextEncoder();
35+
private pending: Uint8Array | null;
36+
constructor(private readonly consume: () => string) { }
37+
38+
writev(_iovs: Uint8Array[]): number {
39+
return 0;
40+
}
41+
readv(iovs: Uint8Array[]): number {
42+
let read = 0;
43+
for (const buffer of iovs) {
44+
let remaining = buffer.byteLength;
45+
if (this.pending) {
46+
const reading = Math.min(remaining, this.pending.byteLength);
47+
buffer.set(this.pending.slice(0, reading), 0);
48+
remaining -= reading;
49+
if (remaining < this.pending.byteLength) {
50+
this.pending = this.pending.slice(reading);
51+
continue;
52+
} else {
53+
this.pending = null;
54+
}
55+
}
56+
while (remaining > 0) {
57+
const newText = this.consume();
58+
const bytes = this.encoder.encode(newText);
59+
if (bytes.length == 0) {
60+
return read;
61+
}
62+
if (bytes.length > remaining) {
63+
buffer.set(bytes.slice(0, remaining), buffer.byteLength - remaining);
64+
this.pending = bytes.slice(remaining);
65+
read += remaining;
66+
remaining = 0;
67+
} else {
68+
buffer.set(bytes, buffer.byteLength - remaining);
69+
read += bytes.length;
70+
remaining -= bytes.length;
71+
}
72+
}
73+
}
74+
return read;
75+
}
76+
close(): void {}
877
}
978

1079
/**
@@ -34,15 +103,21 @@ const iovec_t = {
34103
*/
35104
export function useStdio(
36105
useOptions: {
106+
stdin: () => string,
37107
stdout: (lines: string) => void,
38-
stderr: (lines: string) => void
108+
stderr: (lines: string) => void,
39109
} = {
110+
stdin: () => { return "" },
40111
stdout: console.log,
41112
stderr: console.error,
42113
}
43114
): WASIFeatureProvider {
44115
return (options, abi, memoryView) => {
45-
const decoder = new TextDecoder('utf-8');
116+
const fdTable = [
117+
new ReadableTextProxy(useOptions.stdin),
118+
new WritableTextProxy(useOptions.stdout),
119+
new WritableTextProxy(useOptions.stderr),
120+
]
46121
return {
47122
fd_prestat_get: (fd: number, buf: number) => {
48123
return WASIAbi.WASI_ERRNO_BADF;
@@ -51,35 +126,21 @@ export function useStdio(
51126
return WASIAbi.WASI_ERRNO_BADF;
52127
},
53128
fd_write: (fd: number, iovs: number, iovsLen: number, nwritten: number) => {
54-
if (fd > 2) return WASIAbi.WASI_ERRNO_BADF;
55-
129+
const fdEntry = fdTable[fd];
130+
if (!fdEntry) return WASIAbi.WASI_ERRNO_BADF;
56131
const view = memoryView();
57-
const partialBuffers: Uint8Array[] = [];
58-
let iovsOffset = iovs;
59-
let concatBufferSize = 0;
60-
61-
for (let i = 0; i < iovsLen; i++) {
62-
const offset = view.getUint32(iovsOffset + iovec_t.bufferOffset, true);
63-
const len = view.getUint32(iovsOffset + iovec_t.lengthOffset, true);
64-
65-
partialBuffers.push(new Uint8Array(view.buffer, offset, len));
66-
iovsOffset += iovec_t.size;
67-
concatBufferSize += len;
68-
}
69-
const concatBuffer = new Uint8Array(concatBufferSize);
70-
let offset = 0;
71-
for (const buffer of partialBuffers) {
72-
concatBuffer.set(buffer, offset);
73-
offset += buffer.length;
74-
}
75-
76-
const lines = decoder.decode(concatBuffer);
77-
if (fd === 1) {
78-
useOptions.stdout(lines);
79-
} else if (fd === 2) {
80-
useOptions.stderr(lines);
81-
}
82-
view.setUint32(nwritten, concatBuffer.length, true);
132+
const iovsBuffers = abi.iovViews(view, iovs, iovsLen);
133+
const writtenValue = fdEntry.writev(iovsBuffers);
134+
view.setUint32(nwritten, writtenValue, true);
135+
return WASIAbi.WASI_ESUCCESS;
136+
},
137+
fd_read: (fd: number, iovs: number, iovsLen: number, nread: number) => {
138+
const fdEntry = fdTable[fd];
139+
if (!fdEntry) return WASIAbi.WASI_ERRNO_BADF;
140+
const view = memoryView();
141+
const iovsBuffers = abi.iovViews(view, iovs, iovsLen);
142+
const readValue = fdEntry.readv(iovsBuffers);
143+
view.setUint32(nread, readValue, true);
83144
return WASIAbi.WASI_ESUCCESS;
84145
}
85146
}

test/wasi-test-suite/harness.ts

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -6,9 +6,17 @@ import { readFile } from "fs/promises";
66
export async function runTest(filePath: string) {
77
let stdout = "";
88
let stderr = "";
9+
const stdin = await (async () => {
10+
const path = filePath.replace(/\.wasm$/, ".stdin");
11+
if (!existsSync(path)) {
12+
return "";
13+
}
14+
return await readFile(path, "utf8");
15+
})()
916
const features = [
1017
useEnviron, useArgs, useClock, useProc,
1118
useRandom(), useStdio({
19+
stdin: () => { return stdin },
1220
stdout: (lines) => { stdout += lines },
1321
stderr: (lines) => { stderr += lines },
1422
})
@@ -35,7 +43,7 @@ export async function runTest(filePath: string) {
3543
const { instance } = await WebAssembly.instantiate(await readFile(filePath), {
3644
wasi_snapshot_preview1: wasi.wasiImport,
3745
});
38-
let exitCode: number | undefined;
46+
let exitCode: number;
3947
try {
4048
exitCode = wasi.start(instance);
4149
} catch (e) {
@@ -44,6 +52,8 @@ export async function runTest(filePath: string) {
4452
// SIGABRT (=0x6) signal. It results in exit code 0x80 + signal number in shell.
4553
// Reference: https://tldp.org/LDP/abs/html/exitcodes.html#EXITCODESREF
4654
exitCode = 0x86;
55+
} else {
56+
throw e;
4757
}
4858
}
4959
const expectedExitCode = await (async () => {

test/wasi-test-suite/libc.test.ts

Lines changed: 0 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,6 @@ describe("wasi-test-suite-libc", () => {
88
const UNSUPPORTED = [
99
"clock_getres-monotonic.wasm",
1010
"clock_gettime-monotonic.wasm",
11-
"stdin-hello.wasm",
1211
"ftruncate.wasm",
1312
]
1413

0 commit comments

Comments
 (0)