lib/wasmex/instance.ex

defmodule Wasmex.Instance do
  @moduledoc ~S"""
  Instantiates a Wasm module and allows calling exported functions on it.

  In the majority of cases, you will not need to use this module directly
  but use the main module `Wasmex` instead.
  This module expects to be executed within GenServer context which `Wasmex` sets up.
  """

  @type t :: %__MODULE__{
          resource: binary(),
          reference: reference()
        }

  defstruct resource: nil,
            # The actual NIF instance resource.
            # Normally the compiler will happily do stuff like inlining the
            # resource in attributes. This will convert the resource into an
            # empty binary with no warning. This will make that harder to
            # accidentally do.
            reference: nil

  defp __wrap_resource__(resource) do
    %__MODULE__{
      resource: resource,
      reference: make_ref()
    }
  end

  @doc ~S"""
  Instantiates a Wasm module with the given imports.

  Returns the instantiated Wasm instance.

  The `import` parameter is a nested map of Wasm namespaces.
  Each namespace consists of a name and a map of function names to function signatures.

  Function signatures are a tuple of the form `{:fn, arg_types, return_types, callback}`.
  Where `arg_types` and `return_types` are lists of `:i32`, `:i64`, `:f32`, `:f64`.

  Each `callback` function receives a `context` map as the first argument followed by the arguments specified in its signature.
  `context` has the following keys:

    * `:memory` - The default exported `Wasmex.Memory` of the Wasm instance
    * `:caller` - The caller of the Wasm instance which MUST be used instead of a `Wasmex.Store` in all Wasmex functions called from within the callback. Failure to do so will result in a deadlock. The `caller` MUST NOT be used outside of the callback.

  ## Example

  This example instantiates a Wasm module with one namespace `env` having
  three imported functions `imported_sum3`, `imported_sumf`, and `imported_void`.

  The imported function `imported_sum3` takes three `:i32` (32 bit integer) arguments and returns a `:i32` number.
  Its implementation is defined by the callback function `fn _context, a, b, c -> a + b + c end`.

      iex> %{store: store, module: module} = TestHelper.wasm_module()
      iex> imports = %{
      ...>   "env" =>
      ...>     %{
      ...>       "imported_sum3" => {:fn, [:i32, :i32, :i32], [:i32], fn _context, a, b, c -> a + b + c end},
      ...>       "imported_sumf" => {:fn, [:f32, :f32], [:f32], fn _context, a, b -> a + b end},
      ...>       "imported_void" => {:fn, [], [], fn _context -> nil end}
      ...>     }
      ...> }
      iex> {:ok, %Wasmex.Instance{}} = Wasmex.Instance.new(store, module, imports)
  """
  @spec new(Wasmex.StoreOrCaller.t(), Wasmex.Module.t(), %{
          optional(binary()) => (... -> any())
        }) ::
          {:ok, __MODULE__.t()} | {:error, binary()}
  def new(store_or_caller, module, imports) when is_map(imports) do
    %Wasmex.StoreOrCaller{resource: store_or_caller_resource} = store_or_caller
    %Wasmex.Module{resource: module_resource} = module

    case Wasmex.Native.instance_new(store_or_caller_resource, module_resource, imports) do
      {:error, err} -> {:error, err}
      resource -> {:ok, __wrap_resource__(resource)}
    end
  end

  @doc ~S"""
  Whether the Wasm `instance` exports a function with the given `name`.

  ## Example

      iex> %{store: store, module: module} = TestHelper.wasm_module()
      iex> {:ok, instance} = Wasmex.Instance.new(store, module, %{})
      iex> Wasmex.Instance.function_export_exists(store, instance, "sum")
      true
      iex> Wasmex.Instance.function_export_exists(store, instance, "does_not_exist")
      false
  """
  @spec function_export_exists(Wasmex.StoreOrCaller.t(), __MODULE__.t(), binary()) ::
          boolean()
  def function_export_exists(store_or_caller, instance, name) when is_binary(name) do
    %Wasmex.StoreOrCaller{resource: store_or_caller_resource} = store_or_caller
    %__MODULE__{resource: instance_resource} = instance

    Wasmex.Native.instance_function_export_exists(
      store_or_caller_resource,
      instance_resource,
      name
    )
  end

  @doc ~S"""
  Calls a function the given `name` exported by the Wasm `instance` with the given `params`.

  The Wasm function will be invoked asynchronously in a new OS thread.
  The calling Process/GenServer will receive a `{:returned_function_call, result, from}`
  message once execution finishes.
  The result either is an `{:error, reason}` or `:ok`.

  `call_exported_function/5` assumes to be called within a GenServer context, it expects a `from` argument
  as given by `c:GenServer.handle_call/3`. `from` is returned unchanged to allow
  the wrapping GenServer to reply to their caller.

  A BadArg exception may be thrown when given unexpected input data.

  ## Function parameters

  Parameters for Wasm functions are automatically casted to Wasm values.
  Note that WebAssembly only knows number datatypes (floats and integers of various sizes).

  You can pass arbitrary data to WebAssembly by writing that data into an instances `Wasmex.Memory`.
  The `memory/2` function returns the instances memory.

  ## Example

      iex> %{store: store, module: module} = TestHelper.wasm_module()
      iex> {:ok, instance} = Wasmex.Instance.new(store, module, %{})
      iex> Wasmex.Instance.call_exported_function(store, instance, "sum", [1, 2], :from)
      :ok
      iex> receive do
      ...>   {:returned_function_call, {:ok, [3]}, :from} -> :ok
      ...> after
      ...>  1000 -> raise "message_expected"
      ...> end
  """
  @spec call_exported_function(
          Wasmex.StoreOrCaller.t(),
          __MODULE__.t(),
          binary(),
          [any()],
          GenServer.from()
        ) ::
          :ok | {:error, binary()}
  def call_exported_function(store_or_caller, instance, name, params, from)
      when is_binary(name) do
    %{resource: store_or_caller_resource} = store_or_caller
    %__MODULE__{resource: instance_resource} = instance

    Wasmex.Native.instance_call_exported_function(
      store_or_caller_resource,
      instance_resource,
      name,
      params,
      from
    )
  end

  @doc ~S"""
  Returns the `Wasmex.Memory` of the Wasm `instance`.

  ## Example

      iex> %{store: store, module: module} = TestHelper.wasm_module()
      iex> {:ok, instance} = Wasmex.Instance.new(store, module, %{})
      iex> {:ok, %Wasmex.Memory{}} = Wasmex.Instance.memory(store, instance)
  """
  @spec memory(Wasmex.StoreOrCaller.t(), __MODULE__.t()) ::
          {:ok, Wasmex.Memory.t()} | {:error, binary()}
  def memory(store, instance) do
    Wasmex.Memory.from_instance(store, instance)
  end
end

defimpl Inspect, for: Wasmex.Instance do
  import Inspect.Algebra

  def inspect(dict, opts) do
    concat(["#Wasmex.Instance<", to_doc(dict.reference, opts), ">"])
  end
end