- Rust 53%
- Elixir 45%
- Nix 2%
Wasmer #
WebAssembly execution from Elixir, powered by Wasmer. This binding provides a small public API for compiling WAT or Wasm binary modules, instantiating modules without imports, looking up exported functions, and calling them with tagged WebAssembly numeric values.
Installation #
Add wasmer to the dependencies in your Mix project:
def deps do
[
{:wasmer, "~> 0.1.0"}
]
end
This package builds its Rustler NIF from source during Mix compilation. The build host must have:
- Cargo, supplied with Rust
- a stable Rust toolchain
- a C or C++ toolchain supported by Wasmer on the target platform
After installing the prerequisites, fetch and compile the dependency with
mix deps.get and mix compile.
Quick start #
The complete execution sequence is:
wat = ~S"""
(module
(func (export "add_one") (param i32) (result i32)
local.get 0
i32.const 1
i32.add))
"""
{:ok, store} = Wasmer.Store.new()
{:ok, module} = Wasmer.Module.new(store, wat)
{:ok, instance} = Wasmer.Instance.new(module)
{:ok, add_one} = Wasmer.Instance.function(instance, "add_one")
Wasmer.Function.call(add_one, [{:i32, 41}])
# {:ok, [{:i32, 42}]}
Wasmer.Store.new/0 creates an isolated store. Compile source with
Wasmer.Module.new/2, instantiate it with Wasmer.Instance.new/1, retrieve a
named function with Wasmer.Instance.function/2, and invoke it with
Wasmer.Function.call/2.
Module input and imports #
Wasmer.Module.new/2 accepts either WAT text or a Wasm binary as its second
argument. It performs checked compilation; it does not interpret the argument
as a file path. An empty module can be compiled from the Wasm binary header:
empty_wasm = <<0, 97, 115, 109, 1, 0, 0, 0>>
{:ok, store} = Wasmer.Store.new()
{:ok, module} = Wasmer.Module.new(store, empty_wasm)
{:ok, _instance} = Wasmer.Instance.new(module)
Instances are created with an empty import collection. There is no import map
or Elixir host-callback API in this release. A module that requires an import
therefore returns {:error, {:instantiate, message}}.
Values #
Function arguments and results use tagged tuples:
{:i32, integer}— a signed 32-bit integer from-2_147_483_648through2_147_483_647{:i64, integer}— a signed 64-bit integer from-9_223_372_036_854_775_808through9_223_372_036_854_775_807{:f32, float}— a WebAssembly 32-bit floating-point value{:f64, float}— a WebAssembly 64-bit floating-point value{:v128, binary}— exactly sixteen bytes in WebAssembly little-endian lane order
The tags must be the atoms shown above. Integers outside the signed range,
non-float floating-point values, malformed tuples, :v128 binaries that are
not sixteen bytes, and unsupported reference values are rejected with
{:error, :invalid_value}.
Calls return results in WebAssembly result order:
{:ok, [{:i32, 3}, {:i64, 4}, {:f32, 5.0}, {:f64, 6.0}]}
Binary memory #
For binary data, a module must define and export its own linear memory. Instance
creation uses Imports::new(). The binding does not import memory.
Use Wasmer.Instance.memory/2 to get a named memory export. The offset and
length are byte values. Write the binary at an explicit offset. Then pass the
offset and byte size through existing {:i32, value} function arguments. The
guest owns allocation and memory growth. The binding does not select a default
memory, allocate guest space, or expand binary function arguments.
wat = ~S"""
(module
(memory (export "memory") 1)
(data (i32.const 32) "guest")
(func (export "load_byte") (param i32) (result i32)
local.get 0
i32.load8_u))
"""
{:ok, store} = Wasmer.Store.new()
{:ok, module} = Wasmer.Module.new(store, wat)
{:ok, instance} = Wasmer.Instance.new(module)
{:ok, memory} = Wasmer.Instance.memory(instance, "memory")
:ok = Wasmer.Memory.write(memory, 0, <<0, 255, 65>>)
{:ok, load_byte} = Wasmer.Instance.function(instance, "load_byte")
{:ok, [{:i32, 255}]} = Wasmer.Function.call(load_byte, [{:i32, 1}])
{:ok, "guest"} = Wasmer.Memory.read(memory, 32, 5)
Wasmer.Memory.read/3 and Wasmer.Memory.write/3 check every range. A read
or write outside the exported memory returns {:error, :out_of_bounds}.
Results and errors #
Successful creators return {:ok, handle} and successful calls return
{:ok, values}. Public operations may return exactly one of these error
tuples:
{:error, {:compile, diagnostic}}
{:error, {:instantiate, diagnostic}}
{:error, :not_found}
{:error, :not_a_function}
{:error, {:runtime, diagnostic}}
{:error, :invalid_value}
{:error, :worker_unavailable}
{:error, :not_a_memory}
{:error, :out_of_bounds}
{:error, :allocation_failed}
:not_found means that a named export does not exist. :not_a_function means
that the named export exists but is not a function. :not_a_memory means that
the named export exists but is not a linear memory. :allocation_failed means
that the NIF cannot allocate a required cross-thread byte buffer or output
binary. Compilation, instantiation, and guest runtime failures carry diagnostic
binaries. Those diagnostics are informational text, not stable Wasmer message
strings.
Execution and concurrency #
Each store owns one dedicated Wasmer worker. Calls for modules, instances, and functions belonging to the same store are serialized on that worker, while separate stores are isolated from one another. This preserves Wasmer's store thread-affinity requirements.
Native calls use Rustler's dirty-CPU scheduler. Guest execution that does not return can occupy the dedicated worker indefinitely, and this API provides no timeout or interruption mechanism. Do not call guest functions that may run forever.