Skip to content

Instantly share code, notes, and snippets.

@guest271314
Created January 7, 2025 08:24
Show Gist options
  • Save guest271314/fec412134903e50521c12370edc88ef3 to your computer and use it in GitHub Desktop.
Save guest271314/fec412134903e50521c12370edc88ef3 to your computer and use it in GitHub Desktop.
Reading stdin in Static Hermes

Facebook's Static Hermes doesn't provide a way to read stdin by default. Does hermes expose the capability to read standard input and write standard output? #1469.

CommonJS support was removed. ECMAScript Modules are not yet supported. We can't use imported files to facilitate reading from standard input. There's no process.argv to read arguments passed to the program.

There's static_h/examples/ffi/fopen.js which opens and reads a flat file.

Modifying that code to read from /dev/stdin on Linux

// ...
let f = fopen("/dev/stdin", "r");
print(String(freadAll(f)).trim());
echo '4 5' | shermes -exec -typed fopen.ts
4 5

That works!

However, WASI doesn't support reading /dev/stdin. Using JavaScript compiled to WASM with WASI-SDK using the above code wasmer parses the "/dev/stdin" reference and reads from stdin. wasmtime doesn't, and throws an error.

echo '4 5' |  wasmtime out/fopen.wasm
Error: failed to run main module `out/fopen.wasm`

Caused by:
    0: failed to invoke command default
    1: error while executing at wasm backtrace:
           0: 0x130e14 - <unknown>!<wasm function 4066>
           1: 0xa5995 - <unknown>!<wasm function 2511>
           2: 0xa5e29 - <unknown>!<wasm function 2520>
           3: 0x5884 - <unknown>!<wasm function 33>
           4: 0x2be4 - <unknown>!<wasm function 24>
           5: 0x25bd - <unknown>!<wasm function 23>
           6: 0xb9a8c - <unknown>!<wasm function 2934>
           7: 0xb9984 - <unknown>!<wasm function 2933>
           8: 0xb96a2 - <unknown>!<wasm function 2932>
           9: 0xb95c6 - <unknown>!<wasm function 2931>
          10: 0x35ac - <unknown>!<wasm function 25>
          11: 0x130be0 - <unknown>!<wasm function 4044>
          12: 0x2393 - <unknown>!<wasm function 21>
    2: wasm trap: wasm `unreachable` instruction executed

Fortunately somebody left us some hints and breadcrumbs to follow to fix whatever is not working here with wasmtime.

Error: failed to invoke command default. wasmer executes the same file #9922

WASI doesn't have /dev nor /dev/stdin. Looks like Wasmer adds /dev/stdin as non-standard extension: https://github.com/wasmerio/wasmer/blob/1d03406174b204ac403f61e2deb0a41601c8e41b/lib/virtual-fs/src/builder.rs#L106-L110 Instead of using fopen("/dev/stdin") you can use the stdin global variable instead. This is of type FILE * just like the return value of fopen. You just have to make sure not to fclose it.

The Hermes (Static) documentation is not exhaustive about FFI usage.

This signature expects a function. How do we define stdin in the Static Hermes JavaScript context, even using FFI?

  // stdio.h
  const _fopen = $SHBuiltin.extern_c(
    { include: "stdio.h" },
    function fopen(path: c_ptr, mode: c_ptr): c_ptr {
      throw 0;
    },
  );

We have access to the C API with $SHBuiltin.extern_c. In WASI-SDK /share/wasi-sysroot/include/wasm32-wasi/stdio.h we can see what we can use to get the same result using wasmer and wasmtime.

// ...
extern FILE *const stdin;
extern FILE *const stdout;
extern FILE *const stderr;

#define stdin  (stdin)
#define stdout (stdout)
#define stderr (stderr)

FILE *fopen(const char *__restrict, const char *__restrict);
FILE *freopen(const char *__restrict, const char *__restrict, FILE *__restrict);
int fclose(FILE *);

int remove(const char *);
int rename(const char *, const char *);

int feof(FILE *);
int ferror(FILE *);
int fflush(FILE *);
void clearerr(FILE *);

int fseek(FILE *, long, int);
long ftell(FILE *);
void rewind(FILE *);

int fgetpos(FILE *__restrict, fpos_t *__restrict);
int fsetpos(FILE *, const fpos_t *);

size_t fread(void *__restrict, size_t, size_t, FILE *__restrict);
size_t fwrite(const void *__restrict, size_t, size_t, FILE *__restrict);

int fgetc(FILE *);
int getc(FILE *);
int getchar(void);
int ungetc(int, FILE *);
// ...

Let's see what we can come up with to solve the issue we encountered.

In ISO/IEC 9899:TC3 Committee Draft September 7, 2007 WG14/N1256 there's

7.19.7.6 [The getchar function]

Synopsis

    #include <stdio.h>
     int getchar(void);

Description

The getchar function is equivalent to getc with the argument stdin.

Returns

The getchar function returns the next character from the input stream pointed to by stdin. If the stream is at end-of-file, the end-of-file indicator for the stream is set and getchar returns EOF. If a read error occurs, the error indicator for the stream is set and getchar returns EOF.

No arguments. We don't have to (try to) define stdin in Static Hermes context to use fgets.

Let's test this option

const _getchar = $SHBuiltin.extern_c({
    include: "stdio.h"
  },
  function getchar(): c_int {
    throw 0;
  },
);

function getchars(): string {
  "use unsafe";
  try {
    let n: number = 50;
    let c: string = String();
    while (--n) {
      let int = _getchar();
      if (int === -1) {
        break;
      }
      c += String.fromCodePoint(int);
      print("int:", int);
      print("string:", c);
    }
    return String(c).trim();
  } catch (e) {
    return String(e.message);
  }
}

let stdin = getchars();

print(stdin);
echo '4 5' | shermes -exec -typed fopen-dev-stdin.ts
int: 52
string: 4
int: 32
string: 4 
int: 53
string: 4 5
int: 10
string: 4 5

4 5

Works so far.

Test the code with wasmtime, that errored when using /dev/stdin

./cross-compile-js.sh fopen.ts
usr/bin/cc /tmp/fopen.ts-556939.c -Os ...
../hermes/include/hermes/VM/static_h.h:334:2: warning: "JS exceptions are currenly broken with WASI" [-W#warnings]
  334 | #warning "JS exceptions are currenly broken with WASI"
      |  ^
1 warning generated.
total 17M
-rwxrwxr-x 1 user user  69K Jan  7 00:10 fopen
-rw-rw-r-- 1 user user   44K Jan  7 00:10 fopen.c
-rw-rw-r-- 1 user user   12K Jan  7 00:10 fopen.o
-rw-rw-r-- 1 user user  2.9K Jan  7 00:10 fopen.ts
-rwxrwxr-x 1 user user  1.5M Jan  7 00:10 fopen.wasm
-rw-rw-r-- 1 user user   15M Jan  7 00:10 fopen.wat

Check wasmer again

echo '4 5' | wasmer ./out/fopen.wasm
5 of 23 (0-indexed, factorial 24) => [0,3,2,1]

Check native executable compiled by shermes

echo '7 8' | ./out/fopen
8 of 5039 (0-indexed, factorial 5040) => [0,1,2,4,5,3,6]

Check shermes

echo '12 2' | shermes -exec -typed ./out/fopen.ts
2 of 479001599 (0-indexed, factorial 479001600) => [0,1,2,3,4,5,6,7,8,10,9,11]

Check wasmtime

echo '9 32' | wasmtime ./out/fopen.wasm
32 of 362879 (0-indexed, factorial 362880) => [0,1,2,3,5,6,7,4,8]
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment