with AssemblyScript
AssemblyScript is a project that compiles TypeScript to WebAssembly.
The basic build command:
asc main.ts -o main.wasm
// basically JS with some types: | |
export function add(a: u32, b: u32): u32 { | |
return a + b; | |
} |
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.
.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)
.
For AssemblyScript module's you'll need to set the dialect
option so wasm-ffi will know how to handle strings and arrays.
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]] | |
}, { | |
// need to set option for assemblyscript | |
dialect: 'assemblyscript', | |
]}); | |
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.
import 'allocator/buddy'; | |
export function allocate(size: usize): usize { | |
return allocate_memory(size); | |
} | |
export function deallocate(ptr: usize): void { | |
free_memory(ptr); | |
} |
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')); | |
}); | |
}); |
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.
const library = new Wrapper({ | |
get_sum: ['number', ['array']], | |
}); | |
// ... | |
$('#get-sum').addEventListener('click', () => { | |
const arr = new U32Array([1, 1, 2, 3, 5, 8, 13, 21]); | |
const sum = library.get_sum(arr); | |
$('#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('u32')], | |
pass_pointer: ['number', [types.pointer('u32')]], | |
}); | |
// ... | |
$('#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('u32', 365); | |
const value = library.pass_pointer(ptr); | |
$('#pointer-log').innerText = `Wasm read ${value} from the pointer you sent`; | |
}); |
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 order of the properties should be the same as
your AssemblyScript definition. Use this new Struct
type 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: 'u8', | |
favorite_number: 'u32', | |
}); | |
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: 'u8', | |
favorite_number: 'u32', | |
}); | |
// ... | |
$('#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 AssemblyScript you need to
import the JS function into your wasm module. These functions
need 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: { | |
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 <input>.value and multiplies it by 2 | |
const number = library.multiply_input('body #number'); | |
$('#multiply-log').innerText = `Result: ${number}`; | |
}); |