Host functions

View Source

A host function is an import the module declares that you provide as an Erlang fun. You need one whenever the guest has to reach outside its sandbox: logging, reading a table, calling a service, sending a message.

Declare the import in the module

(module
  (import "env" "lookup" (func $lookup (param i32 i32) (result i32)))
  (memory (export "memory") 1)
  (func (export "run") (param i32 i32) (result i32)
    local.get 0 local.get 1 call $lookup))

Provide it at instantiate time

Lookup = fun(Ctx, [Ptr, Len]) ->
    {ok, Key} = wasmtime:read_memory(Ctx, Ptr, Len),
    case ets:lookup(prices, Key) of
        [{_, Price}] -> {ok, [Price]};
        [] -> {error, ~"unknown key"}
    end
end,
{ok, Inst} = wasmtime:instantiate(Mod, #{imports => #{{~"env", ~"lookup"} => Lookup}}).

The fun receives the instance as Ctx and the arguments as a list of values. Return {ok, Results} with exactly the values the import's type declares, or {error, Reason}.

What happens on error

{error, Reason} traps the guest. The call returns:

{error, #{class := host, kind := host_error, message := ~"unknown key"}}

A non-binary Reason is formatted with ~0p. An exception in the fun is caught and reported the same way, with the class, reason and stacktrace in message. The instance stays usable.

Where the fun runs

In the process that called wasmtime:call/3, while the instance thread waits. This is the erlang-python model: the caller is the callback handler. Consequences:

  • The fun can use the caller's state, dictionary and links.
  • The fun can read and write guest memory through Ctx; the guest is stopped.
  • The fun can call wasmtime:call on another instance. Calling the instance it runs on is refused with {error, #{kind := reentrant}}: the guest is parked waiting for this fun, so that call could never run. The same applies to a host process serving the call.

Serve host calls from another process

By default the fun runs in the process that called wasmtime:call. To keep callers plain clients and run every host fun in one dedicated process, name it at instantiate time and have it answer the messages:

Handler = spawn_link(fun Loop() ->
    receive
        {set, Inst} -> put(inst, Inst), Loop();
        Msg ->
            ok = wasmtime:handle_host_call(get(inst), Msg),
            Loop()
    end
end),
{ok, Inst} = wasmtime:instantiate(Mod, #{host => Handler, imports => Imports}),
Handler ! {set, Inst}.

handle_host_call/2 runs the import fun for a {wasmtime_host_call, Ref, HostId, Key, Args} message and replies to the guest; it returns ignore for any other message. Host calls made by the module's start section during instantiate/2 still go to the caller, which is the only process that has the instance at that point. If the handler process is gone the guest traps at once with message => ~"host process is gone".

Bound the wait

{ok, Inst} = wasmtime:instantiate(Mod, #{host_timeout => 1000, imports => ...}).

If a host function has not returned after host_timeout milliseconds (default 30000), the guest traps with message => ~"host function timed out". The Erlang fun itself is not killed; it runs to completion and its late result is dropped.

A timeout on wasmtime:call/4 does not help here: it fires from the calling process's receive, which is not running while that process executes the host fun. Use host_timeout for the guest side and keep host funs short, or hand long work to another process.

Notes

  • A host function is the guest asking Erlang and waiting for the answer. For the other direction, Erlang sending to a guest that keeps running, see streams.
  • Only function imports can be provided from Erlang. A memory, table or global import fails instantiation with kind => unsupported_import.
  • A host function can wait for the guest's own callback only by keeping the funcref it received and calling it with call_ref/3 after the call returns: call_ref from inside the fun is refused as reentrant, like any call on the instance the fun runs on. See references.
  • Imports with exception-typed parameters or results (exnref) are refused with kind => unsupported_type.
  • An import the module needs and the map does not provide fails instantiation with class => link. Extra keys in the map are ignored, so one map can serve several modules.
  • If the calling process dies during a host call, the instance is interrupted and the next queued call proceeds. wasmtime:interrupt/1 from another process during a host call ends the guest with kind => interrupt at once.