Packages

Run WebAssembly from Elixir. Load WASM modules in Rust, Go, C — call them like native functions.

Current section

Files

Jump to
firebird lib firebird.ex
Raw

lib/firebird.ex

defmodule Firebird do
@moduledoc """
Run WebAssembly from Elixir — load WASM modules written in Rust, Go, C,
or any language and call them like native functions.
## Quick Start (30 seconds)
# Verify setup
Firebird.check!() # => :ok
# One-liner: load → call → stop
{:ok, 8} = Firebird.run_one("math.wasm", :add, [5, 3])
# Load once, call many
wasm = Firebird.load!("math.wasm")
8 = Firebird.call_one!(wasm, :add, [5, 3])
55 = Firebird.call_one!(wasm, :fibonacci, [10])
Firebird.stop(wasm)
## `use Firebird` — Declarative Wrappers
defmodule MyApp.Math do
use Firebird, wasm: "priv/wasm/math.wasm"
wasm_fn :add, args: 2
wasm_fn :fibonacci, args: 1
end
# Add to supervision tree: children = [MyApp.Math]
# Then call: {:ok, [8]} = MyApp.Math.add(5, 3)
Or auto-discover all exports:
defmodule MyApp.Math do
use Firebird, wasm: "priv/wasm/math.wasm", auto: true
end
## API Overview
| Function | Returns |
|----------|---------|
| `run_one(path, fn, args)` | `{:ok, value}` — one-shot, single value |
| `run_one!(path, fn, args)` | `value` — one-shot, raises |
| `call_one(wasm, fn, args)` | `{:ok, value}` — single value |
| `call_one!(wasm, fn, args)` | `value` — raises |
| `call(wasm, fn, args)` | `{:ok, [results]}` — list |
| `call!(wasm, fn, args)` | `[results]` — raises |
| `with_instance(path, fun)` | `{:ok, result}` — auto-cleanup |
| `functions(wasm)` | `[%{name, params, results, arity}]` |
| `validate(source)` | `:ok` or `{:error, reason}` |
| `sample()` | `pid` — bundled sample for quick testing |
See `Firebird.Pool` for concurrent execution, `Firebird.Lazy` for
on-demand loading, `Firebird.Memory` for structured memory management
(strings, typed arrays, arena allocation), and `Firebird.Module` for
declarative wrappers.
"""
@doc """
Use Firebird in a module to create a WASM wrapper.
This is a convenient shortcut for `use Firebird.Module`.
## Examples
defmodule MyApp.Math do
use Firebird, wasm: "priv/wasm/math.wasm"
wasm_fn :add, args: 2
wasm_fn :multiply, args: 2
end
All options from `Firebird.Module` are supported:
- `:wasm` - Path to WASM file (required)
- `:name` - Process name (default: module name)
- `:auto` - Auto-discover exports (default: false)
- `:opts` - Options passed to `Firebird.load/2`
"""
defmacro __using__(opts) do
quote do
use Firebird.Module, unquote(opts)
end
end
@doc """
Load a WASM module from a file path or binary.
## Arguments
- `wasm_source` - Path to a .wasm file (string) or raw WASM bytes (binary)
- `opts` - Optional keyword list:
- `:memory_limit` - Maximum memory in bytes (default: 64MB)
- `:timeout` - Execution timeout in ms (default: 5000)
- `:wasi` - Enable WASI support (default: false)
## Examples
{:ok, instance} = Firebird.load("priv/wasm/math.wasm")
{:ok, instance} = Firebird.load(wasm_bytes)
{:ok, instance} = Firebird.load("app.wasm", wasi: true)
"""
@spec load(binary() | String.t(), keyword()) :: {:ok, pid()} | {:error, term()}
def load(wasm_source, opts \\ []) do
Firebird.Runtime.load(wasm_source, opts)
end
@doc """
Load a WASM module from the application's `priv/` directory.
This is the idiomatic way to load WASM files that ship with your app.
## Examples
# Loads from priv/wasm/math.wasm of your app
{:ok, instance} = Firebird.load_priv(:my_app, "wasm/math.wasm")
# Loads from priv/math.wasm of your app
{:ok, instance} = Firebird.load_priv(:my_app, "math.wasm")
"""
@spec load_priv(atom(), String.t(), keyword()) :: {:ok, pid()} | {:error, term()}
def load_priv(app, relative_path, opts \\ []) do
path = Path.join(:code.priv_dir(app) |> to_string(), relative_path)
load(path, opts)
end
@doc """
Load a WASM module from a file path explicitly.
## Examples
{:ok, instance} = Firebird.load_file("fixtures/math.wasm")
"""
@spec load_file(String.t(), keyword()) :: {:ok, pid()} | {:error, term()}
def load_file(path, opts \\ []) do
Firebird.Runtime.load_file(path, opts)
end
@doc """
Load a WASM module using the SyncNif fast path (~2μs per-call overhead).
Returns a reference (not a PID). Works with all `Firebird.Phoenix.*` modules.
~20x lower per-call overhead than `Firebird.load/2` which uses Wasmex (~40μs).
## Examples
{:ok, instance} = Firebird.load_sync("fixtures/phoenix_router.wasm")
{:ok, match} = Firebird.Phoenix.Router.match_compiled(instance, "GET", "/users/42")
"""
@spec load_sync(String.t()) :: {:ok, reference()} | {:error, term()}
def load_sync(path) when is_binary(path) do
resolved = Firebird.Runtime.resolve_wasm_path(path)
Firebird.SyncNif.load(resolved)
end
@doc """
Call an exported function on a WASM instance.
## Examples
{:ok, [8]} = Firebird.call(instance, :add, [5, 3])
{:ok, [55]} = Firebird.call(instance, "fibonacci", [10])
"""
@spec call(pid(), atom() | String.t(), list()) :: {:ok, list()} | {:error, term()}
def call(instance, function, args) when is_list(args) do
Firebird.Runtime.call(instance, function, args)
end
def call(_instance, _function, args) when not is_list(args) do
raise ArgumentError, """
Firebird.call/3 expects args as a list, got: #{inspect(args)}
Example: Firebird.call(wasm, :add, [5, 3])
Firebird.call(wasm, :fibonacci, [10])
"""
end
@doc """
Call an exported function, raising on error.
## Examples
[8] = Firebird.call!(instance, :add, [5, 3])
"""
@spec call!(pid(), atom() | String.t(), list()) :: list()
def call!(instance, function, args) when is_list(args) do
Firebird.Runtime.call!(instance, function, args)
end
def call!(_instance, _function, args) when not is_list(args) do
raise ArgumentError, """
Firebird.call!/3 expects args as a list, got: #{inspect(args)}
Example: Firebird.call!(wasm, :add, [5, 3])
"""
end
@doc """
Call multiple functions in sequence.
## Examples
{:ok, [[3], [12]]} = Firebird.call_many(instance, [
{:add, [1, 2]},
{:multiply, [3, 4]}
])
"""
@spec call_many(pid(), [{atom() | String.t(), list()}]) :: {:ok, [list()]} | {:error, term()}
def call_many(instance, calls) when is_list(calls) do
Firebird.Runtime.call_many(instance, calls)
end
@doc """
Check if a function exists in the WASM module.
## Examples
true = Firebird.function_exists?(instance, :add)
false = Firebird.function_exists?(instance, :nonexistent)
"""
@spec function_exists?(pid(), atom() | String.t()) :: boolean()
def function_exists?(instance, function) do
Firebird.Runtime.function_exists?(instance, function)
end
@doc """
List all exported functions from a WASM instance.
## Examples
exports = Firebird.exports(instance)
# => [:add, :multiply, :fibonacci]
"""
@spec exports(pid()) :: [atom()]
def exports(instance) do
Firebird.Runtime.exports(instance)
end
@doc """
List all exports with their types.
## Examples
all = Firebird.all_exports(instance)
# => [{"add", :function}, {"memory", :memory}]
"""
@spec all_exports(pid()) :: [{String.t(), atom()}]
def all_exports(instance) do
Firebird.Runtime.all_exports(instance)
end
@doc """
List all exported functions with their type signatures.
Returns a list of maps with `:name`, `:params`, `:results`, and `:arity` keys.
This is a richer alternative to `exports/1` for understanding a module.
## Examples
functions = Firebird.functions(instance)
# => [
# %{name: :add, params: [:i32, :i32], results: [:i32], arity: 2},
# %{name: :fibonacci, params: [:i32], results: [:i32], arity: 1},
# ...
# ]
"""
@spec functions(pid()) :: [map()]
def functions(instance) do
for func <- exports(instance) do
case function_type(instance, func) do
{:ok, {params, results}} ->
%{name: func, params: params, results: results, arity: length(params)}
_ ->
%{name: func, params: [], results: [], arity: 0}
end
end
end
@doc """
Get the type signature of an exported function.
## Examples
{:ok, {[:i32, :i32], [:i32]}} = Firebird.function_type(instance, :add)
"""
@spec function_type(pid(), atom() | String.t()) ::
{:ok, {list(), list()}} | {:error, :not_found}
def function_type(instance, function) do
Firebird.Runtime.function_type(instance, function)
end
@doc """
Read bytes from WASM linear memory.
## Examples
{:ok, bytes} = Firebird.read_memory(instance, 0, 10)
"""
@spec read_memory(pid(), non_neg_integer(), non_neg_integer()) ::
{:ok, binary()} | {:error, term()}
def read_memory(instance, offset, length) do
Firebird.Runtime.read_memory(instance, offset, length)
end
@doc """
Write bytes to WASM linear memory.
## Examples
:ok = Firebird.write_memory(instance, 0, <<1, 2, 3>>)
"""
@spec write_memory(pid(), non_neg_integer(), binary()) :: :ok | {:error, term()}
def write_memory(instance, offset, data) do
Firebird.Runtime.write_memory(instance, offset, data)
end
@doc """
Get the size of WASM linear memory in bytes.
## Examples
{:ok, size} = Firebird.memory_size(instance)
"""
@spec memory_size(pid()) :: {:ok, non_neg_integer()} | {:error, term()}
def memory_size(instance) do
Firebird.Runtime.memory_size(instance)
end
@doc """
Grow WASM linear memory by pages (each page = 64KB).
## Examples
{:ok, old_pages} = Firebird.grow_memory(instance, 1)
"""
@spec grow_memory(pid(), non_neg_integer()) :: {:ok, non_neg_integer()} | {:error, term()}
def grow_memory(instance, pages) do
Firebird.Runtime.grow_memory(instance, pages)
end
@doc """
Stop a WASM instance and free resources.
## Examples
:ok = Firebird.stop(instance)
"""
@spec stop(pid() | reference()) :: :ok
def stop(instance) when is_reference(instance) do
# SyncNif references are garbage collected, no explicit stop needed
:ok
end
def stop(instance) do
Firebird.Runtime.stop(instance)
catch
:exit, _ -> :ok
end
@doc """
Check if a WASM instance is alive.
## Examples
true = Firebird.alive?(instance)
"""
@spec alive?(pid()) :: boolean()
def alive?(instance) do
Firebird.Runtime.alive?(instance)
end
@doc """
Serialize a compiled module for caching.
## Examples
{:ok, bytes} = Firebird.serialize(instance)
"""
@spec serialize(pid()) :: {:ok, binary()} | {:error, term()}
def serialize(instance) do
Firebird.Runtime.serialize(instance)
end
@doc """
Get a summary of a WASM instance: exports, memory size, alive status.
## Examples
info = Firebird.info(instance)
# => %{alive: true, exports: [:add, :multiply], memory_bytes: 65536}
"""
@spec info(pid()) :: map()
def info(instance) do
is_alive = alive?(instance)
if is_alive do
%{
alive: true,
exports: exports(instance),
memory_bytes:
case memory_size(instance) do
{:ok, size} -> size
_ -> nil
end
}
else
%{alive: false, exports: [], memory_bytes: nil}
end
end
# ---------------------------------------------------------------------------
# Convenience APIs
# ---------------------------------------------------------------------------
@doc """
Load a WASM module, raising on error.
## Examples
instance = Firebird.load!("priv/wasm/math.wasm")
"""
@spec load!(binary() | String.t(), keyword()) :: pid()
def load!(wasm_source, opts \\ []) do
case load(wasm_source, opts) do
{:ok, pid} ->
pid
error ->
raise_wasm_error!(nil, [], error)
end
end
@doc """
Load a WASM module, call a function, and stop the instance — all in one step.
Perfect for one-off computations where you don't need to keep the instance alive.
## Examples
iex> Firebird.run("fixtures/math.wasm", :add, [5, 3])
{:ok, [8]}
iex> Firebird.run("fixtures/math.wasm", :fibonacci, [10])
{:ok, [55]}
"""
@spec run(binary() | String.t(), atom() | String.t(), list(), keyword()) ::
{:ok, list()} | {:error, term()}
def run(wasm_source, function, args, opts \\ []) do
with {:ok, pid} <- load(wasm_source, opts) do
try do
call(pid, function, args)
after
stop(pid)
end
end
end
@doc """
Load, call, and stop — raising on error.
## Examples
iex> Firebird.run!("fixtures/math.wasm", :add, [5, 3])
[8]
iex> Firebird.run!("fixtures/math.wasm", :fibonacci, [10])
[55]
"""
@spec run!(binary() | String.t(), atom() | String.t(), list(), keyword()) :: list()
def run!(wasm_source, function, args, opts \\ []) do
case run(wasm_source, function, args, opts) do
{:ok, results} ->
results
error ->
raise_wasm_error!(function, args, error)
end
end
@doc """
Execute a block with a WASM instance, automatically cleaning up afterward.
## Examples
iex> Firebird.with_instance("fixtures/math.wasm", fn instance ->
...> {:ok, [a]} = Firebird.call(instance, :add, [5, 3])
...> {:ok, [b]} = Firebird.call(instance, :multiply, [a, 2])
...> b
...> end)
{:ok, 16}
"""
@spec with_instance(binary() | String.t(), (pid() -> term()), keyword()) ::
{:ok, term()} | {:error, term()}
def with_instance(wasm_source, fun, opts \\ []) when is_function(fun, 1) do
case load(wasm_source, opts) do
{:ok, pid} ->
try do
{:ok, fun.(pid)}
after
stop(pid)
end
{:error, _} = error ->
error
end
end
@doc """
Returns the Firebird library version.
## Examples
iex> Firebird.version()
"1.0.0"
"""
@spec version() :: String.t()
def version do
"1.0.0"
end
@doc """
Load a WASM module from bytes (binary data).
Explicit API for loading from in-memory WASM bytes rather than file paths.
Useful when you generate or fetch WASM dynamically.
## Examples
bytes = File.read!("math.wasm")
{:ok, instance} = Firebird.load_bytes(bytes)
"""
@spec load_bytes(binary(), keyword()) :: {:ok, pid()} | {:error, term()}
def load_bytes(bytes, opts \\ []) when is_binary(bytes) do
load(bytes, opts)
end
# ---------------------------------------------------------------------------
# Pipe-friendly API
# ---------------------------------------------------------------------------
@doc """
Pipe-friendly call that threads the instance through.
Returns `{:ok, results, instance}` so you can pipe further calls.
## Examples
{:ok, instance} = Firebird.load("math.wasm")
{:ok, [sum], instance} = Firebird.pipe(instance, :add, [5, 3])
{:ok, [product], instance} = Firebird.pipe(instance, :multiply, [sum, 2])
Firebird.stop(instance)
# Or use the pipeline operator:
{:ok, wasm} = Firebird.load("math.wasm")
{:ok, [8], wasm} = Firebird.pipe(wasm, :add, [5, 3])
"""
@spec pipe(pid(), atom() | String.t(), list()) :: {:ok, list(), pid()} | {:error, term()}
def pipe(instance, function, args) when is_list(args) do
case call(instance, function, args) do
{:ok, results} -> {:ok, results, instance}
error -> error
end
end
@doc """
Pipe-friendly call that raises on error and returns `{results, instance}`.
## Examples
wasm = Firebird.load!("math.wasm")
{[sum], wasm} = Firebird.pipe!(wasm, :add, [5, 3])
{[product], wasm} = Firebird.pipe!(wasm, :multiply, [sum, 2])
Firebird.stop(wasm)
"""
@spec pipe!(pid(), atom() | String.t(), list()) :: {list(), pid()}
def pipe!(instance, function, args) when is_list(args) do
{call!(instance, function, args), instance}
end
# ---------------------------------------------------------------------------
# Inspection convenience
# ---------------------------------------------------------------------------
@doc """
Inspect a WASM file and return information about its exports.
Convenience wrapper around `Firebird.Inspector.inspect_file/2`.
## Examples
{:ok, info} = Firebird.inspect_file("math.wasm")
info.functions
# => [%{name: "add", params: [:i32, :i32], results: [:i32], arity: 2}, ...]
"""
@spec inspect_file(String.t(), keyword()) :: {:ok, map()} | {:error, term()}
defdelegate inspect_file(path, opts \\ []), to: Firebird.Inspector
# ---------------------------------------------------------------------------
# Pool convenience wrappers
# ---------------------------------------------------------------------------
@doc """
Start a pool of WASM instances for concurrent execution.
## Options
- `:wasm` or `:wasm_path` - Path to WASM file
- `:wasm_bytes` - Raw WASM bytes
- `:size` or `:pool_size` - Number of instances (default: schedulers_online)
- `:name` - Optional GenServer name
## Examples
{:ok, pool} = Firebird.start_pool(wasm: "math.wasm", size: 4)
{:ok, [8]} = Firebird.pool_call(pool, :add, [5, 3])
Firebird.stop_pool(pool)
"""
@spec start_pool(keyword()) :: {:ok, pid()} | {:error, term()}
def start_pool(opts) do
Firebird.Pool.start_link(opts)
end
@doc """
Call a WASM function using a pooled instance.
## Examples
{:ok, [8]} = Firebird.pool_call(pool, :add, [5, 3])
"""
@spec pool_call(GenServer.server(), atom() | String.t(), list()) ::
{:ok, list()} | {:error, term()}
def pool_call(pool, function, args) do
Firebird.Pool.call(pool, function, args)
end
@doc """
Call a WASM function using a pooled instance, raising on error.
## Examples
[8] = Firebird.pool_call!(pool, :add, [5, 3])
"""
@spec pool_call!(GenServer.server(), atom() | String.t(), list()) :: list()
def pool_call!(pool, function, args) do
Firebird.Pool.call!(pool, function, args)
end
@doc """
Call a WASM function using a pooled instance, returning unwrapped single value.
## Examples
{:ok, 8} = Firebird.pool_call_one(pool, :add, [5, 3])
"""
@spec pool_call_one(GenServer.server(), atom() | String.t(), list()) ::
{:ok, term()} | {:error, term()}
def pool_call_one(pool, function, args) do
Firebird.Pool.call_one(pool, function, args)
end
@doc """
Call a WASM function using a pooled instance, returning unwrapped single value, raising on error.
## Examples
8 = Firebird.pool_call_one!(pool, :add, [5, 3])
"""
@spec pool_call_one!(GenServer.server(), atom() | String.t(), list()) :: term()
def pool_call_one!(pool, function, args) do
Firebird.Pool.call_one!(pool, function, args)
end
@doc """
Stop a pool and all its instances.
"""
@spec stop_pool(GenServer.server()) :: :ok
def stop_pool(pool) do
Firebird.Pool.stop(pool)
catch
:exit, _ -> :ok
end
# ---------------------------------------------------------------------------
# WAT (WebAssembly Text) support
# ---------------------------------------------------------------------------
@doc """
Load a WASM module from WAT (WebAssembly Text Format) source code.
This is great for prototyping and learning — write WASM in human-readable
text format and load it directly. Requires `wat2wasm` to be installed.
## Examples
{:ok, wasm} = Firebird.from_wat(\"""
(module
(func (export "add") (param i32 i32) (result i32)
local.get 0
local.get 1
i32.add)
)
\""")
{:ok, [8]} = Firebird.call(wasm, :add, [5, 3])
Firebird.stop(wasm)
"""
@spec from_wat(String.t(), keyword()) :: {:ok, pid()} | {:error, term()}
def from_wat(wat_source, opts \\ []) do
with {:ok, wasm_bytes} <- compile_wat(wat_source) do
load(wasm_bytes, opts)
end
end
@doc """
Load a WASM module from WAT source, raising on error.
## Examples
wasm = Firebird.from_wat!(\"""
(module
(func (export "double") (param i32) (result i32)
local.get 0
i32.const 2
i32.mul)
)
\""")
[10] = Firebird.call!(wasm, :double, [5])
"""
@spec from_wat!(String.t(), keyword()) :: pid()
def from_wat!(wat_source, opts \\ []) do
case from_wat(wat_source, opts) do
{:ok, pid} ->
pid
{:error, {:wat_compile_error, msg}} ->
raise Firebird.WasmError.load_failed({:wat_compile_error, msg})
{:error, {:wat2wasm_not_found, _}} ->
raise %Firebird.WasmError{
message: """
wat2wasm not found. Install it:
• macOS: brew install wabt
• Ubuntu: apt install wabt
• Or download from: https://github.com/WebAssembly/wabt/releases
""",
reason: :wat2wasm_not_found
}
{:error, reason} ->
raise Firebird.WasmError.load_failed(reason)
end
end
@doc """
Compile WAT source to WASM binary bytes.
## Examples
{:ok, bytes} = Firebird.compile_wat("(module)")
"""
@spec compile_wat(String.t()) :: {:ok, binary()} | {:error, term()}
def compile_wat(wat_source) do
case System.find_executable("wat2wasm") do
nil ->
{:error,
{:wat2wasm_not_found,
"Install wabt: brew install wabt (macOS) or apt install wabt (Ubuntu)"}}
wat2wasm ->
tmp_wat =
Path.join(System.tmp_dir!(), "firebird_#{:erlang.unique_integer([:positive])}.wat")
tmp_wasm = Path.rootname(tmp_wat) <> ".wasm"
try do
File.write!(tmp_wat, wat_source)
case System.cmd(wat2wasm, [tmp_wat, "-o", tmp_wasm], stderr_to_stdout: true) do
{_, 0} ->
wasm_bytes = File.read!(tmp_wasm)
{:ok, wasm_bytes}
{error_output, _} ->
{:error, {:wat_compile_error, error_output}}
end
after
File.rm(tmp_wat)
File.rm(tmp_wasm)
end
end
end
# ---------------------------------------------------------------------------
# Quick describe
# ---------------------------------------------------------------------------
@doc """
Print a human-readable summary of a WASM file or instance.
Works with file paths or running instances.
## Examples
Firebird.describe("path/to/math.wasm")
Firebird.describe(instance)
"""
@spec describe(pid() | String.t()) :: :ok
def describe(instance) when is_pid(instance) do
info = info(instance)
IO.puts("🔥 WASM Instance (#{if info.alive, do: "alive", else: "stopped"})")
IO.puts(" Memory: #{format_bytes(info.memory_bytes || 0)}")
IO.puts(" Exports (#{length(info.exports)}):")
for func <- info.exports do
case function_type(instance, func) do
{:ok, {params, results}} ->
IO.puts(
" #{func}(#{Enum.join(Enum.map(params, &inspect/1), ", ")}) → #{inspect(results)}"
)
_ ->
IO.puts(" #{func}")
end
end
:ok
end
def describe(path) when is_binary(path) do
# Use smart path resolution for bare filenames
resolved = Firebird.Runtime.resolve_wasm_path(path)
case inspect_file(resolved) do
{:ok, info} ->
IO.puts("🔥 #{Path.basename(resolved)} (#{format_bytes(info.size_bytes)})")
IO.puts(" Functions (#{length(info.functions)}):")
for f <- info.functions do
IO.puts(
" #{f.name}(#{Enum.join(Enum.map(f.params, &inspect/1), ", ")}) → #{inspect(f.results)}"
)
end
:ok
{:error, reason} ->
IO.puts("❌ Cannot inspect #{path}: #{inspect(reason)}")
:ok
end
end
defp format_bytes(0), do: "0 B"
defp format_bytes(bytes) when bytes < 1024, do: "#{bytes} B"
defp format_bytes(bytes) when bytes < 1_048_576, do: "#{Float.round(bytes / 1024, 1)} KB"
defp format_bytes(bytes), do: "#{Float.round(bytes / 1_048_576, 1)} MB"
# ---------------------------------------------------------------------------
# Setup verification & bundled sample
# ---------------------------------------------------------------------------
@doc """
Returns the path to the bundled sample math.wasm file.
This WASM module exports `add/2`, `multiply/2`, and `fibonacci/1` functions
and is included with Firebird for testing and getting started quickly.
## Examples
iex> path = Firebird.sample_wasm_path()
iex> File.exists?(path)
true
iex> String.ends_with?(path, "sample_math.wasm")
true
"""
@spec sample_wasm_path() :: String.t()
def sample_wasm_path do
Path.join(:code.priv_dir(:firebird), "wasm/sample_math.wasm")
end
@doc """
Load a pre-loaded sample WASM instance for quick experimentation.
Shortcut for `Firebird.load!(Firebird.sample_wasm_path())`.
Perfect for `iex` sessions:
iex> wasm = Firebird.sample()
iex> is_pid(wasm) and Process.alive?(wasm)
true
iex> Firebird.call_one!(wasm, :add, [5, 3])
8
iex> Firebird.stop(wasm)
:ok
"""
@spec sample() :: pid()
def sample do
load!(sample_wasm_path())
end
@doc """
Validate that a WASM file or binary can be loaded without keeping the instance.
Returns `:ok` if the WASM is valid and loadable, or `{:error, reason}` otherwise.
Useful for build-time validation or CI checks.
## Examples
iex> Firebird.validate("fixtures/math.wasm")
:ok
iex> match?({:error, _}, Firebird.validate("nonexistent_file.wasm"))
true
"""
@spec validate(binary() | String.t()) :: :ok | {:error, term()}
def validate(wasm_source) do
case load(wasm_source) do
{:ok, pid} ->
stop(pid)
:ok
{:error, _} = error ->
error
end
end
@doc """
Verify that Firebird is correctly installed and working.
Loads the bundled sample WASM module, calls `add(2, 3)`, and verifies
the result is `[5]`. Returns `:ok` on success, raises on failure.
Use this to quickly confirm your setup is working:
iex> Firebird.check!()
:ok
## What it checks
1. Wasmex NIF is loaded and working
2. WASM module loading works
3. Function calls work correctly
4. Results are returned properly
"""
@spec check!() :: :ok
def check! do
path = sample_wasm_path()
unless File.exists?(path) do
raise """
Firebird sample WASM file not found at: #{path}
This usually means the :firebird dependency wasn't compiled properly.
Try: mix deps.compile firebird --force
"""
end
case run(path, :add, [2, 3]) do
{:ok, [5]} ->
:ok
{:ok, other} ->
raise "Firebird check failed: expected [5], got #{inspect(other)}"
{:error, reason} ->
raise """
Firebird check failed: #{inspect(reason)}
Common fixes:
• mix deps.get
• mix deps.compile wasmex --force
• Ensure your system has a compatible Rust toolchain for wasmex
"""
end
end
@doc """
Start a playground with a pre-loaded sample WASM instance.
Returns a WASM instance loaded with the bundled sample math module.
Perfect for exploring in `iex`:
iex> wasm = Firebird.playground()
iex> Firebird.call!(wasm, :add, [5, 3])
[8]
iex> Firebird.call!(wasm, :fibonacci, [10])
[55]
iex> Firebird.stop(wasm)
:ok
"""
@spec playground() :: pid()
def playground do
wasm = load!(sample_wasm_path())
IO.puts("🔥 Firebird playground ready! Instance: #{inspect(wasm)}")
IO.puts(" Try: Firebird.call!(wasm, :add, [5, 3])")
IO.puts(" Try: Firebird.call!(wasm, :fibonacci, [10])")
IO.puts(" Try: Firebird.describe(wasm)")
IO.puts(" Done: Firebird.stop(wasm)")
wasm
end
@doc """
Non-raising version of `check!/0`. Returns `:ok` or `{:error, reason}`.
## Examples
iex> Firebird.check()
:ok
"""
@spec check() :: :ok | {:error, term()}
def check do
check!()
rescue
e -> {:error, Exception.message(e)}
end
@doc """
Run a quick demo showing Firebird in action.
Loads the bundled sample WASM, runs a few operations, and prints the results.
Great for verifying setup in `iex`.
Firebird.demo()
# 🔥 Firebird Demo
# Loading sample_math.wasm...
# add(5, 3) = 8
# ...
"""
@spec demo() :: :ok
def demo do
path = sample_wasm_path()
IO.puts("🔥 Firebird Demo")
IO.puts(" Loading #{Path.basename(path)}...")
{:ok, wasm} = load(path)
exports = exports(wasm)
IO.puts(" Exports: #{inspect(exports)}")
{:ok, [sum]} = call(wasm, :add, [5, 3])
IO.puts(" add(5, 3) = #{sum}")
{:ok, [product]} = call(wasm, :multiply, [4, 7])
IO.puts(" multiply(4, 7) = #{product}")
{:ok, [fib]} = call(wasm, :fibonacci, [10])
IO.puts(" fibonacci(10) = #{fib}")
stop(wasm)
IO.puts(" ✅ Everything works!")
:ok
end
# ---------------------------------------------------------------------------
# Map convenience
# ---------------------------------------------------------------------------
@doc """
Map a WASM function over a list of argument lists.
Returns a list of unwrapped single values. Perfect for batch processing.
## Examples
wasm = Firebird.load!("math.wasm")
results = Firebird.map(wasm, :fibonacci, [[1], [2], [3], [4], [5]])
# => [1, 1, 2, 3, 5]
results = Firebird.map(wasm, :add, [[1, 2], [3, 4], [5, 6]])
# => [3, 7, 11]
Firebird.stop(wasm)
"""
@spec map(pid(), atom() | String.t(), [list()]) :: [term()]
def map(instance, function, args_list) when is_list(args_list) do
Enum.map(args_list, fn args ->
call_one!(instance, function, args)
end)
end
# ---------------------------------------------------------------------------
# Single-value convenience APIs
# ---------------------------------------------------------------------------
@doc """
Call a WASM function and return a single unwrapped value.
Most WASM functions return exactly one value. This avoids the `[value]`
list wrapping that `call/3` uses. For multi-value returns, use `call/3`.
## Examples
{:ok, 8} = Firebird.call_one(instance, :add, [5, 3])
{:ok, 55} = Firebird.call_one(instance, :fibonacci, [10])
"""
@spec call_one(pid(), atom() | String.t(), list()) :: {:ok, term()} | {:error, term()}
def call_one(instance, function, args) do
case call(instance, function, args) do
{:ok, [single]} -> {:ok, single}
{:ok, multi} -> {:ok, multi}
error -> error
end
end
@doc """
Call a WASM function and return a single unwrapped value, raising on error.
## Examples
8 = Firebird.call_one!(instance, :add, [5, 3])
55 = Firebird.call_one!(instance, :fibonacci, [10])
"""
@spec call_one!(pid(), atom() | String.t(), list()) :: term()
def call_one!(instance, function, args) do
case call_one(instance, function, args) do
{:ok, result} ->
result
{:error, {:call_error, func_name, :instance_stopped}} ->
raise Firebird.WasmError.instance_stopped(func_name)
{:error, {:call_error, func_name, reason}} ->
if Process.alive?(instance) do
available = exports(instance)
if to_string(func_name) not in Enum.map(available, &to_string/1) do
raise Firebird.WasmError.function_not_found(func_name, available)
else
raise Firebird.WasmError.call_failed(func_name, reason, args)
end
else
raise Firebird.WasmError.instance_stopped(func_name)
end
{:error, reason} ->
raise Firebird.WasmError.call_failed(function, reason, args)
end
end
@doc """
One-shot: load, call (single-value), and stop — all in one step.
Like `run/4` but unwraps single-value results automatically.
## Examples
iex> Firebird.run_one("fixtures/math.wasm", :add, [5, 3])
{:ok, 8}
iex> Firebird.run_one("fixtures/math.wasm", :fibonacci, [10])
{:ok, 55}
"""
@spec run_one(binary() | String.t(), atom() | String.t(), list(), keyword()) ::
{:ok, term()} | {:error, term()}
def run_one(wasm_source, function, args, opts \\ []) do
case run(wasm_source, function, args, opts) do
{:ok, [single]} -> {:ok, single}
{:ok, multi} -> {:ok, multi}
error -> error
end
end
@doc """
One-shot: load, call (single-value), and stop — raising on error.
## Examples
iex> Firebird.run_one!("fixtures/math.wasm", :add, [5, 3])
8
iex> Firebird.run_one!("fixtures/math.wasm", :fibonacci, [10])
55
"""
@spec run_one!(binary() | String.t(), atom() | String.t(), list(), keyword()) :: term()
def run_one!(wasm_source, function, args, opts \\ []) do
case run_one(wasm_source, function, args, opts) do
{:ok, result} ->
result
error ->
raise_wasm_error!(function, args, error)
end
end
# ---------------------------------------------------------------------------
# Result unwrapping
# ---------------------------------------------------------------------------
@doc """
Unwrap a single-value WASM result.
WASM functions return results as lists. This pipes nicely to extract
single values:
{:ok, 8} = Firebird.call(wasm, :add, [5, 3]) |> Firebird.unwrap()
{:ok, 8} = Firebird.run("math.wasm", :add, [5, 3]) |> Firebird.unwrap()
"""
@spec unwrap({:ok, list()} | {:error, term()}) :: {:ok, term()} | {:error, term()}
defdelegate unwrap(result), to: Firebird.Unwrap
@doc """
Unwrap a single-value WASM result from a bang call.
8 = Firebird.call!(wasm, :add, [5, 3]) |> Firebird.unwrap!()
8 = Firebird.run!("math.wasm", :add, [5, 3]) |> Firebird.unwrap!()
"""
@spec unwrap!(list()) :: term()
defdelegate unwrap!(result), to: Firebird.Unwrap
# ---------------------------------------------------------------------------
# Builder API
# ---------------------------------------------------------------------------
@doc """
Build a WASM module from a source directory (Go or Rust).
Auto-detects the project type and compiles using the appropriate toolchain.
## Examples
{:ok, wasm_path} = Firebird.build("src/go_math")
{:ok, wasm_path} = Firebird.build("src/rust_math", output: "priv/wasm/math.wasm")
"""
@spec build(String.t(), keyword()) :: {:ok, String.t()} | {:error, term()}
defdelegate build(source_dir, opts \\ []), to: Firebird.Builder
@doc """
Build a WASM module and immediately load it.
Auto-detects WASI requirements for Go projects.
## Examples
{:ok, instance} = Firebird.build_and_load("fixtures/go_math")
{:ok, [8]} = Firebird.call(instance, :add, [5, 3])
"""
@spec build_and_load(String.t(), keyword()) :: {:ok, pid()} | {:error, term()}
defdelegate build_and_load(source_dir, opts \\ []), to: Firebird.Builder
# ---------------------------------------------------------------------------
# Cache API
# ---------------------------------------------------------------------------
@doc """
Load a WASM module with caching for fast subsequent loads.
On first load, compiles the module and caches the serialized form.
On subsequent loads, deserializes from cache (significantly faster).
## Examples
{:ok, instance} = Firebird.load_cached("math.wasm")
{:ok, instance} = Firebird.load_cached("math.wasm", cache_dir: "/tmp/cache")
"""
@spec load_cached(String.t(), keyword()) :: {:ok, pid()} | {:error, term()}
defdelegate load_cached(wasm_path, opts \\ []), to: Firebird.Cache
# ---------------------------------------------------------------------------
# Preloader API
# ---------------------------------------------------------------------------
@doc """
Precompile a WASM module for fast multi-instance creation.
Compiles the WASM bytes once, then allows creating instances
without recompilation. Essential for pools and concurrent execution.
## Examples
{:ok, compiled} = Firebird.precompile("math.wasm")
{:ok, inst1} = Firebird.instantiate(compiled)
{:ok, inst2} = Firebird.instantiate(compiled)
"""
@spec precompile(binary() | String.t(), keyword()) ::
{:ok, Firebird.Preloader.compiled()} | {:error, term()}
defdelegate precompile(wasm_source, opts \\ []), to: Firebird.Preloader, as: :compile
@doc """
Create a new instance from a precompiled WASM module.
Much faster than `load/2` because it skips compilation.
## Examples
{:ok, compiled} = Firebird.precompile("math.wasm")
{:ok, instance} = Firebird.instantiate(compiled)
"""
@spec instantiate(Firebird.Preloader.compiled()) :: {:ok, pid()} | {:error, term()}
defdelegate instantiate(compiled), to: Firebird.Preloader
# ---------------------------------------------------------------------------
# Memory Management API
# ---------------------------------------------------------------------------
@doc """
Create a structured memory allocator for a WASM instance.
Returns a `Firebird.Memory` struct with typed read/write helpers,
bump allocation, and arena-style deallocation. Essential for passing
strings, arrays, and complex data to/from Go/Rust WASM modules.
## Options
- `:alignment` — byte alignment for allocations (default: 8)
- `:base_offset` — starting offset in linear memory (default: 1024)
## Examples
wasm = Firebird.load!("string_utils.wasm")
alloc = Firebird.memory_allocator(wasm)
# Write string, call WASM, read result
{alloc, ptr, len} = Firebird.Memory.write_string(alloc, "hello")
{:ok, [hash]} = Firebird.call(wasm, :hash, [ptr, len])
# Arena-style cleanup
alloc = Firebird.Memory.free_all(alloc)
See `Firebird.Memory` for the full API including typed arrays,
scalar helpers, secure free, and scoped arenas.
"""
@spec memory_allocator(pid(), keyword()) :: Firebird.Memory.t()
def memory_allocator(instance, opts \\ []) do
Firebird.Memory.new(instance, opts)
end
# ---------------------------------------------------------------------------
# Pipeline API
# ---------------------------------------------------------------------------
@doc """
Create a new WASM function pipeline for chaining calls.
## Examples
{:ok, [result]} =
Firebird.pipeline(instance)
|> Firebird.Pipeline.call(:add, [5, 3])
|> Firebird.Pipeline.call(:multiply, [:_, 2])
|> Firebird.Pipeline.run()
"""
@spec pipeline(pid()) :: Firebird.Pipeline.t()
def pipeline(instance), do: Firebird.Pipeline.new(instance)
# ---------------------------------------------------------------------------
# Quick / Inline Execution
# ---------------------------------------------------------------------------
@doc """
Execute a WASM function with the absolute minimum ceremony.
Accepts a `.wasm` file path (auto-resolved) or inline WAT source.
Returns the single unwrapped value.
## Examples
# With a file — auto-resolves from priv/wasm, wasm/, fixtures/
8 = Firebird.quick("math.wasm", :add, [5, 3])
# With inline WAT
42 = Firebird.quick(~s|(module (func (export "answer") (result i32) i32.const 42))|, :answer, [])
# With the bundled sample
55 = Firebird.quick(:sample, :fibonacci, [10])
"""
@spec quick(String.t() | :sample, atom() | String.t(), list()) :: term()
def quick(:sample, function, args) do
run_one!(sample_wasm_path(), function, args)
end
def quick(source, function, args) when is_binary(source) do
# Check if it looks like WAT (starts with parenthesis)
trimmed = String.trim(source)
if String.starts_with?(trimmed, "(") do
{:ok, instance} = from_wat(source)
try do
call_one!(instance, function, args)
after
stop(instance)
end
else
run_one!(source, function, args)
end
end
# ---------------------------------------------------------------------------
# Introspection helpers
# ---------------------------------------------------------------------------
@doc """
List the function signatures of all exports in a WASM file.
Loads the file, inspects it, and stops the instance. Useful for
exploring unfamiliar WASM modules.
## Examples
Firebird.list_functions("math.wasm")
# => [
# %{name: :add, params: [:i32, :i32], results: [:i32], arity: 2},
# %{name: :multiply, params: [:i32, :i32], results: [:i32], arity: 2},
# %{name: :fibonacci, params: [:i32], results: [:i32], arity: 1}
# ]
"""
@spec list_functions(String.t(), keyword()) :: [map()] | {:error, term()}
def list_functions(path, opts \\ []) do
case load(path, opts) do
{:ok, instance} ->
fns = functions(instance)
stop(instance)
fns
{:error, _} = error ->
error
end
end
@doc """
Print a status overview of Firebird.
Shows version, runtime status, and available WASM files.
Useful for debugging and verifying your setup.
Firebird.status()
# 🔥 Firebird 1.0.0
# ✅ Runtime: operational
# ...
"""
@spec status() :: :ok
def status do
IO.puts("🔥 Firebird #{version()}")
IO.puts("")
# Check runtime
case check() do
:ok -> IO.puts(" ✅ Runtime: working")
{:error, reason} -> IO.puts(" ❌ Runtime: #{reason}")
end
# Check tools
tools = Firebird.Builder.check_tools()
IO.puts(" 🔧 Tools:")
for {name, version} <- tools do
status = if version, do: "✅ #{version}", else: "❌ not found"
IO.puts(" #{name}: #{status}")
end
# Check WASM directories
IO.puts(" 📁 WASM files:")
for dir <- ["priv/wasm", "wasm", "fixtures"] do
if File.dir?(dir) do
wasm_files = Path.wildcard(Path.join(dir, "*.wasm"))
if length(wasm_files) > 0 do
IO.puts(" #{dir}/: #{length(wasm_files)} file(s)")
end
end
end
# Sample
IO.puts("")
IO.puts(" Try: Firebird.quick(:sample, :add, [5, 3])")
:ok
end
# ---------------------------------------------------------------------------
# Shared error raising for bang functions
# ---------------------------------------------------------------------------
# Raises the appropriate WasmError for a given error tuple.
# Centralizes error dispatch so all bang functions (run!, run_one!, etc.)
# produce the same helpful, structured error messages as call!/3.
@doc false
defp raise_wasm_error!(function, args, error) do
case error do
{:error, {:file_error, :enoent, path}} ->
raise Firebird.WasmError.file_not_found(path)
{:error, {:file_error, reason, path}} ->
raise Firebird.WasmError.load_failed(reason, path)
{:error, {:wasm_error, reason}} ->
raise Firebird.WasmError.load_failed(reason)
{:error, {:arity_mismatch, func_name, expected: expected, got: got}} ->
raise Firebird.WasmError.arity_mismatch(func_name, expected, got)
{:error, {:type_mismatch, func_name, arg_index, expected: expected_type, got: actual_value}} ->
raise Firebird.WasmError.type_mismatch(func_name, arg_index, expected_type, actual_value)
{:error, {:call_error, func_name, :instance_stopped}} ->
raise Firebird.WasmError.instance_stopped(func_name)
{:error, {:call_error, func_name, reason}} ->
raise Firebird.WasmError.call_failed(func_name, reason, args)
{:error, reason} ->
raise Firebird.WasmError.call_failed(function, reason, args)
end
end
end