With Rust & --target=wasm32-unknown-unknown
The wasm32 target is available now in nightly and soon to be
in stable. You can build a project with it using cargo's target flag:
cargo build --target=wasm32-unknown-unknown
This will build a .wasm
module in the target folder, so
you'll probably want to move a copy of it out after each build:
cp ./target/wasm32-unknown-unknown/debug/example.wasm ...
In order to make a Rust function accessible from a WebAssembly module
you need to make it public (pub
) and annotate it with
#[no_mangle]
cdylib
. You can do this in your Cargo.toml.
A couple tips depending on what version of rust you are using:
opt-level
>= 2.
You can set this for debug builds in your Cargo.toml.
RUSTFLAGS=-C debuginfo=2
when
building with --release
if you want to retain
debuging info / have better error stack traces.
If you want more examples dealing with foreign function interfaces in Rust, check out the Rust FFI Omnibus. Its a good resource that goes in detail about the Rust side of things.
#[no_mangle] pub fn add(a: u32, b: u32) -> u32 { a + b }
[lib] crate-type = ["cdylib"] path = "./main.rs" [profile.dev] opt-level = 2
The examples below aren't even trying to be memory safe, so when you actually go to implement something, be careful!
Read more about how wasm-ffi handles memory in the readme.
.wasm
modules
To load a .wasm
module, you need to fetch it from
the server and then load it with
WebAssembly.instantiateStreaming
.
You can do that yourself or wasm-ffi
will do it with
.fetch(url)
.
import { Wrapper } from 'wasm-ffi'; // WebAssembly function you want to wrap will go here const library = new Wrapper({ // do_thing: [return_type, [arg_type_1. arg_type_2]] }); library.fetch('main.wasm').then(() => { // library is now loaded and you can call your wrapped WebAssembly functions: // library.do_thing() });
.wasm
Requirements
wasm-ffi
can read data from WebAssembly memory without
any changes to your module. You can read strings, read struct fields,
modify existing struct fields, etc.
If your JavaSript needs to allocate any memory, though, you need to
expose allocate
/ deallocate
functions.
If you want to pass a string to WebAssembly, or create a new struct
you'll need this.
// these are crafty way to malloc/free in stable rust: #[no_mangle] pub fn allocate(length: usize) -> *mut c_void { let mut v = Vec::with_capacity(length); let ptr = v.as_mut_ptr(); std::mem::forget(v); ptr } #[no_mangle] pub fn deallocate(ptr: *mut c_void, length: usize) { unsafe { std::mem::drop(Vec::from_raw_parts(ptr, 0, length)); } }
The format for wrapping functions is:
functionName: [returnType, [...argTypes]]
say
takes a string pointer and returns a string pointer:
say: ['string', ['string']]
// wrap our exported C function `say`: const library = new Wrapper({ say: ['string', ['string']], }); library.fetch('main.wasm').then(() => { // call `say` on btn click: $('#say-hello').addEventListener('click', () => { alert(library.say('Hello')); }); });
wasm-ffi
supports ArrayBuffers & TypedArrays
as an argument type. It will write the arrays to memory and
pass the pointer to your .wasm
function.
By default the array will be freed after the function returns.
// wrap `get_sum` function: 2 arguments, array pointer & array length const library = new Wrapper({ get_sum: ['number', ['array', 'number']], }); // ... $('#get-sum').addEventListener('click', () => { const arr = new Uint32Array([1, 1, 2, 3, 5, 8, 13, 21]); const sum = library.get_sum(arr, arr.length); $('#sum-log').innerText = `Sum of ${arr} is: ${sum}`; });
A Pointer
object encapsulates a reference to wasm memory.
.ref()
: get memory address.deref()
: get data.set(value)
: set pointers value.free()
: free from wasm memory
Create a new pointer with new Pointer(type, value)
.
import { types, Pointer } from 'wasm-ffi'; const library = new Wrapper({ get_pointer: [types.pointer('uint32')], pass_pointer: ['number', [types.pointer('uint32')]], }); // ... $('#get-pointer').addEventListener('click', () => { const ptr = library.get_pointer(); $('#pointer-log').innerText = `Value ${ptr.deref()} is located @ ${ptr.ref()}`; }); $('#pass-pointer').addEventListener('click', () => { const ptr = new ffi.Pointer('uint32', 365); const value = library.pass_pointer(ptr); $('#pointer-log').innerText = `Wasm read ${value} from the pointer you sent`; });
When you have a function that accepts or returns a struct pointer
wasm-ffi
will wrap that pointer into an object that
is easier to use.
To use a struct you first have to define a struct type. The order of the properties should be the same as how you have it defined in C. Use this struct definition in your wrapped function signatures.
You can get the memory address of a struct with .ref()
and you can free it from memory with .free()
(Remember to use #[repr(C)]
on your structs in Rust.)
import { Struct } from 'wasm-ffi'; // define a new struct type: Person const Person = new Struct({ name: 'string', age: 'uint8', favorite_number: 'uint32', }); const library = new Wrapper({ get_person: [Person], person_facts: ['string', [Person]], }); // ... $('#get-person').addEventListener('click', () => { const p = library.get_person(); const about = `${p.name} is ${p.age}. His favorite number is ${p.favorite_number}.`; $('#person-log').innerText = about; }); $('#modify-person').addEventListener('click', () => { // modify the properties of a struct: const p = library.get_person(); p.age = 255; p.favorite_number = 100; $('#person-log').innerText = `New age: ${p.age}\n`; $('#person-log').innerText += `New favorite: ${p.favorite_number}\n`; $('#person-log').innerText += library.person_facts(p); });
A struct definition (like above) is also a constructor you can use to make new structs from JS. Any struct made in JS will get written to memory the first time it is used in a WebAssembly function.
const Person = new Struct({ name: 'string', age: 'uint8', favorite_number: 'uint32', }); // ... $('#make-person').addEventListener('click', () => { const name = $('#name').value; const age = parseInt($('#age').value); const num = parseInt($('#num').value); const person = new Person({ name: name, age: age, favorite_number: num, }); $('#make-log').innerText = library.person_facts(person); });
To call a JS function from Rust you need to import the JS function
into your wasm module. These functions need to be placed in the
env
namespace.
// `barrel_roll` will call the `rotate` function below const library = new Wrapper({ barrel_roll: [], // no arguments }); // this needs to be done before `library.fetch('main.wasm')` library.imports({ env: { rotate: function() { $('body').classList.toggle('rotate'); }, }, }); // ... $('#barrel-roll').addEventListener('click', () => { library.barrel_roll(); });
You can also wrap imported functions using the same notation as with
a Wrapper
. The last argument is the function to wrap. This
will convert the inputs and outputs of the function to the right types.
// `multiply_input` will call `get_input_value` below: const library = new Wrapper({ multiply_input: ['number', ['string']], }); // change imports to a callback to expose the wrap fn // (remember to return an object here with the extra parenthesis: `({ ... })`) library.imports(wrap => ({ env: { // ... // `get_input_value` takes a string and returns a number get_input_value: wrap(['number', ['string']], (selector) => { return parseInt($(selector).value); }), }, })); // ... $('#multiply').addEventListener('click', () => { // gets the .value and multiplies it by 2 var number = library.multiply_input('body #number'); $('#multiply-log').innerText = `Result: ${number}`; });