wasmtime (erlang_wasmtime v0.1.1)

View Source

Run WebAssembly modules natively with Wasmtime and let them call Erlang.

Wat = ~"(module (func (export \"add\") (param i32 i32) (result i32) local.get 0 local.get 1 i32.add))",
{ok, Mod}  = wasmtime:compile({wat, Wat}),
{ok, Inst} = wasmtime:instantiate(Mod),
{ok, [3]}  = wasmtime:call(Inst, ~"add", [1, 2]).

Nothing raises for guest failures: compile, link, trap, WASI and host errors all come back as {error, Map} with class, kind and message keys.

Each instance owns one OS thread and one Wasmtime store. A call runs on that thread while the calling process waits in receive; a host function (an import backed by an Erlang fun) runs in the calling process. Only one call runs on an instance at a time; concurrent callers are queued.

Summary

Types

Every failure has a class, a machine-readable kind and a message. Non-zero WASI exits also carry status; traps carry the wasm frames in trace, innermost first.

What the linked Wasmtime library can do. A runtime-only build has no compiler (compile/1 and serialize/1 answer kind => unavailable) and may have no wat or wasi. See building.md, "Runtime-only builds".

One wasm frame of a trap: the function's index and byte offset, and its names when the module has them.

A host function. Returns the results the guest expects, or {error, Reason} which traps the guest.

A WebAssembly proposal that compile_options() can turn on or off.

A reference the guest handed out: a funcref, an externref or a GC value (struct, array, any other anyref). Opaque; ref_info/1 says which. The object stays alive while the term does; drop the term to let the guest's collector reclaim it. A ref belongs to one instance.

A WebAssembly value. nan, infinity and neg_infinity stand for the floats Erlang cannot represent; a v128 is a 16-byte binary.

WASI configuration. Nothing is granted by default.

Functions

Read element Index of an array.

The length of an array the guest created.

Write element Index of an array.

Wait for the result of call_async/3, serving host calls meanwhile.

Call an exported function and wait for its results.

Start a call and return at once with a reference for await/2,3.

Call a funcref the instance handed out (from a table, a global, a result or a host function argument), with the options of call/4.

End the guest's input. What is queued is still delivered; after that stdin reads return end of file and erlang.recv returns -1. Idempotent.

Compile a module from its binary form, or from text as {wat, Text}.

Load a module produced by serialize/1.

Load a module produced by serialize/1 onto the engine for these compile_options/0. Needed for fuel => true (deserialize/1 covers the defaults and the fuel engine on its own); the loaded module then belongs to that engine, which module_options/1 reports.

List what the module exports, as {Name, Kind}.

Wrap an Erlang term as an externref the guest can hold and hand back.

The term an externref/2 reference wraps.

What the linked Wasmtime library can do; see features/0.

Fuel left after the last call, for a module compiled with fuel => true.

Run the instance's garbage collector now. Objects no longer reachable from the guest or from a ref() are reclaimed and externref/2 terms released. Fails with kind => busy while the guest runs.

Read an exported global; a reference-typed one gives a ref(), null or {i31, N}.

Write an exported mutable global; kind => immutable for a constant one.

Serve one host call message in a host process.

List what the module imports, as {Module, Name, Kind}.

Instantiate a module in its own store and thread.

Interrupt the call running on the instance, from any process.

Size of the default memory as {Pages, Bytes}.

Size of the exported memory called Name as {Pages, Bytes}.

The compile_options/0 a module was compiled or deserialized with.

Read Len bytes at Ptr from the instance's default memory: the export named memory, or the first exported memory.

Same as read_memory/3 on the exported memory called Name.

Take what the captured stdout and stderr hold and empty them.

The reference carried by every message this instance sends: {wasmtime_stream, Ref, Kind, Bytes} and {wasmtime_host_call, Ref, ...}. A process serving several instances matches on it.

What a reference is: #{kind => externref | funcref | struct | array | anyref, instance => Ref} where instance is the ref/1 of the instance it belongs to.

Queue one message for the guest.

Serialize a compiled module into Wasmtime's precompiled form.

Read field Index of a struct the guest created.

Write field Index of a struct; i8 and i16 fields take integers.

Read an element of an exported table: a ref() or null.

