diff --git a/doc/Emscripten.md b/doc/Emscripten.md index d7b10ded11f..08699feaf74 100644 --- a/doc/Emscripten.md +++ b/doc/Emscripten.md @@ -3,68 +3,154 @@ id: emscripten title: Building with Emscripten --- -## Setting up Emscripten +# Compiling to Wasm -To setup Emscripten for building Hermes, we recommend using `emsdk`, which is -the same way Emscripten recommends for most circumstances. -Follow the directions on the -[Emscripten website for `emsdk`](https://emscripten.org/docs/getting_started/downloads.html) -to download the SDK. +## Prerequisites -``` -emsdk install latest -emsdk activate latest -source ./emsdk_env.sh -``` +This guide assumes you are familiar with building Hermes locally and have the necessary +tools installed (e.g., CMake, Ninja, and a compatible toolchain). The instructions are +written for macOS but should work on Linux with minimal adjustments. -If you install `emsdk` at `~/emsdk` and activate `latest`, -then you should use this shell variable for the rest of these instructions: +## Install Emscripten -``` -$EmscriptenRoot = ~/emsdk/upstream/emscripten -``` +Install `emsdk` by following the directions on the [Emscripten website for `emsdk`](https://emscripten.org/docs/getting_started/downloads.html). -If you are using the old `fastcomp` instead, replace `upstream` in the above instruction with `fastcomp`. +Then, inside the installed directory, we need to make sure we have the latest toolchain: -WARNING: The old `fastcomp` backend was [removed in emscripten `2.0.0` (August 2020)](https://emscripten.org/docs/compiling/WebAssembly.html?highlight=fastcomp#backends) +```shell +./emsdk install latest +./emsdk activate latest +``` +## Create a Parent Builds Directory -## Setting up Workspace and Host Hermesc +We need a directory to contain the different Hermes builds: -Hermes now requires a two stage build process because the VM now contains -Hermes bytecode which needs to be compiled by Hermes. +```shell +mkdir ~/hermes-builds +cd ~/hermes-builds +``` -Please follow the [Cross Compilation](./CrossCompilation.md) to set up a workplace -and build a host hermesc at `$HERMES_WS_DIR/build_host_hermesc`. +## Setup Environment Variables +For convenience, we rely on two environment variables throughout this document: +```shell +export Emsdk= +export HermesSourcePath= +``` -# Building Hermes With Emscripten and CMake +## Build Hermes for the Host - cmake -S ${HermesSourcePath?} -B build \ - -DCMAKE_TOOLCHAIN_FILE=${EmscriptenRoot?}/cmake/Modules/Platform/Emscripten.cmake \ - -DCMAKE_BUILD_TYPE=MinSizeRel \ - -DEMSCRIPTEN_FASTCOMP=1 \ - -DCMAKE_EXE_LINKER_FLAGS="-s NODERAWFS=1 -s WASM=0 -s ALLOW_MEMORY_GROWTH=1" - # Build Hermes - cmake --build ./build --target hermes --parallel - # Execute hermes - node bin/hermes.js --help +Hermes uses a two stage build process because parts of Hermes need to be +built using Hermes itself. For the first stage, we need to build Hermes for +the host. We will also use the host build later to compile .js to .c. -In the commands above, replace `${HermesSourcePath?}` with the path where you -cloned Hermes, and `${EmscriptenRoot?}` with the path to your Emscripten -install. +```shell +# From within the hermes-builds directory +cmake -G Ninja -DCMAKE_BUILD_TYPE=Release -S ${HermesSourcePath?} -B build-host +# Build hermes and shermes for the host +cmake --build build-host --target hermesc --target hermes --target shermes --parallel +``` +## Compile the Hermes VM to Wasm + +```shell +# From within the hermes-builds directory +cmake -G Ninja \ + -S ${HermesSourcePath?} -B build-wasm \ + -DCMAKE_TOOLCHAIN_FILE=${Emsdk?}/upstream/emscripten/cmake/Modules/Platform/Emscripten.cmake \ + -DCMAKE_BUILD_TYPE=Release \ + -DIMPORT_HOST_COMPILERS=build-host/ImportHostCompilers.cmake \ + -DHAVE_SYS_IOCTL_H=0 \ + -DCMAKE_EXE_LINKER_FLAGS="-sNODERAWFS=1 -sALLOW_MEMORY_GROWTH=1 -sSTACK_SIZE=256KB" +# Build the VM and libraries +cmake --build build-wasm --target hermes --target shermes --target shermes-dep --parallel +``` -Each option is explained below: +Important options: +* `-G Ninja` is optional but recommended. It instructs CMake to use Ninja as a build tool. * `CMAKE_BUILD_TYPE`: set it to one of CMake's build modes: `Debug`, `Release`, `MinSizeRel`, etc. -* `EMSCRIPTEN_FASTCOMP`: set to `1` if using fastcomp, or `0` if using upstream - (LLVM) -* `WASM`: whether to use asm.js (`0`), WebAssembly (`1`), or both (`2`) +* `-DHAVE_SYS_IOCTL_H=0` is needed for compatibility with the Emscripten runtime environment. * `NODERAWFS`: set to `1` if you will be running Hermes directly with Node. It enables direct access to the filesystem. -* `ALLOW_MEMORY_GROWTH`: whether to pre-allocate all memory, or let it grow over - time +* `ALLOW_MEMORY_GROWTH`: whether to pre-allocate all memory, or let it grow over time -You can customize the build generator by passing the `-G` option to CMake, for -example `-G Ninja`. +Under Emscripten, Hermes relies on a [small amount of JavaScript](../lib/Platform/Unicode/PlatformUnicodeEmscripten.cpp) +to be executed by the Wasm host (like Node.js or a browser). If you intend to run it under a "pure" Wasm host, consider +using this flag: + +* `-DHERMES_UNICODE_LITE=` if set to ON, provides a minimal mostly stubbed-out Unicode implementation. + +> Note that running under a "pure" Wasm host is not described here and will likely require more tweaks. + +Now that the VM is compiled to Wasm, we can examine it: +```shell +ls -l build-wasm/bin/hermes.* +``` + +## Execute Some JavaScript with the Wasm Hermes VM + +Let's create a small .js file: +```shell +echo 'var x = "hello"; console.log(`${x} world`);' > hello.js +``` + +Let's run the example with the Wasm VM: +```shell +node ./build-wasm/bin/hermes.js hello.js +``` + +Let's compile the example to bytecode and then run the bytecode: +```shell +node ./build-wasm/bin/hermes.js hello.js --emit-binary -out hello.hbc +node ./build-wasm/bin/hermes.js hello.hbc +``` + +Finally, let's compare the performance of the Wasm VM with the native one +by running one of the micro-benchmarks that come with Hermes. +```shell +# Run the micro-benchmark with the host VM. +./build-host/bin/hermes -w ${HermesSourcePath?}/benchmarks/bench-runner/resource/test-suites/micros/interp-dispatch.js +# Run it with the Wasm VM +node ./build-wasm/bin/hermes.js -w ${HermesSourcePath?}/benchmarks/bench-runner/resource/test-suites/micros/interp-dispatch.js +``` + +## Compile JavaScript to Wasm + +Now, let's compile JavaScript to Wasm! This is still not directly supported by +the Hermes CLI tools, so we will need a manual step. But no worries, it is easy. + +First, we must make sure we have activated the Emscripten SDK in the current shell. +```shell +emcc --help +``` + +If that runs fine, the SDK is already active. Otherwise, we need to activate it: +```shell +source ${Emsdk?}/emsdk_env.sh +``` +> Note that we didn't need to activate it when we were building with CMake, because +> we used a CMake toolchain file. + +Now we are ready to compile the example js file we created earlier directly to Wasm. +`wasm-compile.sh` is a helper script in the Hermes `utils/` directory. +```shell +${HermesSourcePath?}/utils/wasm-compile.sh build-host build-wasm hello.js +``` + +The compilation generates two files: +- `hello-wasm.wasm`: the compiled Wasm module. Note that this also contains +the entire JS library. +- `hello-wasm.js`: this is the Node.js wrapper to load the Wasm module. + +We can run it: +```shell +node ./hello-wasm.js +``` + +Now let's try compiling the micro-benchmark to Wasm: +```shell +${HermesSourcePath?}/utils/wasm-compile.sh build-host build-wasm \ + ${HermesSourcePath?}/benchmarks/bench-runner/resource/test-suites/micros/interp-dispatch.js +``` diff --git a/utils/wasm-compile.sh b/utils/wasm-compile.sh new file mode 100755 index 00000000000..d3a202b8e2d --- /dev/null +++ b/utils/wasm-compile.sh @@ -0,0 +1,65 @@ +#!/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 + +# Check if exactly one argument is provided +if [[ $# -ne 3 ]]; then + echo "Usage: $0 " + exit 1 +fi + +# Check if HermesSourcePath is set +if [[ -z "${HermesSourcePath+x}" ]]; then + echo "Error: HermesSourcePath is not set." + exit 1 +fi + +# Check if the host build directory is valid +if [[ ! -x $1/bin/shermes ]]; then + echo "Error: '$1' does not contain a bin/shermes executable." + exit 1 +fi +shermes="$1/bin/shermes" + +# Check if the wasm build directory is valid +if [[ ! -f $2/bin/hermes.js ]]; then + echo "Error: '$2' does not contain bin/hermes.js" + exit 1 +fi +wasm_build="$2" + +# Check if the file exists +if [[ ! -f $3 ]]; then + echo "Error: File '$3' does not exist." + exit 1 +fi +# Extract the filename without path and extension +input="$3" +file_name=$(basename "$input") # Remove path +file_name="${file_name%.*}" # Remove extension + +echo "Using shermes to compile $input... to ${file_name}.c" +"$shermes" -emit-c "$input" + +echo "Using emcc to compile ${file_name}.c to ${file_name}.o" +emcc "${file_name}.c" -c \ + -O3 \ + -DNDEBUG \ + -fno-strict-aliasing -fno-strict-overflow \ + -I${wasm_build}/lib/config \ + -I${HermesSourcePath}/include + +echo "Using emcc to link ${file_name}.o to ${file_name}-wasm.js/.wasm" +emcc -O3 ${file_name}.o -o ${file_name}-wasm.js \ + -L${wasm_build}/lib \ + -L${wasm_build}/jsi \ + -L${wasm_build}/tools/shermes \ + -lshermes_console_a -lhermesvm_a -ljsi \ + -sALLOW_MEMORY_GROWTH=1 -sSTACK_SIZE=256KB + +ls -lh ${file_name}-wasm.*