wasm-ffi usage

C examples using dcodeIO/webassembly

Compiling C to WebAssembly

There are multiple ways to do this, but @dcodeIO's webassembly package is an easy way to get started. It uses a single header to expose a minimal standard library based on musl and dlmalloc.

The basic build command:
wa compile -o main.wasm main.c

#include <webassembly.h>
// macro used to export functions to WebAssembly
export int add(int a, int b) {
return a + b;
}

Memory Leaks

These examples aren't trying to prevent leaks, so when you actually go do something, remember to free memory.

More about how wasm-ffi handles memory in the readme.

Loading .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).

The default configuration of dcodeIO/webassembly requires memory to be imported at the env.memory namespace.

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]]
});
// the dcodeIO/webassembly package needs to import WebAssembly memory before
// instantiating the module. It looks for it at the `env.memory` namespace:
library.imports({
env: {
memory: new WebAssembly.Memory({ initial: 10 }),
},
});
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 JavaScript needs to allocate any memory (passing strings, making structs) you need to expose allocate & deallocate functions.

export void *allocate(int size) {
return malloc(size);
}
export void deallocate(void *ptr) {
free(ptr);
}
Strings

The format for wrapping functions is:
functionName: [returnType, [...argTypes]]

Ex: the say function takes a string pointer and returns a string pointer: ['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'));
});
});
Arrays

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}`;
});
Pointers

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`;
});
Structs

wasm-ffi wraps struct pointers into objects that let you access fields.

To use a struct you first have to define a struct type. The properties should be in the same order as your C source. 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()

 
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);
});
Structs: creating from JavaScript

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);
});
Calling JS functions from C

To call a JS function from C you need to import the JS function into your wasm module.

Most wasm targeting compilers expect external functions to be 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: {
memory: new WebAssembly.Memory({ initial: 10 }),
rotate: function() {
$('body').classList.toggle('rotate');
},
},
});
// ...
$('#barrel-roll').addEventListener('click', () => {
library.barrel_roll();
});
Import wrapping

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 <input>.value and multiplies it by 2
const number = library.multiply_input('body #number');
$('#multiply-log').innerText = `Result: ${number}`;
});