Repository navigation
Are reading from stdin, writing to stdout and WASI targets supported? #364
|
Are reading from stdin, writing to stdout and WASI targets supported? |
Replies: 11 comments 12 replies
|
stdout — yes. stdin — there is no built-in declare function getchar(): i32;
// reads one line from stdin (without the line break); null at end of input
function readLine(): string | null {
let buffer: char[] = [];
let c = getchar();
if (c == -1) return null;
while (c != -1 && c != 10) {
if (c != 13) buffer.push(<char>c);
c = getchar();
}
buffer.push(<char>0);
return <string><Opaque>Ref(buffer[0]);
}
function main() {
let line: string | null = null;
while ((line = readLine()) != null) {
console.log(`> ${line}`);
}
}It works the same with WASM / WASI — WASM is supported, WASI is not a target yet. |
|
I've dealth with I do the I would say make sure binary I/O is there. |
|
I tried on tslang version v0.0-pre-alpha92. Got ./tslang --emit=exe --opt -o ~/bin/nm_tslang ./nm_tslang.ts
/media/user/x/TypeScriptCompiler/nm_tslang.ts:2:1: error: 'write' is declared again with another signature: a declared function is one external symbol, so it can't have overloads. Declare it once, with optional or union parameters, or give each signature a name of its own and bind them with @linkname
declare function write(fd: s32, buffer: Opaque, count: u32): s32;
^
/media/user/x/TypeScriptCompiler/nm_tslang.ts:2:1: error: failed statement
declare function write(fd: s32, buffer: Opaque, count: u32): s32;
|
|
I'll try testing with 120 later today or tomorrow. FYI this is what I use to test on the command line https://github.com/guest271314/NativeMessagingHosts/blob/main/nm_standalone_test.js, e.g., ~/bin/nm_standalone_test.js ./nm_js2wasm.sh~/bin/nm_standalone_test.js wasmtime ./output/nm_scriptc_sync_stream.wasm~/bin/nm_standalone_test.js wasmtime ./output/nm_perry_sync_stream.wasm~/bin/nm_standalone_test.js wasmtime ./output/nm_jz_sync_stream.wasm |
|
Right now I'm testing the same source code compiled using bun check --aot --explain nm_bun_sync_stream.ts
note: Untyped code works, but runs slower. 2 causes:
24 | const res: number = readSync(fd, view, offset, len, null);
^
note: 1. unknown-callee: The callee isn't known at build time, so the result has no type.
at nm_bun_sync_stream.ts:24:25
note: 1 indirect call
note: 1 of these runs in a loop
note: in function 'read'
note: fix: call the function directly, or narrow the result.
note: https://bun.com/docs/compiler/explain#unknown-callee
29 | const res: number = writeSync(fd, view, offset, len);
^
note: 2. unknown-callee: The callee isn't known at build time, so the result has no type.
at nm_bun_sync_stream.ts:29:25
note: 1 indirect call
note: 1 of these runs in a loop
note: in function 'write'
note: fix: call the function directly, or narrow the result.
note: https://bun.com/docs/compiler/explain#unknown-callee
note: All property accesses, including dependencies:
17 free: one load or store at a fixed offset
0 tested: a test of its own, then a load or store at a fixed offset
0 by name: looked up by name
Calls: 366 direct, 440 indirect
100% of property accesses are one load: 17 free, 0 tested, 0 by name (17)
✓ No type errors in 1 file, compiled 279 lines in 0.30 s (908 lines/s)
|
|
Here's the code I've so far successfully compiled to WASM with WASI support using the above. Next up compile all to native executable. #!/home/user/bin/bun -b
//! Bun Native Messaging host, synchronous streaming fs I/O (AOT experiment)
//! bun check --aot --explain nm_bun_sync_stream.ts
//! 100.0% of property accesses are one load
//! ✓ No type errors in 1 file
import { readSync, writeSync } from "node:fs";
const STDIN_FD: number = 0;
const STDOUT_FD: number = 1;
const STDERR_FD: number = 2;
const HEADER_SIZE: number = 4;
const COMMA: number = 44;
const OPEN_BRACKET: number = 91;
const CLOSE_BRACKET: number = 93;
const NO_BYTE: number = -1;
const CHUNK_SIZE: number = 1024 * 1024;
const STREAM_BUFFER_SIZE: number = CHUNK_SIZE * 2;
const UTILITY_BUFFER_SIZE: number = 4096;
class SystemIO {
static read(fd: number, view: Uint8Array, offset: number, len: number): number {
const res: number = readSync(fd, view, offset, len, null);
if (typeof res === "number") return res;
return 0;
}
static write(fd: number, view: Uint8Array, offset: number, len: number): number {
const res: number = writeSync(fd, view, offset, len);
if (typeof res === "number") return res;
return 0;
}
}
class NativeHostPipeline {
body: Uint8Array = new Uint8Array(STREAM_BUFFER_SIZE);
header: Uint8Array = new Uint8Array(HEADER_SIZE);
out: Uint8Array = new Uint8Array(HEADER_SIZE + STREAM_BUFFER_SIZE + 2);
util: Uint8Array = new Uint8Array(UTILITY_BUFFER_SIZE);
readLengthFromHeader(): number {
const h: Uint8Array = this.header;
const s0: number = h[0];
const s1: number = h[1];
const s2: number = h[2];
const s3: number = h[3];
return ((s0 | (s1 << 8) | (s2 << 16) | (s3 << 24)) >>> 0);
}
writeBuffer(fd: number, u8: Uint8Array, length: number): void {
let offset: number = 0;
while (offset < length) {
const n: number = SystemIO.write(fd, u8, offset, length - offset);
if (n === 0) throw new Error("write returned 0");
if (typeof n === "number") {
offset = offset + n;
}
}
}
readExactBody(offset: number, length: number): void {
let filled: number = 0;
const view: Uint8Array = this.body;
while (filled < length) {
const n: number = SystemIO.read(STDIN_FD, view, offset + filled, length - filled);
if (n === 0) throw new Error("stdin closed prematurely");
if (typeof n === "number") {
filled = filled + n;
}
}
}
emit(from: number, to: number, prepend: number, append: number): void {
const bodyLength: number = to - from;
const payload: number =
bodyLength + (prepend !== NO_BYTE ? 1 : 0) + (append !== NO_BYTE ? 1 : 0);
const total: number = HEADER_SIZE + payload;
const out: Uint8Array = this.out;
const body: Uint8Array = this.body;
out[0] = payload & 255;
out[1] = (payload >> 8) & 255;
out[2] = (payload >> 16) & 255;
out[3] = (payload >> 24) & 255;
let cursor: number = HEADER_SIZE;
if (prepend !== NO_BYTE) {
out[cursor] = prepend;
cursor++;
}
let src: number = from;
for (let i: number = 0; i < bodyLength; i++) {
out[cursor] = body[src];
cursor++;
src++;
}
if (append !== NO_BYTE) {
out[cursor] = append;
cursor++;
}
this.writeBuffer(STDOUT_FD, out, total);
}
sendChunk(fromIndex: number, toIndex: number): void {
const body: Uint8Array = this.body;
const startByte: number = body[fromIndex];
const endByte: number = body[toIndex - 1];
let actualFrom: number = fromIndex;
let prepend: number = NO_BYTE;
let append: number = NO_BYTE;
if (startByte === OPEN_BRACKET && endByte !== CLOSE_BRACKET) {
append = CLOSE_BRACKET;
} else if (startByte === COMMA) {
actualFrom = fromIndex + 1;
prepend = OPEN_BRACKET;
if (endByte !== CLOSE_BRACKET) {
append = CLOSE_BRACKET;
}
}
this.emit(actualFrom, toIndex, prepend, append);
}
streamIncomingMessage(totalIncomingBytes: number): void {
let remainingBytes: number = totalIncomingBytes;
let bufferPopulation: number = 0;
const body: Uint8Array = this.body;
while (remainingBytes > 0 || bufferPopulation > 0) {
const spaceAvailable: number = STREAM_BUFFER_SIZE - bufferPopulation;
const amountToRead: number =
spaceAvailable < remainingBytes ? spaceAvailable : remainingBytes;
if (amountToRead > 0) {
this.readExactBody(bufferPopulation, amountToRead);
if (typeof amountToRead === "number") {
bufferPopulation = bufferPopulation + amountToRead;
remainingBytes = remainingBytes - amountToRead;
}
}
if (bufferPopulation <= CHUNK_SIZE) {
if (remainingBytes === 0) {
if (bufferPopulation > 0) {
this.sendChunk(0, bufferPopulation);
bufferPopulation = 0;
}
break;
}
continue;
}
const chunkBacktrack: number = CHUNK_SIZE - 8;
const searchStart: number = chunkBacktrack < bufferPopulation ? chunkBacktrack : 0;
let foundComma: number = -1;
for (let i: number = searchStart; i < bufferPopulation; i++) {
if (body[i] === COMMA) {
foundComma = i;
break;
}
}
const splitIndex: number = foundComma !== -1 ? foundComma : CHUNK_SIZE;
this.sendChunk(0, splitIndex);
const remainingInWindow: number = bufferPopulation - splitIndex;
for (let i: number = 0; i < remainingInWindow; i++) {
body[i] = body[splitIndex + i];
}
bufferPopulation = remainingInWindow;
}
}
writeStringToUtilBuffer(str: string): number {
const len: number = str.length;
const u: Uint8Array = this.util;
for (let i: number = 0; i < len; i++) {
const cp: number = str.charCodeAt(i);
u[i] = cp;
}
return len;
}
writeManualErrorJson(msg: string): number {
const u: Uint8Array = this.util;
let cursor: number = 0;
u[cursor++] = 123; // {
u[cursor++] = 34; // "
u[cursor++] = 101; // e
u[cursor++] = 114; // r
u[cursor++] = 114; // r
u[cursor++] = 111; // o
u[cursor++] = 114; // r
u[cursor++] = 34; // "
u[cursor++] = 58; // :
u[cursor++] = 34; // "
const msgLen: number = msg.length;
for (let i: number = 0; i < msgLen; i++) {
const cp: number = msg.charCodeAt(i);
if (cp === 34 || cp === 92) {
u[cursor++] = 92;
}
u[cursor++] = cp;
}
u[cursor++] = 34; // "
u[cursor++] = 125; // }
return cursor;
}
handleCrash(msg: string): never {
const len: number = this.writeManualErrorJson(msg);
this.writeBuffer(STDERR_FD, this.util, len);
const body: Uint8Array = this.body;
const u: Uint8Array = this.util;
for (let i: number = 0; i < len; i++) {
body[i] = u[i];
}
this.sendChunk(0, len);
// satisfies jz WASI runtime mapping
process.exit(1);
// Explicit scalar fallback guarantees a strict 'never' return type bounds context
throw msg;
}
mainProcessingLoop(): number {
const header: Uint8Array = this.header;
for (;;) {
let bytesRead: number = 0;
let filled: number = 0;
while (filled < HEADER_SIZE) {
const n: number = SystemIO.read(STDIN_FD, header, filled, HEADER_SIZE - filled);
if (n === 0) {
bytesRead = filled;
break;
}
if (typeof n === "number") {
filled = filled + n;
}
bytesRead = filled;
}
if (bytesRead === 0) return 0;
if (bytesRead < HEADER_SIZE) return 1;
const totalIncomingBytes: number = this.readLengthFromHeader();
this.streamIncomingMessage(totalIncomingBytes);
}
}
}
function run(): void {
const pipeline: NativeHostPipeline = new NativeHostPipeline();
const initMessage: string = '"TypeScript Native Messaging streaming host"';
const initLen: number = pipeline.writeStringToUtilBuffer(initMessage);
pipeline.writeBuffer(STDERR_FD, pipeline.util, initLen);
const statusCode: number = pipeline.mainProcessingLoop();
if (statusCode === 1) {
pipeline.handleCrash("stdin closed mid-header");
}
}
run(); |
tslang --emit=obj nm_tslang.ts~/bin/wasi-sdk/bin/clang nm_tslang.o -L/media/user/x/TypeScriptCompiler -o nm_tslang.wasm
wasm-ld: error: unknown file type: nm_tslang.o
clang: error: linker command failed with exit code 1 (use -v to see invocation)What's the roadmap here? |
|
Is https://github.com/ASDAlexander77/TypeScript2Cxx built in to this? If not, that would be a usefult option; Static Hermes has |
|
updating support for wasm32 and wasm64 in next release |
|
I observed a bug that now I've encountered twice in Chromium using |
|
FYI https://gist.github.com/guest271314/3ead8bde5922106f6f00226cfa0bebcb |
Yes, it compiles with tslang (tested with v0.0-pre-alpha120 on Windows) after a few small changes:
node:fsis not available, soSystemIOuses the default library's stdio functions, which take a position in the array:stdio_readisfreadon stdin. It waits until it haslenbytes or stdin ends, so it suits this program, which always asks for an exact number of bytes.stdio_write_bytes(fd, ...)takes 1 for stdout and 2 for stderr. Both put the stream in binary mode on Windows.view.__elementRef(offset)gives the address of an element, so reads and writes can start at an offset (aUint8Arrayhere is not a view over anArrayBuffer).process.exit(1)becomesexit(1), withdeclare function ex…