Grow an exported table by Delta null elements; returns the previous size.

Grow an exported table by Delta elements holding Init; returns the previous size.

Write an element of an exported table: a ref() of the table's type, or null.

Number of elements in an exported table.

Decode and validate a binary module without compiling it.

Validate against compile_options/0: with proposals disabled, a module using one is refused.

Version of the linked Wasmtime library.

Write Data at Ptr in the default memory. Same rules as read_memory/3.

Same as write_memory/3 on the exported memory called Name.

Types

call_ref()

-opaque call_ref()

compile_options()

-type compile_options() ::
          #{fuel => boolean(),
            opt_level => none | speed | speed_and_size,
            proposals => #{proposal() => boolean()}}.

Options for compile/2, validate/2 and deserialize/2.

  • fuel: compile with fuel metering (see call/4).
  • opt_level: Cranelift's optimization level, speed by default; none compiles fastest, speed_and_size trades some speed for smaller code.
  • proposals: WebAssembly proposals to enable or disable on top of Wasmtime's defaults. Disabling one makes validation refuse modules that use it: #{simd => false, threads => false} for a plugin format that must not need them.

Of these, only fuel is part of a precompiled module's compatibility check: give it again to deserialize/2 (or rely on deserialize/1, which tries the fuel engine too). The optimization level and disabled proposals need nothing at load time. Each distinct option set is one Wasmtime engine, created on first use and kept; at most 32 exist per VM.

error()

-type error() ::
          {error,
           #{class := compile | link | call | trap | host | wasi | memory | global | table | exit,
             kind := atom(),
             message := binary(),
             status => integer(),
             trace => [frame()]}}.

Every failure has a class, a machine-readable kind and a message. Non-zero WASI exits also carry status; traps carry the wasm frames in trace, innermost first.

features()

-type features() :: #{compiler := boolean(), wat := boolean(), wasi := boolean()}.

What the linked Wasmtime library can do. A runtime-only build has no compiler (compile/1 and serialize/1 answer kind => unavailable) and may have no wat or wasi. See building.md, "Runtime-only builds".

frame()

-type frame() ::
          #{func_index := non_neg_integer(),
            func_offset := non_neg_integer(),
            func_name := binary() | undefined,
            module_name := binary() | undefined}.

One wasm frame of a trap: the function's index and byte offset, and its names when the module has them.

host_fun()

-type host_fun() :: fun((instance(), [value()]) -> {ok, [value()]} | {error, term()}).

A host function. Returns the results the guest expects, or {error, Reason} which traps the guest.

instance()

-opaque instance()

module_ref()

-opaque module_ref()

options()

-type options() ::
          #{imports => #{{binary(), binary()} => host_fun()},
            wasi => wasi_options(),
            memory_limit => pos_integer() | unlimited,
            max_tables => pos_integer() | unlimited,
            max_table_elements => pos_integer() | unlimited,
            max_instances => pos_integer() | unlimited,
            host_timeout => timeout(),
            host => pid(),
            stream => pid(),
            inbox_limit => pos_integer()}.

proposal()

-type proposal() ::
          simd | relaxed_simd | relaxed_simd_deterministic | bulk_memory | multi_value | multi_memory |
          memory64 | tail_call | wide_arithmetic | custom_page_sizes | threads | reference_types |
          function_references | gc | exceptions.

A WebAssembly proposal that compile_options() can turn on or off.

ref()

-opaque ref()

A reference the guest handed out: a funcref, an externref or a GC value (struct, array, any other anyref). Opaque; ref_info/1 says which. The object stays alive while the term does; drop the term to let the guest's collector reclaim it. A ref belongs to one instance.

value()

-type value() ::
          integer() |
          float() |
          nan | infinity | neg_infinity |
          <<_:128>> |
          null |
          ref() |
          {i31, integer()}.

A WebAssembly value. nan, infinity and neg_infinity stand for the floats Erlang cannot represent; a v128 is a 16-byte binary.

wasi_options()

-type wasi_options() ::
          #{args => inherit | [iodata()],
            env => inherit | [{iodata(), iodata()}],
            dirs => [{Guest :: iodata(), Host :: iodata(), read | write}],
            stdin => none | inherit | stream | {file, iodata()} | {binary, iodata()},
            stdout => none | inherit | stream | {file, iodata()} | capture,
            stderr => none | inherit | stream | {file, iodata()} | capture,
            output_limit => pos_integer()}.

