lib/wasmex/store_or_caller.ex

defmodule Wasmex.StoreOrCaller do
  @moduledoc ~S"""
  Either a `Wasmex.Store` or "Caller" for imported functions.

  A Store is a collection of Wasm instances and host-defined state, see `Wasmex.Store`.
  A Caller takes the place of a Store in imported function calls. If a Store is needed in
  Elixir-provided imported functions, always use the provided Caller because
  using the Store will cause a deadlock (the running Wasm instance locks the Stores Mutex).

  When configured, a StoreOrCaller can consume fuel to halt or yield execution as desired.
  See `Wasmex.EngineConfig.consume_fuel/2` for more information on fuel consumption.
  """

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

  defstruct resource: nil,
            # The actual NIF store 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

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

  @doc ~S"""
  Adds fuel to this Store for Wasm to consume while executing.

  For this method to work, fuel consumption must be enabled via
  `Wasmex.EngineConfig.consume_fuel/2. By default a `Wasmex.Store`
  starts with 0 fuel for Wasm to execute with (meaning it will
  immediately trap and halt execution). This function must be
  called for the store to have some fuel to allow WebAssembly
  to execute.

  Most Wasm instructions consume 1 unit of fuel. Some
  instructions, such as `nop`, `drop`, `block`, and `loop`, consume 0
  units, as any execution cost associated with them involves other
  instructions which do consume fuel.

  Note that at this time when fuel is entirely consumed it will cause
  Wasm to trap.

  ## Errors

  This function will return an error if fuel consumption is not enabled
  via `Wasmex.EngineConfig.consume_fuel/2`.

  ## Examples

      iex> {:ok, engine} = Wasmex.Engine.new(%Wasmex.EngineConfig{consume_fuel: true})
      iex> {:ok, store} = Wasmex.Store.new(nil, engine)
      iex> Wasmex.StoreOrCaller.add_fuel(store, 10)
      :ok
  """
  @spec add_fuel(__MODULE__.t(), pos_integer()) :: :ok | {:error, binary()}
  def add_fuel(%__MODULE__{resource: resource}, fuel) do
    case Wasmex.Native.store_or_caller_add_fuel(resource, fuel) do
      {} -> :ok
      {:error, reason} -> {:error, reason}
    end
  end

  @doc ~S"""
  Synthetically consumes fuel from this Store.

  For this method to work fuel, consumption must be enabled via
  `Wasmex.EngineConfig.consume_fuel/2`.

  WebAssembly execution will automatically consume fuel but if so desired
  the embedder can also consume fuel manually to account for relative
  costs of host functions, for example.

  This function will attempt to consume `fuel` units of fuel from within
  this store. If the remaining amount of fuel allows this then `{:ok, N}`
  is returned where `N` is the amount of remaining fuel. Otherwise an
  error is returned and no fuel is consumed.

  ## Errors

  This function will return an error either if fuel consumption is not
  enabled via `Wasmex.EngineConfig.consume_fuel/2` or if `fuel` exceeds
  the amount of remaining fuel within this store.

  ## Examples

      iex> {:ok, engine} = Wasmex.Engine.new(%Wasmex.EngineConfig{consume_fuel: true})
      iex> {:ok, store} = Wasmex.Store.new(nil, engine)
      iex> Wasmex.StoreOrCaller.add_fuel(store, 10)
      iex> Wasmex.StoreOrCaller.fuel_remaining(store)
      {:ok, 10}
  """
  @spec consume_fuel(__MODULE__.t(), pos_integer() | 0) ::
          {:ok, pos_integer()} | {:error, binary()}
  def consume_fuel(%__MODULE__{resource: resource}, fuel) do
    case Wasmex.Native.store_or_caller_consume_fuel(resource, fuel) do
      {:error, reason} -> {:error, reason}
      fuel_remaining -> {:ok, fuel_remaining}
    end
  end

  @doc ~S"""
  Returns the amount of fuel available for future execution of this store.

  ## Examples

      iex> {:ok, engine} = Wasmex.Engine.new(%Wasmex.EngineConfig{consume_fuel: true})
      iex> {:ok, store} = Wasmex.Store.new(nil, engine)
      iex> Wasmex.StoreOrCaller.add_fuel(store, 10)
      iex> Wasmex.StoreOrCaller.fuel_remaining(store)
      {:ok, 10}
  """
  @spec fuel_remaining(__MODULE__.t()) :: {:ok, pos_integer()} | {:error, binary()}
  def fuel_remaining(%__MODULE__{} = store_or_caller) do
    consume_fuel(store_or_caller, 0)
  end

  @doc ~S"""
  Returns the amount of fuel consumed by this store's execution so far.

  Note that fuel, if enabled, must be initially added via
  `Wasmex.StoreOrCaller.add_fuel/2`.

  ## Errors

  If fuel consumption is not enabled via
  `Wasmex.EngineConfig.consume_fuel/2` then this function will return
  an error tuple.

  ## Examples

      iex> {:ok, engine} = Wasmex.Engine.new(%Wasmex.EngineConfig{consume_fuel: true})
      iex> {:ok, store} = Wasmex.Store.new(nil, engine)
      iex> Wasmex.StoreOrCaller.fuel_consumed(store)
      {:ok, 0}
      iex> Wasmex.StoreOrCaller.add_fuel(store, 10)
      iex> {:ok, _fuel} = Wasmex.StoreOrCaller.consume_fuel(store, 8)
      iex> Wasmex.StoreOrCaller.fuel_consumed(store)
      {:ok, 8}
  """
  @spec fuel_consumed(__MODULE__.t()) :: {:ok, pos_integer()} | {:error, binary()}
  def fuel_consumed(%__MODULE__{resource: resource}) do
    case Wasmex.Native.store_or_caller_fuel_consumed(resource) do
      {:error, reason} -> {:error, reason}
      nil -> {:error, "Could not consume fuel: fuel is not configured in this store"}
      fuel_consumed -> {:ok, fuel_consumed}
    end
  end
end

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

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