Wasmex is an Elixir library for executing WebAssembly binaries:
- Easy to use: The
wasmex
API mimics the standard WebAssembly API, - Fast:
wasmex
executes the WebAssembly modules as fast as possible, - Safe: All calls to WebAssembly will be fast and completely safe and sandboxed.
It uses wasmer to execute WASM binaries through a NIF. We use Rust to implement the NIF to make it as safe as possible.
The package can be installed by adding wasmex
to your list of
dependencies in mix.exs
:
def deps do
[
{:wasmex, "~> 0.3.0"}
]
end
Note: Rust is required to install the Elixir library (Cargo — the build tool for Rust — is used to compile the extension). See how to install Rust.
The docs can be found at https://hexdocs.pm/wasmex.
There is a toy WASM program in test/wasm_test/src/lib.rs
, written in Rust (but could potentially be any other language that compiles to WebAssembly).
It defines many functions we use for end-to-end testing, but also serves as example code. For example:
#[no_mangle]
pub extern fn sum(x: i32, y: i32) -> i32 {
x + y
}
Once this program compiled to WebAssembly (which we do every time when running tests), we end up with a test/wasm_test/target/wasm32-unknown-unknown/debug/wasmex_test.wasm
binary file.
This WASM file can be executed in Elixir:
{:ok, bytes } = File.read("wasmex_test.wasm")
{:ok, instance } = Wasmex.start_link(bytes)
{:ok, [42]} == Wasmex.call_function(instance, "sum", [50, -8])
Instantiates a WebAssembly module represented by bytes and allows calling exported functions on it:
# Get the WASM module as bytes.
{:ok, bytes } = File.read("wasmex_test.wasm")
# Instantiates the WASM module.
{:ok, instance } = Wasmex.start_link(bytes)
# Call a function on it.
{:ok, [result]} = Wasmex.call_function(instance, "sum", [1, 2])
IO.puts result # 3
All exported functions are callable via Wasmex.call_function
.
Arguments of these functions are automatically casted to WebAssembly values.
Note that WebAssembly only knows number datatypes (floats and integers of various sizes).
You can pass arbitrary data to WebAssembly, though, by writing this data into its memory. Wasmex.memory
returns a Memory
struct representing the memory of that particular instance, e.g.:
{:ok, memory} = Wasmex.memory(instance, :uint8, 0)
See below for more information.
Wasmex currently supports importing functions only. We wish to support globals and tables in the future and appreciate any contributions in that direction.
To pass a function into a WASM module, an imports map
must be provided:
imports = %{
env: %{
sum3: {:fn, [:i32, :i32, :i32], [:i32], fn (_context, a, b, c) -> a + b + c end},
}
}
instance = start_supervised!({Wasmex, %{bytes: @import_test_bytes, imports: imports}})
{:ok, [6]} = Wasmex.call_function(instance, "use_the_imported_sum_fn", [1, 2, 3])
The imports object is a map of namespaces.
In the example above, we import the "env"
namespace.
Each namespace is, again, a map listing imports.
Under the name sum3
, we imported a function which is represented with a tuple of:
- the import type:
:fn
(a function), - the functions parameter types:
[:i32, :i32, :i32]
, - the functions return types:
[:i32]
, and - a function reference:
fn (_context, a, b, c) -> a + b + c end
When the WASM code executes the sum3
imported function, the execution context is forwarded to
the given function reference.
The first param is always the call context (containing e.g. the instances memory).
All other params are regular parameters as specified by the parameter type list.
Valid parameter/return types are:
i32
a 32 bit integeri64
a 64 bit integerf32
a 32 bit floatf64
a 64 bit float
A WebAssembly instance has its own memory, represented by the Memory
struct.
It is accessible by the Wasmex.memory
getter.
The Memory.grow
methods allows to grow the memory by a number of pages (of 65kb each).
Wasmex.Memory.grow(memory, 1)
The current size of the memory can be obtained with the length
method:
Wasmex.Memory.length(memory) # in bytes, always a multiple of the the page size (65kb)
When creating the memory struct, an offset
param can be provided to subset the memory array at a particular offset.
offset = 7
index = 4
value = 42
{:ok, memory} = Wasmex.memory(instance, :uint8, offset)
Wasmex.Memory.set(memory, index, value)
IO.puts Wasmex.Memory.get(memory, index) # 42
The Memory
struct views the WebAssembly memory of an instance as an array of values of different types.
Possible types are: uint8
, int8
, uint16
, int16
, uint32
, and int32
.
The underlying data is not changed when viewed in different types - its just its representation that changes.
View memory buffer as a sequence of… | Bytes per element |
---|---|
int8 |
1 |
uint8 |
1 |
int16 |
2 |
uint16 |
2 |
int32 |
4 |
uint32 |
4 |
This can also be resolved at runtime:
{:ok, memory} = Wasmex.memory(instance, :uint16, 0)
Wasmex.Memory.bytes_per_element(memory) # 2
Since the same memory seen in different data types uses the same buffer internally. Let's have some fun:
int8 = Wasmex.memory(instance, :int8, 0)
int16 = Wasmex.memory(instance, :int16, 0)
int32 = Wasmex.memory(instance, :int32, 0)
b₁
┌┬┬┬┬┬┬┐
Memory.set(int8, 0, 0b00000001)
b₂
┌┬┬┬┬┬┬┐
Memory.set(int8, 1, 0b00000100)
b₃
┌┬┬┬┬┬┬┐
Memory.set(int8, 2, 0b00010000)
b₄
┌┬┬┬┬┬┬┐
Memory.set(int8, 3, 0b01000000)
# Viewed in `int16`, 2 bytes are read per value
b₂ b₁
┌┬┬┬┬┬┬┐ ┌┬┬┬┬┬┬┐
assert 0b00000100_00000001 == Memory.get(int16, 0)
b₄ b₃
┌┬┬┬┬┬┬┐ ┌┬┬┬┬┬┬┐
assert 0b01000000_00010000 == Memory.get(int16, 1)
# Viewed in `int32`, 4 bytes are read per value
b₄ b₃ b₂ b₁
┌┬┬┬┬┬┬┐ ┌┬┬┬┬┬┬┐ ┌┬┬┬┬┬┬┐ ┌┬┬┬┬┬┬┐
assert 0b01000000_00010000_00000100_00000001 == Memory.get(int32, 0)
Strings can not directly be used as parameters or return values when calling WebAssembly functions since WebAssembly only knows number data types. But since Strings are just "a bunch of bytes" we can write these bytes into memory and give our WebAssembly function a pointer to that memory location.
Given we have the following Rust function that returns the first byte of a given string in our WebAssembly (note: this is copied from our test code, have a look there if you're interested):
#[no_mangle]
pub extern "C" fn string_first_byte(bytes: *const u8, length: usize) -> u8 {
let slice = unsafe { slice::from_raw_parts(bytes, length) };
match slice.first() {
Some(&i) => i,
None => 0,
}
}
Let's see how we can call this function from Elixir:
bytes = File.read!(TestHelper.wasm_test_file_path)
{:ok, instance} = Wasmex.start_link(bytes)
{:ok, memory} = Wasmex.memory(instance, :uint8, 0)
index = 42
string = "hello, world"
Wasmex.Memory.write_binary(memory, index, string)
# 104 is the letter "h" in ASCII/UTF-8 encoding
{:ok, [104]} == Wasmex.call_function(instance, "string_first_byte", [index, String.length(string)])
Please not that Elixir and Rust assume Strings to be valid UTF-8. Take care when handling other encodings.
Given we have the following Rust function in our WebAssembly (copied from our test code):
#[no_mangle]
pub extern "C" fn string() -> *const u8 {
b"Hello, World!".as_ptr()
}
This function returns a pointer to its memory. This memory location contains the String "Hello, World!" (ending with a null-byte since in C-land all strings end with a null-byte to mark the end of the string).
This is how we would receive this String in Elixir:
bytes = File.read!(TestHelper.wasm_test_file_path)
{:ok, instance} = Wasmex.start_link(bytes)
{:ok, memory} = Wasmex.memory(instance, :uint8, 0)
{:ok, [pointer]} = Wasmex.call_function(instance, "string", [])
returned_string = Wasmex.Memory.read_string(memory, pointer, 13) # "Hello, World!"
Please note that bytes are treated in little-endian, as required by the WebAssembly specification, Chapter Structure, Section Instructions, Sub-Section Memory Instructions:
All values are read and written in little endian byte order.
Quoting the WebAssembly site:
WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications.
About speed:
WebAssembly aims to execute at native speed by taking advantage of common hardware capabilities available on a wide range of platforms.
About safety:
WebAssembly describes a memory-safe, sandboxed execution environment […].
The entire project is under the MIT License. Please read the
LICENSE
file.
Many parts of this project are heavily inspired by the wasmerio family of language integrations. These are also MIT licensed.