Skip to content

Instantly share code, notes, and snippets.

@guest271314
Last active February 7, 2025 11:24
Show Gist options
  • Save guest271314/b10eac16be88350ffcd19387e22ad4d5 to your computer and use it in GitHub Desktop.
Save guest271314/b10eac16be88350ffcd19387e22ad4d5 to your computer and use it in GitHub Desktop.
Compiling JavaScript to WASM with WASI support using Static Hermes

Compiling JavaScript to WASM with WASI support using Static Hermes

WASI

WASI High Level Goals

(In the spirit of WebAssembly's High-Level Goals.)

  1. Define a set of portable, modular, runtime-independent, and WebAssembly-native APIs which can be used by WebAssembly code to interact with the outside world. These APIs preserve the essential sandboxed nature of WebAssembly through a Capability-based API design.
  2. Specify and implement incrementally. Start with a Minimum Viable Product (MVP), then adding additional features, prioritized by feedback and experience.
  3. Supplement API designs with documentation and tests, and, when feasible, reference implementations which can be shared between wasm engines.
  4. Make a great platform:
    • Work with WebAssembly tool and library authors to help them provide WASI support for their users.
    • When being WebAssembly-native means the platform isn't directly compatible with existing applications written for other platforms, design to enable compatibility to be provided by tools and libraries.
    • Allow the overall API to evolve over time; to make changes to API modules that have been standardized, build implementations of them using libraries on top of new API modules to provide compatibility.

Install dependencies

See Building and Running:

Hermes is a C++17 project. clang, gcc, and Visual C++ are supported. Hermes also requires cmake, git, ICU, Python. It builds with CMake and ninja.

The Hermes REPL will also use libreadline, if available.

To install dependencies on Ubuntu:

apt install build-essential cmake git ninja-build libicu-dev python3 tzdata libreadline-dev

On Arch Linux:

pacman -S cmake git ninja icu python zip readline

On Mac via Homebrew:

brew install cmake git ninja

Python pygments is also a dependency.

sudo apt install python3-pip
python3 -m pip install pygments

Static Hermes

git clone --branch shermes-wasm https://github.com/tmikov/hermes

WASI-SDK

Download SDK packages here..

Or, fetch with wget or curl

wget --show-progress --progress=bar -H -O wasi-sdk.tar.gz \
'https://github.com/WebAssembly/wasi-sdk/releases/download/wasi-sdk-25/wasi-sdk-25.0-x86_64-linux.tar.gz' \
&& tar -xf wasi-sdk.tar.gz \
&& mv wasi-sdk-25.0-x86_64-linux wasi-sdk \
&& rm wasi-sdk.tar.gz 

Building

Follow part of instructions here Building with Emscripten and here Cross Compilation for building hermes, shermes, and hermesc.

Build host

mkdir hermes-builds
cd hermes-builds
export HermesSourcePath=../hermes
export WasiSdk=../wasi-sdk
cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -S ${HermesSourcePath?} -B build-host
cmake --build build-host --target hermesc --target hermes --target shermes --parallel

Compile the Hermes VM to Wasm with WASI support

cmake -G Ninja -S ${HermesSourcePath?} -B build-wasm \
-DCMAKE_BUILD_TYPE=MinSizeRel \
-DCMAKE_TOOLCHAIN_FILE=${WasiSdk?}/share/cmake/wasi-sdk.cmake \
-DIMPORT_HOST_COMPILERS=build-host/ImportHostCompilers.cmake \
-DHERMES_UNICODE_LITE=ON \
-DLLVM_ENABLE_THREADS=0 \
-DHERMES_ALLOW_BOOST_CONTEXT=0 \
-DHERMES_CHECK_NATIVE_STACK=OFF
#  Build the VM and libraries
cmake --build build-wasm --target sh-demo --parallel

JavaScript source code

We're running this JavaScript demo.js compiled to WASM using WASI runtime

function createCounterWithGenerator() {
    let count = 0; // Shared mutable variable

    const increment = (by = 1) => count += by;

    function* doSteps(steps) {
        for (let i = 0; i < steps; i++)
            yield increment(); // Use the default parameter for `by` in `increment`
    }

    return {
        increment,
        doSteps
    };
}

console.log("Closures\n=========");
const counter = createCounterWithGenerator();
const steps = 5;

console.log(`Generating ${steps} increments with default step size:`);
for (const value of counter.doSteps(steps))
    console.log(value);

console.log("Further increments:");
for (const value of counter.doSteps(3))
    console.log(value);

// ===================================================================

function show_tdz() {
    function getval() {
        return val;
    }
    let val = getval() + 1;
}

console.log("\nTDZ\n=========");
try {
    show_tdz();
} catch (e) {
    console.log("TDZ Error!", e.stack);
}

// ===================================================================

const prototypeObj = {
    first: "I am in the prototype"
};

const obj = {
    get second() {
        // Add and increment the `third` property
        if (!this.third) {
            this.third = 1; // Initialize if it doesn't exist
        } else {
            this.third++;
        }
        return `Getter executed, third is now ${this.third}`;
    },

    __proto__: prototypeObj // Set prototype using object literal syntax
};

console.log("\nPrototypical Inheritance\n=========");

console.log("First property:", obj.first); // Inherited from prototype
console.log("Second property:", obj.second); // Triggers the getter
console.log("Third property:", obj.third); // Dynamically added and incremented
console.log("Second property again:", obj.second); // Getter increments third
console.log("Third property now:", obj.third); // Reflects incremented value

// ===================================================================

class PrototypeClass {
    constructor() {}

    // Define `first` as a getter in the prototype
    get first() {
        return "I am in the prototype";
    }
}

class DerivedClass extends PrototypeClass {
    constructor() {
        super();
    }

    get second() {
        // Add and increment the `third` property
        if (!this.third) {
            this.third = 1; // Initialize if it doesn't exist
        } else {
            this.third++;
        }
        return `Getter executed, third is now ${this.third}`;
    }
}

console.log("\nClasses\n=========");

const clInst = new DerivedClass();

console.log("First property:", clInst.first); // Inherited from PrototypeClass
console.log("Second property:", clInst.second); // Triggers the getter
console.log("Third property:", clInst.third); // Dynamically added and incremented
console.log("Second property again:", clInst.second); // Getter increments third
console.log("Third property now:", clInst.third); // Reflects incremented value

Run sh-demo

wasmtime build-wasm/tools/sh-demo/sh-demo

or

wasmer build-wasm/tools/sh-demo/sh-demo

or

 wasmtime compile --optimize opt-level=s build-wasm/tools/sh-demo/sh-demo
 wasmtime --allow-precompiled sh-demo.cwasm
Closures
=========
Generating 5 increments with default step size:
1
2
3
4
5
Further increments:
6
7
8

TDZ
=========

Prototypical Inheritance
=========
First property: I am in the prototype
Second property: Getter executed, third is now 1
Third property: 1
Second property again: Getter executed, third is now 2
Third property now: 2

Classes
=========
First property: I am in the prototype
Second property: Getter executed, third is now 1
Third property: 1
Second property again: Getter executed, third is now 2
Third property now: 2

Emit C from JavaScript source with shermes, compile to WASM with WASI-SDK clang and clang++

Within hermes-builds directory

cat hello.js
var x = "hello"; print(`${x} world`);

I copied cxa.cpp from hermes/tools/sh-demo to the hermes-builds directory from this

/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

#include "llvh/Support/ErrorHandling.h"

extern "C" void __cxa_throw(void* thrown_exception,
    std::type_info* tinfo,
    void (*dest)(void*)) {
  llvh::report_fatal_error("C++ exceptions not supported on Wasi");
}

extern "C" void* __cxa_allocate_exception(size_t) {
  llvh::report_fatal_error("C++ exceptions not supported on Wasi");
}

to this

/*
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

//#include "llvh/Support/ErrorHandling.h"

extern "C" void __cxa_throw() {
  //llvh::report_fatal_error("C++ exceptions not supported on Wasi");
}

extern "C" void* __cxa_allocate_exception() {
  //llvh::report_fatal_error("C++ exceptions not supported on Wasi");
}

Emit C

build-host/bin/shermes -Xenable-tdz -emit-c hello.js

Compile to .o file with WASI-SDK's clang

../wasi-sdk/bin/wasm32-wasi-clang hello.c -c \
-O3 \
-DNDEBUG \
-fno-strict-aliasing -fno-strict-overflow \
-I./build-wasm/lib/config \
-I../hermes/include \
-mllvm -wasm-enable-sjlj 

Compile to .wasm file WASI-SDK's clang++

../wasi-sdk/bin/clang++ -O3 fopen.o cxa.cpp -o fopen.wasm \
-L./build-wasm/lib \
-L./build-wasm/jsi \
-L./build-wasm/tools/shermes \
-lshermes_console_a -lhermesvmlean_a -ljsi -lwasi-emulated-mman 

Test

wasmtime hello.wasm
hello world
wasmer hello.wasm
hello world

Modified fopen.ts from hermes/examples/ffi/fopen.js to read STDIN, test -typed option

/**
 * Copyright (c) Meta Platforms, Inc. and affiliates.
 *
 * This source code is licensed under the MIT license found in the
 * LICENSE file in the root directory of this source tree.
 */

// https://github.com/facebook/hermes/tree/static_h/examples/ffi
"use strict";

// Emulate a module scope, since global scope is unsound.
(function (exports) {
  const c_null = $SHBuiltin.c_null();

  // stdio.h
  const _fopen = $SHBuiltin.extern_c(
    { include: "stdio.h" },
    function fopen(path: c_ptr, mode: c_ptr): c_ptr {
      throw 0;
    },
  );
  const _fclose = $SHBuiltin.extern_c(
    { include: "stdio.h" },
    function fclose(f: c_ptr): void {
    },
  );
  const _fread = $SHBuiltin.extern_c(
    { include: "stdio.h" },
    function fread(
      ptr: c_ptr,
      size: c_size_t,
      nitems: c_size_t,
      stream: c_ptr,
    ): c_size_t {
      throw 0;
    },
  );

  // stdlib.h
  const _malloc = $SHBuiltin.extern_c(
    { include: "stdlib.h" },
    function malloc(size: c_size_t): c_ptr {
      throw 0;
    },
  );
  const _free = $SHBuiltin.extern_c(
    { include: "stdlib.h" },
    function free(p: c_ptr): void {
    },
  );

  // string.h
  const _strerror_r = $SHBuiltin.extern_c(
    { include: "string.h" },
    function strerror_r(errnum: c_int, errbuf: c_ptr, buflen: c_size_t): c_int {
      throw 0;
    },
  );

  // Builtin provided by SH to wrap errno.
  const _sh_errno = $SHBuiltin.extern_c(
    { declared: true },
    function _sh_errno(): c_int {
      throw 0;
    },
  );

  // Pointer access builtins.
  const _ptr_write_char = $SHBuiltin.extern_c(
    { declared: true },
    function _sh_ptr_write_char(ptr: c_ptr, offset: c_int, v: c_char): void {
    },
  );
  const _ptr_read_uchar = $SHBuiltin.extern_c(
    { declared: true },
    function _sh_ptr_read_uchar(ptr: c_ptr, offset: c_int): c_uchar {
      throw 0;
    },
  );

  /// Allocate native memory using malloc() or throw an exception.
  function malloc(size: number): c_ptr {
    "inline";
    "use unsafe";

    let res = _malloc(size);
    if (res === 0) throw Error("OOM");
    return res;
  }

  /// Convert a JS string to ASCIIZ.
  function stringToAsciiz(s: any): c_ptr {
    "use unsafe";

    if (typeof s !== "string") s = String(s);
    let buf = malloc(s.length + 1);
    try {
      let i = 0;
      for (let e = s.length; i < e; ++i) {
        let code: number = s.charCodeAt(i);
        if (code > 127) throw Error("String is not ASCII");
        _ptr_write_char(buf, i, code);
      }
      _ptr_write_char(buf, i, 0);
      return buf;
    } catch (e) {
      _free(buf);
      throw e;
    }
  }

  /// Convert an ASCII string of certain size to a JS string.
  function asciiToString_unsafe(buf: c_ptr, size: number): string {
    let res = "";
    for (let i = 0; i < size; ++i) {
      let ch = _ptr_read_uchar(buf, i);
      // if (ch > 127) throw Error("String is not ASCII");
      res += String.fromCharCode(ch);
    }
    return res;
  }

  /// Convert an ASCIIZ string up to a maximum size to a JS string.
  function asciizToString_unsafe(buf: c_ptr, maxsize: number): string {
    let res = "";
    for (let i = 0; i < maxsize; ++i) {
      let ch = _ptr_read_uchar(buf, i);
      //if (ch > 127) throw Error("String is not ASCII");
      //if (ch === 0) break;
      res += String.fromCharCode(ch);
    }
    return res;
  }

  function strerror(errnum: number): string {
    "use unsafe";

    let errbuf = malloc(1024);
    try {
      _strerror_r(errnum, errbuf, 1024);
      return asciizToString_unsafe(errbuf, 1024);
    } finally {
      _free(errbuf);
    }
  }

  /// Very simple hack to ensure safety.
  let handles: c_ptr[] = [];
  // FIXME: fast array doesn't support .pop() yet.
  let closedHandles = Array();

  function fopen(path: string, mode: string): number {
    "use unsafe";

    let pathz: c_ptr = c_null;
    let modez: c_ptr = c_null;
    try {
      pathz = stringToAsciiz(path);
      modez = stringToAsciiz(mode);
      let filePtr = _fopen(pathz, modez);
      if (!filePtr) {
        let errnum = _sh_errno();
        throw Error(path + ": " + strerror(errnum));
      }
      // Allocate a handle.
      if (closedHandles.length > 0) {
        let f = closedHandles.pop();
        handles[f] = filePtr;
        return f;
      }
      handles.push(filePtr);
      return handles.length - 1;
    } finally {
      _free(pathz);
      _free(modez);
    }
  }

  function fclose(f: number): void {
    "use unsafe";

    if (f < 0 || f >= handles.length) throw Error("invalid file handle");
    if (handles[f]) {
      _fclose(handles[f]);
      handles[f] = c_null;
      closedHandles.push(f);
    }
  }

  function fread(size: number, f: number): string {
    "use unsafe";

    if (f < 0 || f >= handles.length) throw Error("invalid file handle");
    if (!handles[f]) throw Error("file is closed");

    if (size <= 0) throw Error("invalid size");
    let buf = malloc(size);
    try {
      let nr = _fread(buf, 1, size, handles[f]);
      return asciiToString_unsafe(buf, nr);
    } finally {
      _free(buf);
    }
  }

  function freadAll(f: number): string {
    let res = "";
    for (;;) {
      let s = fread(1024, f);
      if (!s) break;
      res += s;
    }
    return res;
  }

  // https://stackoverflow.com/a/34238979

  function array_nth_permutation(a, n) {
    let lex = n;
    let b = Array(); // copy of the set a.slice()
    for (let x = 0; x < a.length; x++) {
      b[x] = a[x];
    }
    let len = a.length; // length of the set
    const res = Array(); // return value, undefined
    let i = 1;
    let f = 1;

    // compute f = factorial(len)
    for (; i <= len; i++) {
      f *= i;
    }

    let fac = f;
    // if the permutation number is within range
    if (n >= 0 && n < f) {
      // start with the empty set, loop for len elements
      // let result_len = 0;
      for (; len > 0; len--) {
        // determine the next element:
        // there are f/len subsets for each possible element,
        f /= len;
        // a simple division gives the leading element index
        i = (n - n % f) / f; // Math.floor(n / f);
        // alternately: i = (n - n % f) / f;
        // res[(result_len)++] = b[i];
        // for (let j = i; j < len; j++) {
        //   b[j] = b[j + 1]; // shift elements left
        // }
        res.push(b.splice(i, 1)[0]);
        // reduce n for the remaining subset:
        // compute the remainder of the above division
        n %= f;
        // extract the i-th element from b and push it at the end of res
      }
      return `${lex} of ${fac - 1} (0-indexed, factorial ${fac}) => ${
        JSON.stringify(res)
      }`;
    } else {
      if (n === 0) {
        return `${JSON.stringify(res)}\n`;
      }
      return `${n} >= 0 && ${n} < ${f}: ${n >= 0 && n < f}`;
    }
  }

  let f = fopen("/dev/stdin", "r");
  try {
    let [input, lex] = String(freadAll(f)).split(" ").map(Number);
    if (input < 2 || lex < 0) {
      print(`Expected n > 2, m >= 0, got ${input}, ${lex}\n`); // eval(input)
      return 1;
    } else {
      print(
        array_nth_permutation(Array.from({ length: input }, (_, i) => i), lex),
      );
      return 0;
    }
  } finally {
    fclose(f);
  }

  // Optionally force some methods to be emitted for debugging.
  // exports.foo = foo;
})({});
build-host/bin/shermes -Xenable-tdz -emit-c -typed fopen.ts

Repeat compilations step with WASI-SDK. Test with wasmtime

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

Caused by:
    0: failed to invoke command default
    1: error while executing at wasm backtrace:
           0: 0x130892 - fopen.wasm!abort
           1: 0xa5413 - fopen.wasm!_sh_throw_current
           2: 0xa58a7 - fopen.wasm!_sh_throw
           3: 0x548b - fopen.wasm!_7_fopen
           4: 0x2b92 - fopen.wasm!_1__1_
           5: 0x25a4 - fopen.wasm!_0_global
           6: 0xb950a - fopen.wasm!sh_unit_run(SHRuntime*, SHUnit*)
           7: 0xb9402 - fopen.wasm!_sh_unit_init
           8: 0xb9120 - fopen.wasm!_sh_unit_init_guarded
           9: 0xb9044 - fopen.wasm!_sh_initialize_units
          10: 0x34c2 - fopen.wasm!main
          11: 0x13065e - fopen.wasm!__main_void
          12: 0x239c - fopen.wasm!_start
       note: using the `WASMTIME_BACKTRACE_DETAILS=1` environment variable may show more debugging information
    2: wasm trap: wasm `unreachable` instruction executed

Shell script

#!/bin/bash
# Copyright (c) Meta Platforms, Inc. and affiliates.
#
# This source code is licensed under the MIT license found in the
# LICENSE file in the root directory of this source tree.

set -e  # Exit immediately if a command exits with a non-zero status
set -u  # Treat unset variables as an error and exit immediately

# Extract the filename without path and extension
file_name=$(basename "$1")   # Remove path
file_name="${file_name%.*}"      # Remove extension

rm -rf out

mkdir out

out="$PWD/out"

rm -rf ${file_name}.c ${file_name}.o ${file_name}.wasm ${file_name}.hbc ${file_name}

./build-host/bin/hermes ${file_name}.js --emit-binary -out "${out}/${file_name}.hbc"

./build-host/bin/shermes -v -Os -g -static-link -Xenable-tdz -emit-c "${file_name}.js" -o "${out}/${file_name}.c"

./build-host/bin/shermes -v -Os -g -Xenable-tdz ${file_name}.js -o "${out}/${file_name}" 

../wasi-sdk/bin/wasm32-wasi-clang "${out}/${file_name}.c" -c \
  -O3 \
  -DNDEBUG \
  -fno-strict-aliasing -fno-strict-overflow \
  -I./build-wasm/lib/config \
  -I../hermes/include \
  -mllvm -wasm-enable-sjlj \
  -Wno-c23-extensions \
  -o "${out}/${file_name}.o"

../wasi-sdk/bin/clang++ -O3 "${out}/${file_name}.o" ./build-wasm/tools/sh-demo/CMakeFiles/sh-demo.dir/cxa.cpp.obj -o "${out}/${file_name}.wasm" \
  -L./build-wasm/lib \
  -L./build-wasm/jsi \
  -L./build-wasm/tools/shermes \
  -lshermes_console_a -lhermesvmlean_a -ljsi -lwasi-emulated-mman

../wasi-sdk/bin/strip "${out}/${file_name}.wasm"

ls -lh "${out}"
./wasm-standalone-test.sh hello.js
/usr/bin/cc /tmp/hello.js-c2d6b3.c -Os -I/media/user/123/hermes-builds/build-host/lib/config -I/media/user/123/hermes/include -DNDEBUG -g -fno-strict-aliasing -fno-strict-overflow -L/media/user/123/hermes-builds/build-host/lib -L/media/user/123/hermes-builds/build-host/jsi -L/media/user/123/hermes-builds/build-host/tools/shermes -lshermes_console -Wl,-rpath /media/user/123/hermes-builds/build-host/lib -Wl,-rpath /media/user/123/hermes-builds/build-host/jsi -Wl,-rpath /media/user/123/hermes-builds/build-host/tools/shermes -lm -lhermesvm -o /media/user/123/hermes-builds/out/hello
In file included from /media/user/123/hermes-builds/out/hello.c:2:
../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 1.6M
-rwxrwxr-x 1 user user  29K Jan  4 14:23 hello
-rw-rw-r-- 1 user user  12K Jan  4 14:23 hello.c
-rw-rw-r-- 1 user user  700 Jan  4 14:23 hello.hbc
-rw-rw-r-- 1 user user 4.3K Jan  4 14:23 hello.o
-rwxrwxr-x 1 user user 1.5M Jan  4 14:23 hello.wasm

After implementing reading stdin using getchar(), see Reading stdin in Static Hermes

  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: number = _getchar();
        if (int === -1) {
          break;
        }
        c += String.fromCodePoint(int);
      }
      return String(c).trim();
    } catch (e) {
      return String(e.message);
    }
  }
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