Wasmex.Components.HostResource (wasmex v0.15.1)

Copy Markdown

Defines a host-owned WebAssembly component resource implemented in Elixir.

Given this imported WIT resource:

interface counters {
  resource counter {
    constructor(initial: u32);
    with-value: static func(value: u32) -> counter;
    increment: func() -> u32;
    take-other: func(other: counter) -> u32;
  }
}

implement its callbacks and pass the generated imports to the component:

defmodule CounterHost do
  use Wasmex.Components.HostResource,
    wit_path: "wit",
    resource: "counter"

  def new(initial), do: start_counter(initial)
  def with_value(value), do: start_counter(value)
  def increment(counter), do: Agent.get_and_update(counter, &{&1 + 1, &1 + 1})

  # `other` is an owned argument, so this callback is now responsible for it.
  def take_other(counter, other) do
    value = Agent.get(counter, & &1) + Agent.get(other, & &1)
    Agent.stop(other)
    value
  end

  def drop(counter), do: Agent.stop(counter)

  defp start_counter(initial) do
    {:ok, counter} = Agent.start_link(fn -> initial end)
    counter
  end
end

{:ok, component} =
  Wasmex.Components.start_link(
    bytes: File.read!("component.wasm"),
    imports: CounterHost.imports()
  )

Callback mapping

A resource is represented by any opaque Elixir term. A PID, reference, ETS key, or struct containing those values works well when the resource has mutable state.

  • A WIT constructor maps to new/arity and returns the opaque term.
  • A method maps to its snake-case name and receives the opaque term first.
  • A static function maps to its snake-case name without a resource argument.
  • A resource returned directly or inside a tuple, option, result, record, variant, or list is represented by an opaque term in the matching Elixir position.
  • drop/1 is called when the guest destroys an owned handle. It defaults to a no-op and should be overridden when the term owns external state.

Passing borrow<T> to a callback leaves the guest handle live. Passing own<T> transfers ownership to the callback and removes the guest handle; drop/1 is not subsequently called for that handle. The callback must either release the state or return it in a resource-typed result.

Callback exceptions and incompatible return values trap the active component call. The component server itself remains alive. Wasmex releases any partially lowered Wasmtime handles, but a callback that allocated external state before returning an invalid value remains responsible for that state.

Multiple resources and worlds

Merge resources imported from the same or different interfaces with:

imports =
  Wasmex.Components.HostResource.merge_imports([
    CounterHost,
    LabelHost,
    %{
      "package:namespace/interface@version" => %{
        "freestanding-function" => {:fn, &MyHost.function/1}
      }
    }
  ])

Maps can therefore be included for freestanding imports that share an interface with a generated resource.

Set :world when the WIT package contains multiple worlds. If multiple imported interfaces contain the same resource name, select one with interface: "package:namespace/interface@version".

Use wit: source for a self-contained WIT package. Use wit_path: path for a WIT file or directory. A directory may contain the standard deps subdirectory, making wit_path: the best choice for packages that import WASI or other dependency packages. Files used beneath the path are registered as external compile resources, so changing them recompiles the host module.

User imports are linked after Wasmtime's built-in WASI interfaces and may intentionally override them. For example, a module selecting interface: "wasi:io/error@0.2.12" can implement that standard resource in Elixir. The component and host WIT versions must match exactly.

Summary

Functions

Deep-merges generated host-resource modules and ordinary import maps.

Functions

merge_imports(sources)

@spec merge_imports([module() | map()]) :: map()

Deep-merges generated host-resource modules and ordinary import maps.