wasm (wasm v0.1.0)
View SourceA 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
-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.
-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
-spec call(instance(), binary(), [term()]) -> {ok, [term()]} | {error, wasm_error:error()}.
-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.
-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.
-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.
-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.
-spec format_error(wasm_error:error()) -> iolist().
-spec get_global(instance(), binary()) -> {ok, [term()]} | {error, wasm_error:error()}.
-spec instantiate(module_(), map()) -> {ok, instance()} | {error, wasm_error:error()}.
-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.
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.
-spec load_file(file:filename_all()) -> {ok, module_()} | {error, term()}.
Read a .wasm file and load it.
-spec memory_size(instance() | map()) -> {ok, non_neg_integer()} | {error, term()}.
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.
-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 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.
-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.
-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.
-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.
-spec write_memory(instance() | map(), non_neg_integer(), binary()) -> ok | {error, wasm_error:error()}.