Skip to content

Commit

Permalink
Update doc/Emscripten.md
Browse files Browse the repository at this point in the history
Summary:
Update the Emscripten instruction for Static Hermes and include
documentation how to compile JS to Wasm.

Reviewed By: fbmal7

Differential Revision: D67594748

fbshipit-source-id: a35f1b3b9c590fa44e8b713a02b6c1d18441479e
  • Loading branch information
Tzvetan Mikov authored and facebook-github-bot committed Dec 23, 2024
1 parent c8a72e4 commit f1cf847
Show file tree
Hide file tree
Showing 2 changed files with 195 additions and 44 deletions.
174 changes: 130 additions & 44 deletions doc/Emscripten.md
Original file line number Diff line number Diff line change
Expand Up @@ -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=<path-to-emsdk-directory>
export HermesSourcePath=<path-to-hermes-source-directory>
```

# 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
```
65 changes: 65 additions & 0 deletions utils/wasm-compile.sh
Original file line number Diff line number Diff line change
@@ -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 <path-to-host-build> <path-to-wasm-build> <file>"
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.*

0 comments on commit f1cf847

Please sign in to comment.