Getting started

View Source

This guide takes you from a .wasm file to a call from Erlang. You need it the first time you embed a module; the other guides cover host functions, WASI and the build.

Add the dependency

%% rebar.config
{deps, [{erlang_wasmtime, "0.1.1"}]}.

Run rebar3 compile. The first build downloads the Wasmtime C API and builds priv/wasmtime_nif.so.

Compile a module

From a binary:

{ok, Bin} = file:read_file("plugin.wasm"),
{ok, Mod} = wasmtime:compile(Bin).

From text:

{ok, Mod} = wasmtime:compile({wat, ~"""
    (module
      (memory (export "memory") 1)
      (func (export "add") (param i32 i32) (result i32)
        local.get 0 local.get 1 i32.add))
    """}).

compile/1 runs on a dirty CPU scheduler. The module is immutable: compile once, instantiate as often as you like.

Inspect it

[] = wasmtime:imports(Mod),
[{~"memory", memory}, {~"add", func}] = wasmtime:exports(Mod).

Instantiate and call

{ok, Inst} = wasmtime:instantiate(Mod),
{ok, [3]}  = wasmtime:call(Inst, ~"add", [1, 2]).

Arguments and results follow the function's type: i32/i64 as integers, f32/f64 as floats (with nan, infinity, neg_infinity for what an Erlang float cannot hold), v128 as a 16-byte binary.

Handle failure

{error, #{class := trap, kind := integer_division_by_zero}} =
    wasmtime:call(Inst, ~"div", [1, 0]).

Every error is {error, Map} with class, kind and message. The instance survives a trap and can be called again.

Put a time limit on a call

{error, #{kind := timeout}} = wasmtime:call(Inst, ~"loop", [], #{timeout => 100}).

Or from another process while it runs:

ok = wasmtime:interrupt(Inst).

Run a call without blocking

{ok, Ref} = wasmtime:call_async(Inst, ~"work", [Arg]),
... do something else ...
{ok, [Result]} = wasmtime:await(Inst, Ref, 5000).

The call runs on the instance thread while this process continues. await must run in the process that started the call, because host functions are served there, while it waits; a guest that calls the host earlier waits for await (within host_timeout), or use a host process. Two async calls on one instance run one after the other.

Compile options

{ok, Mod} = wasmtime:compile(Bin, #{
    opt_level => speed_and_size,
    proposals => #{simd => false, threads => false, gc => false},
    fuel => true}).
  • opt_level: Cranelift's none, speed (default) or speed_and_size.
  • proposals: turn WebAssembly proposals off (or on) relative to Wasmtime's defaults. A module that uses a disabled one is refused by compile/2 and validate/2 with class => compile, which is how you pin down what a plugin format may contain.
  • fuel: compile with fuel metering, see below.

module_options/1 returns what a module was compiled with. Each distinct option set is one Wasmtime engine, created on first use (at most 32 per VM). Of the options only fuel is checked when a precompiled module is loaded (deserialize/2); the optimization level and disabled proposals need nothing at load time.

Bound a call by instructions instead of time

{ok, Mod}  = wasmtime:compile(Bin, #{fuel => true}),
{ok, Inst} = wasmtime:instantiate(Mod),
{error, #{kind := out_of_fuel}} = wasmtime:call(Inst, ~"loop", [], #{fuel => 1000000}),
{ok, Left} = wasmtime:fuel_remaining(Inst).

Fuel counts instructions (about one unit each), so the same input always stops at the same point, unlike timeout. It costs a few percent of speed and must be chosen at compile time; fuel belongs to the instance and is consumed across calls until set again.

Read traps with their frames

{error, #{kind := unreachable, trace := [#{func_name := ~"inner"} | _]}} =
    wasmtime:call(Inst, ~"outer", []).

trace lists the wasm frames innermost first, with the function index and byte offset, and names when the module carries a name section.

Globals and tables

{ok, 7}    = wasmtime:global_get(Inst, ~"counter"),
ok         = wasmtime:global_set(Inst, ~"counter", 8),
{ok, 4}    = wasmtime:table_size(Inst, ~"handlers"),
{ok, 4}    = wasmtime:table_grow(Inst, ~"handlers", 2).

A constant global refuses global_set. Table elements and reference-typed globals are references: table_get/3 and table_set/4 read and write them, table_grow/4 fills new slots with one. See references for what a reference term is and what you can do with it.

Read and write memory

ok = wasmtime:write_memory(Inst, 0, <<"hello">>),
{ok, <<"hello">>} = wasmtime:read_memory(Inst, 0, 5),
{ok, {Pages, Bytes}} = wasmtime:memory_size(Inst).

Memory is reachable while the instance is idle, or from inside a host function. During a call it returns {error, #{kind := busy}}. These act on the export named memory (or the first exported memory); read_memory/4, write_memory/4 and memory_size/2 take an export name for modules with several memories.

Notes

  • One call runs on an instance at a time. Calls from several processes are queued in order.
  • The process that calls wasmtime:call must stay able to receive messages: host functions and results arrive there.
  • Drop every reference to an instance and its thread, store and memory are freed by the garbage collector.