WASI configuration. Nothing is granted by default.

  • args, env: what the guest sees, or inherit for the VM's own.
  • dirs: preopened directories, read-only unless write.
  • stdin: end of file by default; a file, the VM's stdin, bytes, or stream: what send/2 queues, as the guest reads it.
  • stdout, stderr: discarded by default; a file, the VM's own, capture into memory, read with read_output/1, or stream: every write goes to the stream process as {wasmtime_stream, Ref, stdout | stderr, Bytes} at once.
  • output_limit: bytes kept per captured stream (default 16 MB); the guest never sees a short write, read_output/1 reports what was dropped.

Functions

array_get(Ref, Index)

-spec array_get(ref(), non_neg_integer()) -> {ok, value()} | error().

Read element Index of an array.

array_len(Ref)

-spec array_len(ref()) -> {ok, non_neg_integer()} | error().

The length of an array the guest created.

array_set(Ref, Index, Value)

-spec array_set(ref(), non_neg_integer(), value()) -> ok | error().

Write element Index of an array.

await(Inst, Ref)

-spec await(instance(), call_ref()) -> {ok, [value()]} | error().

Equivalent to await(Inst, Ref, infinity).

await/3

-spec await(instance(), call_ref(), timeout()) -> {ok, [value()]} | error().

Wait for the result of call_async/3, serving host calls meanwhile.

Must be called by the process that started the call. With a timeout the call is cancelled like in call/4.

call(Inst, Name, Args)

-spec call(instance(), iodata(), [value()]) -> {ok, [value()]} | error().

