The raw ingredients of WebAssembly made beautifully composable with Elixir

For years I’ve been a JavaScript developer. JavaScript runs everywhere: the browser where it was born, on the server, on your phone, on your laptop. But adopting JavaScript means adopting an increasingly complex and bloated suite of tools.

WebAssembly offers a restart, again born in the browser and again running on the server, your phone and laptop. It takes things down to the studs, at a level even lower than JavaScript, which allows even better performance. But take an existing toolset and its WebAssembly output is often hundreds of kilobytes or more, and targets a single platform instead of running everywhere.

Orb takes the lesson of components that React taught: give developers the raw ingredients and make them easily composable. Make UI components. Make state machine components. Make decoder/encoder components for common formats. And then make those run everywhere.

Orb piggybacks on the existing Elixir ecosystem: it has a module system, it has a package manager, it has a test runner, it has a language server, and it has an awesome community.

In fact, Orb lets you run any Elixir code when compiling your WebAssembly. It’s just like how React lets you write any JavaScript when rendering out primitive HTML. Orb is like JSX but for WebAssembly.

Orb is the full power of Elixir at compile time and the portability of WebAssembly at runtime.

Orb is alpha in active development. My aim is to refine the current feature set and complete a .wasm compiler (current it compiles to WebAssembly’s .wat text format) in order to get to beta.

Write functions using an Elixir subset

You can write maths using familiar + - * / operators in Orb.

defmodule TemperatureConverter do
  use Orb

  defw celsius_to_fahrenheit(celsius: F32), F32 do
    celsius * (9.0 / 5.0) + 32.0
  end

  defw fahrenheit_to_celsius(fahrenheit: F32), F32 do
    (fahrenheit - 32.0) * (5.0 / 9.0)
  end
end

wasm_data = Orb.to_wasm(TemperatureConverter)

Try it out:

Compose reusable modules

You can define a module, say for common ASCII operations, and then reuse it by including it into another module.

defmodule ASCIIChecks do
  use Orb

  defw alpha?(char: I32.U8), I32 do
    (char >= ?a and char <= ?z) or (char >= ?A and char <= ?Z)
  end

  defw numeric?(char: I32.U8), I32 do
    char >= ?0 and char <= ?9
  end

  defw alphanumeric?(char: I32.U8), I32 do
    alpha?(char) or numeric?(char)
  end
end

defmodule UsernameValidation do
  use Orb

  import ASCIIChecks
  Orb.include(ASCIIChecks)

  Memory.pages(1)

  defw is_valid_username(char_ptr: I32.U8.UnsafePointer, len: I32), I32 do
    unless len > 0 do
      return(0)
    end

    loop EachChar do
      len = len - 1

      if len === 0 do
        return(alpha?(char_ptr[at!: 0]))
      end

      unless alphanumeric?(char_ptr[at!: len]) do
        return(0)
      end

      EachChar.continue()
    end
    
    1
  end
end

wasm_data = Orb.to_wasm(UsernameValidation)

Dynamic compilation

Orb lets you run any Elixir code at compile-time. You could have dynamically enabled feature flags by reading from the Process dictionary. You could call out to any existing Elixir library from Hex (see example below). You could even make HTTP requests or talk to a database. Orb instructions are just data, so it doesn’t matter what process you use to make or inform that data.

Use existing Elixir libraries at compile-time

Here we use the existing mime Elixir package to lookup a MIME type for a given file extension. These values get compiled as constants in the resulting WebAssembly module automatically.

Mix.install([
  :orb,
  :mime
])

defmodule MimeType do
  use Orb

  defw(txt, Str, do: MIME.type("txt"))
  defw(json, Str, do: MIME.type("json"))
  defw(html, Str, do: MIME.type("html"))
  defw(css, Str, do: MIME.type("css"))
  defw(wasm, Str, do: MIME.type("wasm"))
  defw(epub, Str, do: MIME.type("epub"))
  defw(rss, Str, do: MIME.type("rss"))
  defw(atom, Str, do: MIME.type("atom"))
  defw(csv, Str, do: MIME.type("csv"))
  defw(woff2, Str, do: MIME.type("woff2"))
  defw(pdf, Str, do: MIME.type("pdf"))
end

Write your own DSL

You can write your own DSLs using Elixir functions that spit out Orb instructions. Go another step and write macros that accept blocks and transform each Elixir expression. For example, this is how SilverOrb’s StringBuilder and XMLBuilder work under the hood.

defmodule Assertions do
  defmacro must!(do: conditions) do
    quote do
      Orb.snippet do
        unquote(case conditions do
          {:__block__, _, multiple} -> multiple
          single -> [single]
        end)
        |> Enum.reduce(&I32.band/2)
        |> unless(do: unreachable!())
      end
    end
  end
end

defmodule Example do
  use Orb

  import Assertions

  defw celsius_to_fahrenheit(celsius: F32), F32 do
    must! do
      celsius > -273.15
      celsius < 500.0
    end

    celsius * (9.0 / 5.0) + 32.0
  end
end