emlite
A tiny JS bridge for C/C++ via wasm
Emlite is a tiny JS bridge for native code (C/C++/Rust/Zig) via Wasm, which is agnostic of the underlying toolchain. Thus it can target wasm32-unknown-unknown (freestanding, via stock clang), wasm32-wasi, wasm32-wasip1 and emscripten. It provides a header only library and a single javascript file that allows plain C or C++ code — compiled for wasm — to interoperate with javascript (including the DOM) and other JavaScript objects/runtimes without writing much JS “glue.” It provides both a C api and a higher level C++ api similar to emscripten's val api. The repo also provides higher-level Rust and Zig bindings to emlite. For freestanding builds, it provides a simple bump allocator (invocable via malloc), however this repo also vendors dlmalloc in the src directory. Please check the CMakeLists.txt to see how it's used in the tests and examples.
To use the C++ api, you need a C++-17 capable compiler. To use the C api, a C11 capable compiler should be sufficient.
- Emscripten is a large install (around 1.4 gb), and bundles clang, python, node and java.
- In contrast, if you already have clang installed, wasi-libc's sysroot is around 2.4mb if you're only using Emlite's C api.
- The wasi-sysroot/wasm32-wasi is only 44mb (headers and libraries).
- Even if you install the wasi-sdk, it still is less than 1/4 the size of emscripten.
- Emscripten javascript glue is sometimes difficult to navigate when a problem occurs.
- More established.
- Offers other bundled libraries like SDL, boost and other ports.
- Offers emscripten_sleep which allows better compatibilty for compiling sources containing event-loops like games.
- Automatic translation of OpenGL to WebGL.
- Offers more optimisations by bundling binaryen, wasm-opt and google's Closure compiler.
C++ example:
// define EMLITE_IMPL in only one implementation unit (source file)!
#define EMLITE_IMPL
#include <emlite/emlite.hpp>
using namespace emlite;
EMLITE_USED extern "C" void some_func() {
Console().log(Val("Hello from Emlite"));
auto doc = Val::global("document");
auto body =
doc.call("getElementsByTagName", Val("body"))[0];
auto btn = doc.call("createElement", Val("BUTTON"));
btn.set("textContent", Val("Click Me!"));
btn.call(
"addEventListener",
Val("click"),
Val::make_fn([](auto h) -> Handle {
size_t len = 0;
auto param_vec = Val::vec_from_js_array<Handle>(
Val::take_ownership(h), len
);
Console().log(Val::take_ownership(param_vec[0]));
return Val::undefined().as_handle();
})
);
body.call("appendChild", btn);
}
C example:
// define EMLITE_IMPL in only one implementation unit (source file)!
#define EMLITE_IMPL
#include <emlite/emlite.h>
EMLITE_USED int main() {
em_Val console = em_Val_global("console");
em_Val_call(console, "log", 1, em_Val_from_string("200"));
emlite_reset_object_map();
}
To quickly try out emlite in the browser, create an index.html file: (Note this is not the recommended way to deploy. You should install the required dependencies via npm and use a bundler like webpack to handle bundling, minifying, tree-shaking ...etc).
- Using wasm32-wasi[p1] (wasi-libc, wasi-sysroot, wasi-sdk or emscripten):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { WASI, File, OpenFile, ConsoleStdout } from "https://unpkg.com/@bjorn3/browser_wasi_shim";
import { Emlite } from "https://unpkg.com/emlite";
// or (if you decide to vendor emlite.js)
// import { Emlite } from "./src/emlite.js";
window.onload = async () => {
let fds = [
new OpenFile(new File([])), // 0, stdin
ConsoleStdout.lineBuffered(msg => console.log(`[WASI stdout] ${msg}`)), // 1, stdout
ConsoleStdout.lineBuffered(msg => console.warn(`[WASI stderr] ${msg}`)), // 2, stderr
];
let wasi = new WASI([], [], fds);
let emlite = new Emlite();
let wasm = await WebAssembly.compileStreaming(fetch("./bin/mywasm.wasm"));
let inst = await WebAssembly.instantiate(wasm, {
"wasi_snapshot_preview1": wasi.wasiImport,
"env": emlite.env,
});
emlite.setExports(inst.exports);
// if your C/C++ has a main function, use: `wasi.start(inst)`. If not, use `wasi.initialize(inst)`.
wasi.start(inst);
// test our exported function `add` in tests/dom_test1.cpp works
// window.alert(inst.exports.add(1, 2));
};
</script>
</body>
</html>
- Freestanding The @bjorn3/browser_wasi_shim dependency is not required for freestanding builds:
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module">
import { Emlite } from "https://unpkg.com/emlite";
// or (if you decide to vendor emlite.js)
// import { Emlite } from "./src/emlite.js";
window.onload = async () => {
let emlite = new Emlite();
let wasm = await WebAssembly.compileStreaming(fetch("./bin/mywasm.wasm"));
let inst = await WebAssembly.instantiate(wasm, {
"env": emlite.env,
});
emlite.setExports(inst.exports);
// test our exported function `add` in tests/dom_test1.cpp works
window.alert(inst.exports.add(1, 2));
};
</script>
</body>
</html>
Install emlite via npm:
npm install emlite
In your javascript code:
import { Emlite } from "emlite";
async function main() {
let emlite = new Emlite();
let wasm = await WebAssembly.compileStreaming(fetch("./bin/freestanding/dom_test1_nostdlib.wasm"));
let inst = await WebAssembly.instantiate(wasm, {
env: emlite.env,
});
emlite.setExports(inst.exports);
window.alert(inst.exports.add(1, 2));
}
await main();
You can get emlite from npm:
npm install emlite
Then in your javascript file:
import { Emlite } from "emlite";
import { readFile } from "node:fs/promises";
async function main() {
const emlite = new Emlite();
const wasm = await WebAssembly.compile(
await readFile("./bin/console.wasm"),
);
const instance = await WebAssembly.instantiate(wasm, {
env: emlite.env,
});
emlite.setExports(instance.exports);
// if you have another exported function marked with EMLITE_USED, you can get it in the instance exports
instance.exports.some_func();
}
await main();
To use emlite with wasm32-wasi, wasm32-wasip1 or emscripten** in your web stack, you will need a wasi javascript polyfill, here we use @bjorn3/browser_wasi_shim to provides us with said polyfill:
npm install emlite
npm install @bjorn3/browser_wasi_shim
In your javascript code:
import { Emlite } from "emlite";
import { WASI, File, OpenFile, ConsoleStdout } from "@bjorn3/browser_wasi_shim";
async function main() {
let fds = [
new OpenFile(new File([])), // 0, stdin
ConsoleStdout.lineBuffered(msg => console.log(`[WASI stdout] ${msg}`)), // 1, stdout
ConsoleStdout.lineBuffered(msg => console.warn(`[WASI stderr] ${msg}`)), // 2, stderr
];
let wasi = new WASI([], [], fds);
let emlite = new Emlite();
let wasm = await WebAssembly.compileStreaming(fetch("./bin/dom_test1.wasm"));
let inst = await WebAssembly.instantiate(wasm, {
"wasi_snapshot_preview1": wasi.wasiImport,
"env": emlite.env,
});
emlite.setExports(inst.exports);
// if your C/C++ has a main function, use: `wasi.start(inst)`. If not, use `wasi.initialize(inst)`.
wasi.start(inst);
// test our exported function `add` in tests/dom_test1.cpp works
window.alert(inst.exports.add(1, 2));
}
await main();
** Note that this depends on emscripten's ability to create standalone wasm files, which will also require a wasi shim: https://v8.dev/blog/emscripten-standalone-wasm
You can get emlite from npm:
npm install emlite
Then in your javascript file:
import { Emlite } from "emlite";
import { WASI } from "node:wasi";
import { readFile } from "node:fs/promises";
import { argv, env } from "node:process";
async function main() {
const wasi = new WASI({
version: 'preview1',
args: argv,
env,
});
const emlite = new Emlite();
const wasm = await WebAssembly.compile(
await readFile("./bin/console.wasm"),
);
const instance = await WebAssembly.instantiate(wasm, {
wasi_snapshot_preview1: wasi.wasiImport,
env: emlite.env,
});
wasi.start(instance);
emlite.setExports(instance.exports);
// if you have another exported function marked with EMLITE_USED, you can get it in the instance exports
instance.exports.some_func();
}
await main();
Note that nodejs as of version 22.16 requires a _start function in the wasm module. That can be achieved by defining an int main() {}
function. It's also why we use wasi.start(instance)
in the js module.
You can use CMake's FetchContent to get this repo, otherwise you can just copy the header files into your project.
To build with the wasi-sdk or emscripten, it's sufficient to pass the necessary toolchain file:
cmake -Bbin -GNinja -DCMAKE_TOOLCHAIN_FILE=$EMSCRIPTEN_ROOT/cmake/Modules/Platform/Emscripten.cmake && cmake --build bin
# or
cmake -Bbin -GNinja -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK/share/cmake/wasi-sdk.cmake && cmake --build bin
# You would have to set $EMSCRIPTEN_ROOT or $WASI_SDK accordingly
To build using cmake for freestanding or with wasi-libc or wasi-sysroot, it's preferable to create a cmake toolchain file and pass that to your invocation:
cmake -Bbin -GNinja -DCMAME_TOOLCHAIN_FILE=./my_toolchain_file.cmake
The contents of your toolchain file should be adjust according to your needs. Please check the cmake directory of this repo for examples.
Note that there are certain flags which must be passed to wasm-ld in your CMakeLists.txt file:
set_target_properties(mytarget PROPERTIES LINKER_LANGUAGE CXX SUFFIX .wasm LINK_FLAGS "-Wl,--no-entry,--allow-undefined,--export-all,--import-memory,--export-memory,--strip-all")
Also check the CMakeLists.txt file in the repo to see how the examples and tests are built.
- No need to pass a sysroot, nor a target:
clang++ -Iinclude -o my.wasm main.cpp -Wl,--no-entry,--allow-undefined,--export-all,--import-memory,--export-memory,--strip-all
clang capable of targeting wasm32 is required. If you installed your clang via a package manager, you might require an extra package like libclang-rt-dev-wasm32 (note that it should match the version of your clang install, i.e libclang-rt-18-dev-wasm32). Additionally you might require lld to get wasm-ld. Similarly, it should match your clang version.
- If only using C, you can get the wasi-libc sources from the wasi-libc repo (which requires compiling the source using the given instructions). There are also packages for debian/ubuntu, arch linux, and msys2.
- If using C++ as well, you can grab the wasi-sysroot from the wasi-sdk releases page.
To compile, you'll need to tell clang to target wasm32-wasi (or wasm32-wasip1), and point it to the sysroot you require:
clang++ --target=wasm32-wasi -Iinclude -o my.wasm main.cpp --sysroot /path/to/wasi-sysroot -Wl,--no-entry,--allow-undefined,--export-all,--import-memory,--export-memory,--strip-all
You don't need a sysroot in that case.
You can invoke clang with the -nostdlib
flag:
clang++ --target=wasm32-unknown-unknown -Iinclude -o my.wasm examples/eval.cpp -nostdlib -Wl,--no-entry,--al
low-undefined,--export-all,--import-memory,--export-memory,--strip-all
You can also pass wasm32 as the target, which clang will understand as wasm32-unknown-unknown. As mentioned previously, emlite only includes a simple bump allocator. It's advisable to utilise something like dlmalloc (vendored in the source directory).
To test emlite, you can clone this repo and run it's test suite:
git clone https://github.com/MoAlyousef/emlite
cd emlite
npm install
npm run test_all
npm run serve
Building the tests requires CMake and Ninja.
test_all runs build_tests which builds by default for freestanding. It will also build for wasi-libc, wasi-sysroot, wasi-sdk, and emscripten if the necessary environment variables are set:
- WASI_LIBC
- WASI_SYSROOT
- WASI_SDK
- EMSCRIPTEN_ROOT
It also runs gen_html_tests which genererates the necessary javascript glue code, runs webpack and creates the html files for testing. Each build directory should have an index.html file which has links to the rest of the html files. Running wasm code requires starting a server, which can be done using npm run serve.
Initialize your project:
npm init -y
npm install emlite @bjorn3/browser_wasi_shim
npm install webpack webpack-cli http-server --save-dev
mkdir src
touch src/index.js
touch src/main.cpp
mkdir dist
touch dist/index.html
touch CMakeLists.txt
Modify your package.json:
- Point the entry
main
to./src/index.js
, and change the type to "module":
"main": "src/index.js",
"type": "module",
- Add commands to invoke cmake, webpack and http-server in your
scripts
entry:
"scripts": {
"cmake": "cmake -Bbin -DCMAKE_TOOLCHAIN_FILE=$WASI_SDK/share/cmake/wasi-sdk.cmake && cmake --build bin && cp bin/main.wasm ./dist",
"build": "webpack",
"serve": "http-server ./dist"
},
In your dist/index.html (note that main.js is generated by webpack in the dist folder):
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<script type="module" src="./main.js"></script>
</body>
</html>
In your src/index.js:
import { Emlite } from "emlite";
import { WASI, File, OpenFile, ConsoleStdout } from "@bjorn3/browser_wasi_shim";
async function main() {
let fds = [
new OpenFile(new File([])), // 0, stdin
ConsoleStdout.lineBuffered(msg => console.log(`[WASI stdout] ${msg}`)), // 1, stdout
ConsoleStdout.lineBuffered(msg => console.warn(`[WASI stderr] ${msg}`)), // 2, stderr
];
let wasi = new WASI([], [], fds);
let emlite = new Emlite();
let wasm = await WebAssembly.compileStreaming(fetch("./main.wasm"));
let inst = await WebAssembly.instantiate(wasm, {
"wasi_snapshot_preview1": wasi.wasiImport,
"env": emlite.env,
});
emlite.setExports(inst.exports);
wasi.start(inst);
// call an exported function
// inst.exports.some_func();
}
await main();
In your src/main.cpp:
#define EMLITE_IMPL
#include <emlite/emlite.hpp>
int main() {
emlite::Console().log(emlite::Val("Hello World!"));
}
In your CMakeLists.txt:
cmake_minimum_required(VERSION 3.20)
project(projname)
include(FetchContent)
FetchContent_Declare(
emlite
GIT_REPOSITORY https://github.com/MoAlyousef/emlite.git
GIT_TAG main
GIT_SHALLOW True
)
FetchContent_MakeAvailable(emlite)
add_executable(main src/main.cpp)
target_link_libraries(main PRIVATE emlite::emlite)
set_target_properties(main PROPERTIES LINKER_LANGUAGE CXX SUFFIX .wasm LINK_FLAGS "-Wl,--no-entry,--allow-undefined,--export-all,--import-memory,--export-memory,--strip-all")
Make sure to export WASI_SDK to point to your wasi-sdk directory.
npm run cmake
npm run build
npm run serve