(In the spirit of WebAssembly's High-Level Goals.)
- 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.
- Specify and implement incrementally. Start with a Minimum Viable Product (MVP), then adding additional features, prioritized by feedback and experience.
- Supplement API designs with documentation and tests, and, when feasible, reference implementations which can be shared between wasm engines.
- 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.
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
git clone --branch shermes-wasm https://github.com/tmikov/hermes
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
Follow part of instructions here Building with Emscripten
and here Cross Compilation for building hermes
, shermes
, and hermesc
.
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
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
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
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
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");
}
build-host/bin/shermes -Xenable-tdz -emit-c hello.js
../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
../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
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
#!/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]