Firebird 🔥
Speed up your Elixir projects by up to 42x and Phoenix projects by upward of 12x, thanks to WebAssembly.
Quick Start
1. Add dependency
# mix.exs
def deps do
[{:firebird, "~> 1.0"}]
end2. Configure formatter (optional)
Add :firebird to import_deps so mix format respects the DSL macros:
# .formatter.exs
[
inputs: ["{mix,.formatter}.exs", "{config,lib,test}/**/*.{ex,exs}"],
import_deps: [:firebird]
]3. Verify it works
iex> Firebird.check!()
:ok
iex> Firebird.demo() # See add, multiply, fibonacci in action4. Call WASM functions
# Absolute quickest way — one expression:
8 = Firebird.quick(:sample, :add, [5, 3])
# One-liner: load, call, and stop in one step
{:ok, 8} = Firebird.run_one("math.wasm", :add, [5, 3])
8 = Firebird.run_one!("math.wasm", :add, [5, 3])
# Or load once, call many times
{:ok, wasm} = Firebird.load("math.wasm")
{:ok, 8} = Firebird.call_one(wasm, :add, [5, 3])
55 = Firebird.call_one!(wasm, :fibonacci, [10])
Firebird.stop(wasm)That's it. No config files, no boilerplate, no setup ceremony.
Smart Path Resolution: Firebird auto-searches
priv/wasm/,wasm/, andfixtures/for.wasmfiles. Just use the filename — no full paths needed.
📖 Learn more:Why WASM? | Performance Deep Dive
The Declarative Way (Recommended)
Wrap a WASM module in a clean Elixir module with use Firebird:
defmodule MyApp.Math do
use Firebird, wasm: "priv/wasm/math.wasm"
wasm_fn :add, args: 2
wasm_fn :multiply, args: 2
wasm_fn :fibonacci, args: 1
endAdd to your supervision tree:
# application.ex
children = [MyApp.Math]Call functions naturally:
{:ok, [8]} = MyApp.Math.add(5, 3)
[55] = MyApp.Math.fibonacci!(10)Auto-Discovery
Don't want to declare each function? Use auto: true:
defmodule MyApp.Math do
use Firebird, wasm: "priv/wasm/math.wasm", auto: true
end
# All WASM exports are auto-wrapped as Elixir functionsInline WAT (Prototyping)
Write WebAssembly directly in Elixir — compiled at compile time:
import Firebird.Sigils
bytes = wat!("""
(module
(func (export "add") (param i32 i32) (result i32)
local.get 0 local.get 1 i32.add))
""")
{:ok, wasm} = Firebird.load(bytes)
{:ok, [8]} = Firebird.call(wasm, :add, [5, 3])
Or use Firebird.Quick for one-off evaluations:
{:ok, [42]} = Firebird.Quick.eval_wat("""
(module
(func (export "answer") (result i32) i32.const 42))
""", "answer")Connection Pool
For concurrent workloads, use Firebird.Pool:
# In your supervision tree
children = [
{Firebird.Pool, wasm: "priv/wasm/math.wasm", size: 4, name: :math_pool}
]
# Calls distributed across instances automatically
{:ok, [8]} = Firebird.Pool.call(:math_pool, :add, [5, 3])
[8] = Firebird.Pool.call!(:math_pool, :add, [5, 3])Block API (Auto-Cleanup)
{:ok, result} = Firebird.with_instance("math.wasm", fn wasm ->
{:ok, [a]} = Firebird.call(wasm, :add, [5, 3])
{:ok, [b]} = Firebird.call(wasm, :multiply, [a, 2])
b
end)
# => {:ok, 16}Pipe-Friendly API
Chain operations without losing the instance reference:
wasm = Firebird.load!("math.wasm")
{[sum], wasm} = Firebird.pipe!(wasm, :add, [5, 3])
{[product], wasm} = Firebird.pipe!(wasm, :multiply, [sum, 2])
Firebird.stop(wasm)Auto-Generate a Wrapper
Inspect any WASM file and generate a complete Elixir module:
mix firebird.gen priv/wasm/math.wasm --module MyApp.MathGenerates a ready-to-use module with all exported functions wrapped. Styles:
mix firebird.gen module.wasm --style module # GenServer (default)
mix firebird.gen module.wasm --style pool # Pool-backed
mix firebird.gen module.wasm --style basic # Stateless one-shotScaffold a New Project
mix firebird.init # Basic setup in existing project
mix firebird.init --rust # + Rust WASM scaffold
mix firebird.init --phoenix # + Phoenix helpers
mix firebird.new my_app # New project from scratch
mix firebird.new my_app --rust # With Rust scaffoldCore API
# One-shot (load → call → stop) — single value
{:ok, result} = Firebird.run_one("module.wasm", :function, [args])
result = Firebird.run_one!("module.wasm", :function, [args])
# One-shot — list return
{:ok, [result]} = Firebird.run("module.wasm", :function, [args])
[result] = Firebird.run!("module.wasm", :function, [args])
# Load
{:ok, instance} = Firebird.load("module.wasm")
instance = Firebird.load!("module.wasm")
{:ok, instance} = Firebird.load(wasm_bytes)
{:ok, instance} = Firebird.load("app.wasm", wasi: true)
# Call — single value (recommended for most functions)
{:ok, result} = Firebird.call_one(instance, :function_name, [arg1, arg2])
result = Firebird.call_one!(instance, :function_name, [arg1, arg2])
# Call — list return (for multi-value or explicit matching)
{:ok, [result]} = Firebird.call(instance, :function_name, [arg1, arg2])
[result] = Firebird.call!(instance, :function_name, [arg1, arg2])
# Batch calls
{:ok, results} = Firebird.call_many(instance, [
{:add, [1, 2]},
{:multiply, [3, 4]}
])
# Block API (auto-cleanup)
{:ok, result} = Firebird.with_instance("module.wasm", fn wasm ->
# use wasm here, auto-stopped after block
end)
# Pipe-friendly (threads instance through)
{[sum], wasm} = Firebird.pipe!(wasm, :add, [5, 3])
{[product], wasm} = Firebird.pipe!(wasm, :multiply, [sum, 2])
# Inspect
exports = Firebird.exports(instance)
exists? = Firebird.function_exists?(instance, :add)
{:ok, {[:i32, :i32], [:i32]}} = Firebird.function_type(instance, :add)
info = Firebird.info(instance)
# Memory
{:ok, size} = Firebird.memory_size(instance)
:ok = Firebird.write_memory(instance, 0, <<1, 2, 3>>)
{:ok, bytes} = Firebird.read_memory(instance, 0, 3)
# Lifecycle
true = Firebird.alive?(instance)
:ok = Firebird.stop(instance)
# WAT (inline WASM for prototyping)
{:ok, wasm} = Firebird.from_wat("(module (func (export \"f\") (result i32) i32.const 42))")
{:ok, bytes} = Firebird.compile_wat(wat_source) # WAT → bytes
# Diagnostics
Firebird.check!() # Verify setup works
Firebird.demo() # Interactive demo
Firebird.describe(instance) # Print instance info
Firebird.describe("file.wasm") # Print file info
wasm = Firebird.playground() # Pre-loaded instance for iexCreating WASM Modules
Rust
#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }cargo build --target wasm32-unknown-unknown --releaseGo
//export add
func add(a, b int32) int32 { return a + b }GOOS=wasip1 GOARCH=wasm go build -o module.wasmCompile Elixir to WASM
Write Elixir, compile to WebAssembly:
# lib/wasm_modules/math.ex
defmodule MyMath do
@wasm true
def add(a, b), do: a + b
@wasm true
def fibonacci(0), do: 0
def fibonacci(1), do: 1
def fibonacci(n), do: fibonacci(n - 1) + fibonacci(n - 2)
endmix firebird.target # Compile to WASM
mix firebird.target --optimize --tco # With optimizations
mix firebird.target --watch # Watch for changes
mix firebird.bench --format markdown # Benchmark WASM vs BEAMSupports: arithmetic, comparisons, if/else/unless/cond/case, pattern matching, recursion, guards, pipe operator, variable bindings.
See WASM Target Guide | API Reference | Elixir-to-WASM Research for full details.
Phoenix to WASM 🔥
Firebird includes WASM-accelerated Phoenix components:
| Component | Description |
|---|---|
| Router | HTTP route matching with path params and wildcards |
| Template | HTML template rendering with auto-escaping |
| Plug | Request parsing, response building, CSRF validation |
| Endpoint | Full request lifecycle with plug pipelines |
| Channel | Topic matching, message serialization, presence |
| JSON API | JSON encoding, API responses, pagination, errors |
| Session | Cookie parsing, signing, session management |
| Live | HTML diffing, patch generation, component rendering |
| Validator | Form validation: required, type, length, format, inclusion |
| Form | HTML form helpers with CSRF, method spoofing, escaping |
| WebSocket | Frame encode/decode, upgrade handling, Channel protocol |
| CSRF | Token generation, signing, validation, protection middleware |
| RateLimiter | Token bucket rate limiting with per-client tracking |
Plus Elixir-level framework modules: Conn, Pipeline, Middleware, RouterDSL, RequestHandler, LiveComponent, ErrorHandler, Application, Scaffold, Testing, Form, WebSocket, CSRF, RateLimiter.
mix firebird.phoenix.gen my_app # Generate Phoenix WASM projectSee Phoenix to WASM Guide for full documentation.
Testing WASM Modules
Use Firebird.TestCase for zero-boilerplate WASM testing (like Phoenix's ConnCase):
defmodule MyApp.MathTest do
use Firebird.TestCase, wasm: "priv/wasm/math.wasm"
test "add works", %{wasm: wasm} do
assert_wasm_call wasm, :add, [5, 3], [8]
end
test "single value", %{wasm: wasm} do
assert_wasm_result wasm, :fibonacci, [10], 55
end
test "exports", %{wasm: wasm} do
assert_wasm_exports wasm, [:add, :multiply, :fibonacci]
end
test "type signature", %{wasm: wasm} do
assert_wasm_type wasm, :add, {[:i32, :i32], [:i32]}
end
test "function with arity", %{wasm: wasm} do
assert_wasm_function wasm, :add, 2
end
test "error on missing function", %{wasm: wasm} do
assert_wasm_error wasm, :nonexistent, [1]
end
test "addition truth table", %{wasm: wasm} do
assert_wasm_table wasm, :add, [
{[0, 0], 0},
{[1, 2], 3},
{[5, 3], 8},
{[-1, 1], 0},
{[100, 200], 300}
]
end
endFirebird.TestCase handles ExUnit setup, WASM loading, and cleanup automatically.
Options:
-
No args — loads bundled
sample_math.wasm wasm: "path.wasm"— loads your modulewasm: false— just imports helpers, no auto-loadingwasi: true— enable WASI support
# Quick test with bundled sample (add/2, multiply/2, fibonacci/1):
defmodule QuickTest do
use Firebird.TestCase # auto-loads sample_math.wasm
test "it works", %{wasm: wasm} do
assert_wasm_call wasm, :add, [1, 2], [3]
end
endManual setup (without TestCase)
```elixir defmodule MyApp.MathTest do use ExUnit.Case import Firebird.TestHelpers setup_wasm "priv/wasm/math.wasm" test "add works", %{wasm: wasm} do assert_wasm_call wasm, :add, [5, 3], [8] end end ``` For quick testing with the bundled sample: ```elixir setup_sample_wasm() # Uses Firebird's included sample_math.wasm ```Developer Tools
mix firebird.inspect fixtures/math.wasm # View exports and types
mix firebird.gen module.wasm --module MyApp.M # Generate wrapper module
mix firebird.init # Setup in existing project
mix firebird.new my_app # New project from scratch
mix firebird.analyze lib/wasm_modules/ # Analyze before compiling# Type-safe calls with clear error messages
{:ok, [8]} = Firebird.TypedCall.call(instance, :add, [5, 3])
# Metrics collection
Firebird.Metrics.start_link()
Firebird.Metrics.timed_call(instance, :fibonacci, [30])
# Batch execution
results = Firebird.Batch.map(pool, :fibonacci, Enum.map(1..100, &[&1]))
# Hot reload (development)
{:ok, w} = Firebird.HotReload.start_link(path: "math.wasm")Documentation
- Cheatsheet — Single-page quick reference
- Why WASM? — When to use WASM vs BEAM
- Performance Deep Dive — 75+ benchmarks with methodology
- Decision Guide — Which API to use? WASM vs BEAM? Pool vs Module? Start here
- Getting Started Guide — Full walkthrough
- Performance Guide — Profiling, benchmarking, deciding what to WASM-ify
- Testing Guide — Testing WASM modules with
Firebird.TestCase, assertions, patterns - Cookbook — Real-world recipes: plugins, ETL, sandboxing, batch jobs
- API Reference — Complete API documentation
- Architecture Guide — Module layers, data flow, compiler pipeline, design decisions
- Troubleshooting — Common issues and solutions
- Phoenix to WASM — Full Phoenix WASM guide
- Examples — Working example scripts
- Elixir-to-WASM Guide — Compile Elixir to WebAssembly
Contributing
See CONTRIBUTING.md for project setup, architecture overview, and how to submit changes.
License
MIT