wasm (wasm v0.1.0)

View Source

A WebAssembly runtime for Erlang/OTP.

Start here when you are embedding the runtime. You load a module once, instantiate it as often as you need, call its exports, and destroy the instances when you are done.

{ok, Mod}  = wasm:load(Binary),                  % compiled once, cached
{ok, Inst} = wasm:instantiate(Mod, Imports),     % owned by this process
{ok, [R]}  = wasm:call(Inst, ~"run", [42]),
ok         = wasm:destroy(Inst).

You get one execution path

Create an instance, call it, destroy it. That is the inline path and there is no other. The library ships no process wrapper, because process architecture belongs to your application; ets, counters and atomics do not impose one either.

Build actors on this API rather than beside it. A worker is a process that calls instantiate/3 in its init and call/3 in its handle_call. See docs/worker.md and examples/wasm_worker.erl for the pattern, including per-request isolation, timeouts, and killing runaway code.

You own the instance

An instance belongs to the process that created it and stays valid while that process lives, like a port or an ETS table. You may pass it to another process, but do not call the same instance from two processes at once: both read-modify-write the same mutable state and the last writer wins. Putting the instance inside a process is what serialises the calls.

Pages are released when the owner exits, however it exits, so forgetting destroy/1 is untidy rather than a leak. Destroying an instance releases only its claim on each memory: one it imported stays as long as anybody else holds it, and destroying the same instance twice releases nothing the second time.

Every failure is a value

Nothing here raises, including on hostile input. A malformed binary, an ill-typed module, a trap and a resource limit all come back as {error, E}, where E carries the class (malformed, invalid, link, trap, exhaustion), a machine-readable kind, the specification's message text, and context to diagnose it.

Summary

Types

An instance, owned by the process that created it.

A module: compiled inline, or a handle to a cached one.

Functions

Decode and validate a module binary without caching it.

As compile/1, with an identity for the module.

Release an instance's memory pages and state.

Take an instance's export in the form another module can import it.

Instantiate, resolving imports and running the start function.

Decode, validate and cache a module, returning a handle.

Read a .wasm file and load it.

Keep a garbage-collected reference alive until you release it.

Read a byte range from a host function's instance.

Release a reference, letting it be collected once nothing else holds it.

Release every pinned reference.

Drop your claim on a cached module.

Decode without validating. Use it to inspect a module that fails validation; do not instantiate what it gives you.

Types

instance()

-nominal instance() ::
             #inst{id :: reference(),
                   ckpt :: reference(),
                   entry_key :: undefined | reference(),
                   types :: tuple(),
                   funcs :: tuple(),
                   exports :: #{binary() => {func | table | mem | global, non_neg_integer()}},
                   elems :: tuple(),
                   datas :: tuple(),
                   globaltypes :: tuple(),
                   tags :: tuple(),
                   canon :: tuple(),
                   fields :: tuple(),
                   kinds :: tuple(),
                   supers :: tuple(),
                   heap :: undefined | wasm_heap:heap(),
                   identity :: undefined | {sha256, binary()} | reference(),
                   store :: term(),
                   version :: term(),
                   limits :: map(),
                   ctx :: term()}.

An instance, owned by the process that created it.

module_()

-nominal module_() ::
             #module{identity :: undefined | {sha256, binary()} | reference(),
                     types ::
                         [#subtype{final :: boolean(),
                                   supers :: [typeidx()],
                                   body ::
                                       #functype{params :: [valtype()], results :: [valtype()]} |
                                       #structtype{fields ::
                                                       [#fieldtype{type :: valtype() | i8 | i16,
                                                                   mut :: mut()}]} |
                                       #arraytype{field ::
                                                      #fieldtype{type :: valtype() | i8 | i16,
                                                                 mut :: mut()}}}],
                     rec_groups :: [{non_neg_integer(), non_neg_integer()}],
                     imports :: [#import{module :: binary(), name :: binary(), desc :: externtype()}],
                     funcs ::
                         [#func{type :: typeidx(),
                                locals :: [valtype()],
                                body :: [instr()] | {validated, [annotated()]}}],
                     tables ::
                         [#tabletype{limits ::
                                         #limits{min :: non_neg_integer(),
                                                 max :: undefined | non_neg_integer(),
                                                 shared :: boolean(),
                                                 index_type :: i32 | i64},
                                     elemtype :: reftype(),
                                     init :: undefined | [instr()]}],
                     mems ::
                         [#memtype{limits ::
                                       #limits{min :: non_neg_integer(),
                                               max :: undefined | non_neg_integer(),
                                               shared :: boolean(),
                                               index_type :: i32 | i64}}],
                     tags :: [#tagtype{type :: typeidx()}],
                     globals ::
                         [#global{type :: #globaltype{valtype :: valtype(), mut :: mut()},
                                  init :: [instr()]}],
                     exports ::
                         [#export{name :: binary(),
                                  desc :: {func | table | mem | global | tag, non_neg_integer()}}],
                     start :: undefined | funcidx(),
                     elems ::
                         [#elem{type :: reftype(),
                                init :: [[instr()]],
                                mode :: passive | declarative | {active, tableidx(), [instr()]}}],
                     datas :: [#data{init :: binary(), mode :: passive | {active, memidx(), [instr()]}}],
                     data_count :: undefined | non_neg_integer(),
                     customs :: [{binary(), binary()}]} |
             wasm_module_cache:handle().

A module: compiled inline, or a handle to a cached one.

Functions

call(Inst, Name, Args)

-spec call(instance(), binary(), [term()]) -> {ok, [term()]} | {error, wasm_error:error()}.

call(Inst, Name, Args, Opts)

-spec call(instance(), binary(), [term()], map()) -> {ok, [term()]} | {error, wasm_error:error()}.

compile(Bin)