Equivalent to call(Inst, Name, Args, #{}).

call(Inst, Name, Args, Opts)

-spec call(instance(), iodata(), [value()], #{timeout => timeout(), fuel => non_neg_integer()}) ->
              {ok, [value()]} | error().

Call an exported function and wait for its results.

Host functions the guest calls run in this process, so it must be able to receive messages until the call returns. With timeout the guest is interrupted when the time is up and {error, #{kind := timeout}} is returned. With fuel the call may execute that many units of fuel (about one per instruction) before it traps with kind := out_of_fuel; the module must have been compiled with fuel => true.

timeout covers guest execution and the wait for it. It cannot fire while this process is inside one of its own host functions; host_timeout (an instantiate option) is what bounds the guest there.

call_async/3

-spec call_async(instance(), iodata(), [value()]) -> {ok, call_ref()} | error().

Start a call and return at once with a reference for await/2,3.

The call runs on the instance thread while this process does other work. Host functions are still served by this process, and only while it is inside await/2,3 (or by the host process when one was given), so a guest that calls back before await waits until then, within host_timeout.

call_ref(Inst, Ref, Args)

-spec call_ref(instance(), ref(), [value()]) -> {ok, [value()]} | error().

Equivalent to call_ref(Inst, Ref, Args, #{}).

call_ref(Inst, Ref, Args, Opts)

-spec call_ref(instance(), ref(), [value()], #{timeout => timeout(), fuel => non_neg_integer()}) ->
                  {ok, [value()]} | error().

Call a funcref the instance handed out (from a table, a global, a result or a host function argument), with the options of call/4.

close/1

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

End the guest's input. What is queued is still delivered; after that stdin reads return end of file and erlang.recv returns -1. Idempotent.

compile(Source)

-spec compile(binary() | {wat, iodata()}) -> {ok, module_ref()} | error().

Compile a module from its binary form, or from text as {wat, Text}.

Compilation runs on a dirty CPU scheduler. The result is immutable and can be instantiated any number of times, from any process.

A runtime-only build has no compiler: this returns {error, #{kind := unavailable}} and modules come from deserialize/1.

compile/2

-spec compile(binary() | {wat, iodata()}, compile_options()) -> {ok, module_ref()} | error().

Compile with compile_options/0.

deserialize(Bin)

-spec deserialize(binary()) -> {ok, module_ref()} | error().

Load a module produced by serialize/1.

Wasmtime verifies its own version and the CPU features the code was built for, not the machine code itself. Only bytes that came from serialize/1, from a source you trust, may be passed here; a .wasm file goes to compile/1.

deserialize(Bin, Opts)

-spec deserialize(binary(), compile_options()) -> {ok, module_ref()} | error().

Load a module produced by serialize/1 onto the engine for these compile_options/0. Needed for fuel => true (deserialize/1 covers the defaults and the fuel engine on its own); the loaded module then belongs to that engine, which module_options/1 reports.

exports(Mod)

-spec exports(module_ref()) -> [{binary(), func | global | table | memory | tag}].

List what the module exports, as {Name, Kind}.

externref/2

-spec externref(instance(), term()) -> {ok, ref()} | error().

Wrap an Erlang term as an externref the guest can hold and hand back.

The term is copied; externref_data/1 copies it out again. The object lives while any ref() to it or the guest reaches it. Fails with kind => gc_heap_full when Wasmtime cannot allocate; gc/1 may make room.

externref_data(Ref)

-spec externref_data(ref()) -> {ok, term()} | error().

The term an externref/2 reference wraps.

features()

-spec features() -> features().

What the linked Wasmtime library can do; see features/0.

fuel_remaining/1

-spec fuel_remaining(instance()) -> {ok, non_neg_integer()} | error().

Fuel left after the last call, for a module compiled with fuel => true.

gc/1

-spec gc(instance()) -> ok | error().

Run the instance's garbage collector now. Objects no longer reachable from the guest or from a ref() are reclaimed and externref/2 terms released. Fails with kind => busy while the guest runs.

global_get/2

-spec global_get(instance(), iodata()) -> {ok, value()} | error().

Read an exported global; a reference-typed one gives a ref(), null or {i31, N}.

global_set/3

-spec global_set(instance(), iodata(), value()) -> ok | error().

Write an exported mutable global; kind => immutable for a constant one.

handle_host_call/2

-spec handle_host_call(instance(), term()) -> ok | ignore.

Serve one host call message in a host process.

Call it with every {wasmtime_host_call, Ref, HostId, Key, Args} message the process receives for Inst; it runs the import fun and replies to the guest. Returns ignore for a message that is not a host call of this instance, so it can sit in a receive alongside other messages.

imports(Mod)

-spec imports(module_ref()) -> [{binary(), binary(), func | global | table | memory | tag}].

List what the module imports, as {Module, Name, Kind}.

instantiate(Mod)

-spec instantiate(module_ref()) -> {ok, instance()} | error().

Equivalent to instantiate(Mod, #{}).

instantiate(Mod, Opts)

-spec instantiate(module_ref(), options()) -> {ok, instance()} | error().

Instantiate a module in its own store and thread.

Nothing is granted by default: no host functions, no WASI, 256 MB of linear memory at most. Options:

  • imports: map from {Module, Name} to a host fun. An import the module needs and the map does not provide fails with class => link.
  • wasi: enable WASI preview 1. See wasi_options/0; without dirs the guest has no filesystem, without stdout/stderr its output is discarded. A build without WASI (see features/0) answers kind => unavailable.
  • memory_limit, max_tables, max_table_elements, max_instances: per-store caps enforced by Wasmtime. unlimited removes a cap.
  • host_timeout: how long a host function may run before the guest traps (default 30 s).
  • host: a process that serves host calls instead of the caller. It receives {wasmtime_host_call, Ref, HostId, Key, Args} messages and answers them with handle_host_call/2. Host calls made by the module's start section during instantiate/2 still go to the caller.

The module's start section runs during instantiation and may call host functions; a trap there is reported as class => trap. A WASI _start is an ordinary export and is not run here: call it.

interrupt/1

-spec interrupt(instance()) -> ok | not_running.

Interrupt the call running on the instance, from any process.

The call fails with {error, #{class := trap, kind := interrupt}} within one epoch tick (10 ms), or at once if it is waiting inside a host function. Returns not_running when the instance is idle.

memory_size(Inst)

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

Size of the default memory as {Pages, Bytes}.

memory_size/2

-spec memory_size(instance(), default | iodata()) ->
                     {ok, {non_neg_integer(), non_neg_integer()}} | error().

Size of the exported memory called Name as {Pages, Bytes}.

module_options(Mod)

-spec module_options(module_ref()) -> compile_options().

The compile_options/0 a module was compiled or deserialized with.

read_memory(Inst, Ptr, Len)

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

Read Len bytes at Ptr from the instance's default memory: the export named memory, or the first exported memory.

Works while the instance is idle or while a host function runs (pass the instance the host fun received). Fails with kind => busy if the guest is executing.

read_memory/4

-spec read_memory(instance(), default | iodata(), non_neg_integer(), non_neg_integer()) ->
                     {ok, binary()} | error().

Same as read_memory/3 on the exported memory called Name.

read_output/1

-spec read_output(instance()) -> {ok, {binary(), binary(), {non_neg_integer(), non_neg_integer()}}}.

Take what the captured stdout and stderr hold and empty them.

Returns {ok, {Stdout, Stderr, {DroppedOut, DroppedErr}}}; the counters say how many bytes went past output_limit. Works while the guest runs, so a long-running guest's output can be drained from another process.

ref/1

-spec ref(instance()) -> reference().

The reference carried by every message this instance sends: {wasmtime_stream, Ref, Kind, Bytes} and {wasmtime_host_call, Ref, ...}. A process serving several instances matches on it.

ref_info(Ref)

-spec ref_info(ref()) ->
                  #{kind := externref | funcref | struct | array | anyref, instance := reference()}.

What a reference is: #{kind => externref | funcref | struct | array | anyref, instance => Ref} where instance is the ref/1 of the instance it belongs to.

send/2

-spec send(instance(), iodata()) -> ok | error().

Queue one message for the guest.

The guest reads it through stdin => stream (as bytes, without message boundaries) or the erlang.recv import (one whole message). Never blocks: once inbox_limit bytes (default 16 MB) are queued and unread it returns {error, #{kind := inbox_full}} and the sender retries later. After close/1 it returns {error, #{kind := closed}}.

serialize(Mod)

-spec serialize(module_ref()) -> {ok, binary()} | error().

Serialize a compiled module into Wasmtime's precompiled form.

The result loads with deserialize/1 without compiling, on the same Wasmtime version and a CPU with the same features. Use it to compile once at build time and ship the output, or to keep a cache.

struct_get(Ref, Index)

-spec struct_get(ref(), non_neg_integer()) -> {ok, value()} | error().

Read field Index of a struct the guest created.

struct_set(Ref, Index, Value)

-spec struct_set(ref(), non_neg_integer(), value()) -> ok | error().

Write field Index of a struct; i8 and i16 fields take integers.

table_get/3

-spec table_get(instance(), iodata(), non_neg_integer()) -> {ok, value()} | error().

Read an element of an exported table: a ref() or null.

table_grow(Inst, Name, Delta)

-spec table_grow(instance(), iodata(), non_neg_integer()) -> {ok, non_neg_integer()} | error().

Grow an exported table by Delta null elements; returns the previous size.

table_grow/4

-spec table_grow(instance(), iodata(), non_neg_integer(), value()) -> {ok, non_neg_integer()} | error().

Grow an exported table by Delta elements holding Init; returns the previous size.

table_set/4

-spec table_set(instance(), iodata(), non_neg_integer(), value()) -> ok | error().

Write an element of an exported table: a ref() of the table's type, or null.

table_size/2

-spec table_size(instance(), iodata()) -> {ok, non_neg_integer()} | error().

Number of elements in an exported table.

validate(Bin)

-spec validate(binary()) -> ok | error().

Decode and validate a binary module without compiling it.

Cheaper than compile/1 when the question is only whether the bytes are a well-formed module; the errors have the same shape.

validate(Bin, Opts)

-spec validate(binary(), compile_options()) -> ok | error().

Validate against compile_options/0: with proposals disabled, a module using one is refused.

version()

-spec version() -> binary().

Version of the linked Wasmtime library.

write_memory(Inst, Ptr, Data)

-spec write_memory(instance(), non_neg_integer(), iodata()) -> ok | error().

Write Data at Ptr in the default memory. Same rules as read_memory/3.

write_memory/4

-spec write_memory(instance(), default | iodata(), non_neg_integer(), iodata()) -> ok | error().

Same as write_memory/3 on the exported memory called Name.