emlite
Emlite is a tiny JS bridge for native Rust code via Wasm. It can be used with either the wasm32-wasip1 or the wasm32-unknown-unknown targets, without the need to bring in tools like emscripten nor wasm-bindgen.
Usage
Add emlite to your Cargo.toml:
[dependencies]
emlite = "0.1.0"
Then you can import and use the Val wrapper and its associated methods:
use emlite::{Console, Val};
fn main() {
let con = Console::get();
con.log(&[Val::from("Hello from Emlite!")]);
}
use emlite::*;
fn main() {
let document = Val::global("document");
let elem = document.call("createElement", &argv!["BUTTON"]);
elem.set("textContent", Val::from("Click"));
let body = document.call("getElementsByTagName", &argv!["body"]).at(0);
elem.call(
"addEventListener",
&argv![
"click",
Val::make_js_function(|ev| {
let console = Val::global("console");
console.call("clear", &[]);
println!("client x: {}", Val::from_handle(ev).get("clientX").as_i32());
println!("hello from Rust");
Val::undefined().as_handle()
})
],
);
body.call("appendChild", &argv![elem]);
}
use emlite::*;
fn main() {
#[allow(non_snake_case)]
let mut AudioContext = Val::global("AudioContext");
if !AudioContext.as_bool() {
println!("No global AudioContext, trying webkitAudioContext");
AudioContext = Val::global("webkitAudioContext");
}
println!("Got an AudioContext");
let context = AudioContext.new(&[]);
let oscillator = context.call("createOscillator", &[]);
println!("Configuring oscillator");
oscillator.set("type", Val::from("triangle"));
oscillator.get("frequency").set("value", Val::from(261.63));
println!("Playing");
oscillator.call("connect", &argv![context.get("destination")]);
oscillator.call("start", &argv![0]);
println!("All done!");
}
Building
For the wasm32-wasip1 target
You need:
To get the rust target:
rustup target add wasm32-wasip1
Running the build, you only need to pass the target to cargo:
cargo build --target=wasm32-wasip1
Passing necessary flags for javascript engines (browser, node ...etc)
The most convenient way to pass extra flags to the toolchain is via a .cargo/config.toml file:
[target.wasm32-wasip1]
rustflags = ["-Clink-args=--no-entry --allow-undefined --export-all --import-memory --export-memory --strip-all"]
[profile.release]
lto = true
For the wasm32-unknown-unknown target
You need:
- wasm32-unknown-unknown target.
To get the rust target:
rustup target add wasm32-unknown-unknown
Passing necessary flags for javascript engines (browser, node ...etc)
The most convenient way to pass extra flags to the toolchain is via a .cargo/config.toml file:
[target.wasm32-unknown-unknown]
rustflags = ["-Clink-args=--no-entry --allow-undefined --export-all --import-memory --export-memory --strip-all"]
[profile.release]
lto = true
Deployment
For the wasip1 target
In the browser
To use it in your web stack, you will need a wasi javascript polyfill, here we use @bjorn3/browser_wasi_shim and the emlite npm packages:
import { WASI, File, OpenFile, ConsoleStdout } from "@bjorn3/browser_wasi_shim";
import { Emlite } from "emlite";
window.onload = async () => {
let fds = [
new OpenFile(new File([])), ConsoleStdout.lineBuffered(msg => console.log(`[WASI stdout] ${msg}`)), ConsoleStdout.lineBuffered(msg => console.warn(`[WASI stderr] ${msg}`)), ];
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);
wasi.start(inst);
window.alert(inst.exports.add(1, 2));
};
With a javascript engine like nodejs
If you're vendoring the emlite.js 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);
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 fn main() {} function. It's also why we use wasi.start(instance) in the js module.
For the wasm32-unknown-unknown target
Targeting the browser
Emlite-rs can be used with Rust's wasm32-unknown-unknown target:
import { Emlite } from "./src/emlite.js";
window.onload = async () => {
let emlite = new Emlite();
let wasm = await WebAssembly.compileStreaming(fetch("./target/wasm32-unknown-unknown/release/examples/audio.wasm"));
let inst = await WebAssembly.instantiate(wasm, {
env: emlite.env,
});
emlite.setExports(inst.exports);
inst.exports.main();
};
Targeting node and other javascript engins
import { Emlite } from "../src/emlite.js";
import { readFile } from "node:fs/promises";
async function main() {
const emlite = new Emlite();
const wasm = await WebAssembly.compile(
await readFile("./bin/eval.wasm"),
);
const instance = await WebAssembly.instantiate(wasm, {
env: emlite.env,
});
emlite.setExports(instance.exports);
instance.exports.main();
}
await main();