-spec compile(binary()) -> {ok, module_()} | {error, wasm_error:error()}.

Decode and validate a module binary without caching it.

Use it for one-shot work. If you instantiate more than once, use load/1.

compile(Bin, Opts)

-spec compile(binary(), map()) -> {ok, module_()} | {error, wasm_error:error()}.

As compile/1, with an identity for the module.

Pass one when you already know what names these bytes -- wasm_module_cache passes the content hash it has just computed -- so that anything caching work derived from the module shares it between two loads of the same bytes. Without one the module gets a fresh reference, which is correct and simply does not share. Nothing hashes on this path: five milliseconds on the 1.8 MB QuickJS module is not worth paying to buy sharing the inline API does not promise.

destroy(Inst)

-spec destroy(instance()) -> ok.

Release an instance's memory pages and state.

Idempotent, and optional: pages are released anyway when the owning process exits. Calling it returns them sooner, which matters when one process creates and discards many instances.

exports(Inst)

-spec exports(instance()) -> #{binary() => term()}.

extern(Inst, Name)

-spec extern(instance(), binary()) -> {ok, term()} | {error, wasm_error:error()}.

Take an instance's export in the form another module can import it.

Functions come back as host functions, so wasm-to-wasm linking reuses the same import mechanism as Erlang-implemented imports rather than needing a second path through the interpreter.

format_error(E)

-spec format_error(wasm_error:error()) -> iolist().

get_global(Inst, Name)

-spec get_global(instance(), binary()) -> {ok, [term()]} | {error, wasm_error:error()}.

instantiate(M, Imports)

-spec instantiate(module_(), map()) -> {ok, instance()} | {error, wasm_error:error()}.

instantiate/3

-spec instantiate(module_(), map(), map()) -> {ok, instance()} | {error, wasm_error:error()}.

Instantiate, resolving imports and running the start function.

Opts takes fuel and max_depth. Both default to permissive values, so a trusted embedder does not have to opt out of limits. Set them yourself when the module is untrusted.

To link modules that exchange garbage-collected references, name the instance to share an object store with:

{ok, A} = wasm:instantiate(ModA, #{}),
{ok, T} = wasm:extern(A, ~"table"),
{ok, B} = wasm:instantiate(ModB, #{{~"env", ~"t"} => T}, #{link => A}).

A reference is an id into a store, so an id from another store means nothing here and reading one traps with foreign_reference. Link this way whenever two modules pass structs or arrays between them, through a shared table, a shared global or each other's exports. You do not need it for modules that exchange only numbers, memories and functions.

You say it explicitly rather than having it inferred from Imports, because an import is a bare handle that does not say which instance produced it: extern/2 hands out a table, a memory or a cell, none of which names its origin. Inferring it would work for some import kinds and not others, which is worse than one rule that always holds.

The instance you get back is scoped to your process. Its mutable state lives in an ETS table this process owns, so the handle stops working when this process exits, like a port or an ETS table would. Passing the handle to another process works while the creator is alive. To give an instance a lifetime of its own, put a process in charge of it; see docs/worker.md.

load(Binary)

-spec load(binary()) -> {ok, module_()} | {error, term()}.

Decode, validate and cache a module, returning a handle.

Use this rather than compile/1. Compiling is the expensive step, roughly 20 ms for a 100 KB Rust binary against 15 us to instantiate one, and loading the same bytes twice hands you the cached artefact instead of repeating the work.

load(Binary, Opts)

-spec load(binary(), map()) -> {ok, module_()} | {error, term()}.

load_file(Path)

-spec load_file(file:filename_all()) -> {ok, module_()} | {error, term()}.

Read a .wasm file and load it.

memory_size(Ctx)

-spec memory_size(instance() | map()) -> {ok, non_neg_integer()} | {error, term()}.

pin(Inst, Ref)

-spec pin(instance(), term()) -> ok.

Keep a garbage-collected reference alive until you release it.

References that leave the runtime are pinned for you: call results, get_global/2 results, and the values carried by an uncaught exception. The runtime cannot see what you are holding, so without a pin the next collection frees it.

Pin by hand in one case: your host function keeps a reference it was passed. Its arguments are safe for the duration of the call, because collection does not run below a live frame, and not afterwards.

Fun = fun(_Ctx, [Ref]) ->
          ok = wasm:pin(Inst, Ref),          % keeping it past this call
          ets:insert(mine, {last, Ref}),
          {ok, []}
      end.

Reference counted, so a reference pinned twice needs releasing twice.

read_memory(Ctx, Addr, Len)

-spec read_memory(instance() | map(), non_neg_integer(), non_neg_integer()) ->
                     {ok, binary()} | {error, wasm_error:error()}.

Read a byte range from a host function's instance.

Accepts either an instance or the context map a host function receives, so imports can be written without unpacking anything.

release(Inst, Ref)

-spec release(instance(), term()) -> ok.

Release a reference, letting it be collected once nothing else holds it.

Releasing something that was never pinned is not an error, so you can release everything you have seen without tracking which ones counted.

release_all(Inst)

-spec release_all(instance()) -> ok.

Release every pinned reference.

Use this when you scope references to a request: run the request, take what you need out of the results, then drop the lot. Without it, pins accumulate for the life of the instance.

unload/1

-spec unload(module_()) -> ok.

Drop your claim on a cached module.

The module stays resident until every holder has released it, so you can call this safely even when another part of the system loaded the same bytes.

validate(M)

-spec validate(module_()) -> {ok, module_()} | {error, wasm_error:error()}.

Decode without validating. Use it to inspect a module that fails validation; do not instantiate what it gives you.

write_memory(Ctx, Addr, Bin)

-spec write_memory(instance() | map(), non_neg_integer(), binary()) -> ok | {error, wasm_error:error()}.