Skip to content

hdresearch/firebird

Folders and files

NameName
Last commit message
Last commit date

Latest commit

Β 

History

500 Commits
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 
Β 

Repository files navigation

Firebird Library Mascot

Firebird πŸ”₯

CI Hex.pm

Speed up your Elixir projects by up to 42x and Phoenix projects by upward of 12x, thanks to WebAssembly.

For how more than a billion tokens with coding agents were used to create this, check out this blog post. For what these technologies are or why bring the two together, check out this blog post.

Quick Start

1. Add dependency

# mix.exs
def deps do
  [{:firebird, "~> 1.0"}]
end

2. 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 action

4. 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/, and fixtures/ for .wasm files. 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
end

Add 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 functions

Inline 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.Math

Generates 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-shot

Scaffold 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 scaffold

Core 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 iex

Creating WASM Modules

Rust

#[no_mangle]
pub extern "C" fn add(a: i32, b: i32) -> i32 { a + b }
cargo build --target wasm32-unknown-unknown --release

Go

//export add
func add(a, b int32) int32 { return a + b }
GOOS=wasip1 GOARCH=wasm go build -o module.wasm

Compile 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)
end
mix 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 BEAM

Supports: 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 project

See 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
end

Firebird.TestCase handles ExUnit setup, WASM loading, and cleanup automatically.

Options:

  • No args β€” loads bundled sample_math.wasm
  • wasm: "path.wasm" β€” loads your module
  • wasm: false β€” just imports helpers, no auto-loading
  • wasi: 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
end
Manual setup (without TestCase)
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:

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

Contributing

See CONTRIBUTING.md for project setup, architecture overview, and how to submit changes.

License

MIT

About

No description, website, or topics provided.

Resources

License

Contributing

Stars

Watchers

Forks

Releases

No releases published

Packages

 
 
 

Contributors

Languages