View Source Orb (Orb v0.0.2)

Write WebAssembly modules with Elixir.

WebAssembly is a low-level language — the primitives provided are essentially just integers and floats. However, it has a unique benefit: it can run in every major application environment: browsers, servers, the edge, and mobile devices like phones, tablets & laptops.

Orb exposes the semantics of WebAssembly with a friendly Elixir DSL.

Example

Let’s create a module that calculates the average of a set of numbers.

WebAssembly modules can have state. Here will have two pieces of state: a total count and a running tally. These are stored as globals. (If you are familiar with object-oriented programming, you can think of them as instance variables).

Our module will export two functions: insert and calculate_mean. These two functions will work with the count and tally globals.

defmodule CalculateMean do
  use Orb

  I32.global(
    count: 0,
    tally: 0
  )

  wasm do
    func insert(element: I32) do
      @count = @count + 1
      @tally = @tally + element
    end

    func calculate_mean(), I32 do
      @tally / @count
    end
  end
end

One thing you’ll notice is that we must specify the type of function parameters and return values. Our insert function accepts a 32-bit integer, denoted using I32. It returns no value, while calculate_mean is annotated to return a 32-bit integer.

We get to write math with the intuitive + and / operators. Let’s see the same module without the magic: no math operators and without @ conveniences for working with globals:

defmodule CalculateMean do
  use Orb

  I32.global(
    count: 0,
    tally: 0
  )

  wasm do
    func insert(element: I32) do
      I32.add(global_get(:count), 1)
      global_set(:count)
      I32.add(global_get(:tally), element)
      global_set(:tally)
    end

    func calculate_mean(), I32 do
      I32.div_u(global_get(:tally), global_get(:count))
    end
  end
end

This is the exact same logic as before. In fact, this is what the first version expands to. Orb adds “sugar syntax” to make authoring WebAssembly nicer, to make it feel like writing Elixir or Ruby.

Functions

In Elixir you define functions publicly available outside the module with def/1, and functions private to the module with defp/1. Orb follows the same suffix convention with func/2 and funcp/2.

Consumers of your WebAssembly module will only be able to call exported functions defined using func/2. Making a function public in WebAssembly is known as “exporting”.

Stack based

While it looks like Elixir, there are some key differences between it and programs written in Orb. The first is that state is mutable. While immutability is one of the best features of Elixir, in WebAssembly variables are mutable because raw computer memory is mutable.

The second key difference is that WebAssembly is stack based. Every function has an implicit stack of values that you can push and pop from. This paradigm allows WebAssembly runtimes to efficiently optimize for raw CPU registers whilst not being platform specific.

In Elixir when you write:

def example() do
  1
  2
  3
end

The first two lines with 1 and 2 are inert — they have no effect — and the result from the function is the last line 3.

In WebAssembly / Orb when you write the same sort of thing:

wasm do
  func example() do
    1
    2
    3
  end
end

Then what’s happening is that we are pushing 1 onto the stack, then 2, and then 3. Now the stack has three items on it. Which will become our return value: a tuple of 3 integers. (Our function has no return type specified, so this will be an error if you attempted to compile the resulting module).

So the correct return type from this function would be a tuple of three integers:

wasm do
  func example(), {I32, I32, I32} do
    1
    2
    3
  end
end

If you prefer, Orb allows you to be explicit with your stack pushes with Orb.push/1:

wasm do
  func example(), {I32, I32, I32} do
    push(1)
    push(2)
    push(3)
  end
end

You can use the stack to unlock novel patterns, but for the most part Orb avoids the need to interact with it. It’s just something to keep in mind if you are used to lines of code with simple values not having any side effects.

Locals

Locals are variables that live for the lifetime of a function. They must be specified upfront with their type alongside the function’s definition, and are initialized to zero.

Here we have two locals: under? and over?, both 32-bit integers. We can set their value and then read them again at the bottom of the function.

defmodule WithinRange do
  use Orb

  wasm do
    func validate(num: I32), I32, under?: I32, over?: I32 do
      under? = num < 1
      over? = num > 255

      not (under? or over?)
    end
  end
end

Globals

Globals are like locals, but live for the duration of the entire running module’s life. Their initial type and value are specified upfront.

Globals by default are internal: nothing outside the module can see them. They can be exported to expose them to the outside world.

When exporting a global you decide if it is :readonly or :mutable. Internal globals are mutable by default.

I32.global(some_internal_global: 99)

I32.global(:readonly, some_internal_global: 99)

I32.export_global(:readonly, some_public_constant: 1001)

I32.export_global(:mutable, some_public_variable: 42)

# You can define multiple globals at once:
I32.global(magic_number_a: 99, magic_number_b: 12, magic_number_c: -5)

You can read or write to a global using the @ prefix:

defmodule Counter do
  use Orb

  I32.global(counter: 0)

  wasm do
    func increment() do
      @counter = @counter + 1
    end
  end
end

Memory

WebAssembly provides a buffer of memory when you need more than a handful global integers or floats. This is a contiguous array of random-access memory which you can freely read and write to.

Pages

WebAssembly Memory comes in 64 KiB segments called pages. You use some multiple of these 64 KiB (64 * 1024 = 65,536 bytes) pages.

By default your module will have no memory, so you must specify how much memory you want upfront.

Here’s an example with 16 pages (1 MiB) of memory:

defmodule Example do
  use Orb

  Memory.pages(16)
end

Reading & writing memory

To read from memory, you can use the Memory.load/2 function. This loads a value at the given memory address. Addresses are themselves 32-bit integers. This mean you can perform pointer arithmetic to calculate whatever address you need to access.

However, this can prove unsafe as it’s easy to calculate the wrong address and corrupt your memory. For this reason, Orb provides higher level constructs for making working with memory pointers more pleasant, which are detailed later on.

defmodule Example do
  use Orb

  Memory.pages(1)

  wasm do
    func get_int32(), I32 do
      Memory.load!(I32, 0x100)
    end

    func set_int32(value: I32) do
      Memory.store!(I32, 0x100, value)
    end
  end
end

Initializing memory with data

You can populate the initial memory of your module using Orb.Memory.initial_data/1. This accepts an memory offset and the string to write there.

defmodule MimeTypeDataExample do
  use Orb

  Memory.pages(1)

  wasm do
    Memory.initial_data(offset: 0x100, string: "text/html")
    Memory.initial_data(offset: 0x200, string: """
      <!doctype html>
      <meta charset=utf-8>
      <h1>Hello world</h1>
      """)

    func get_mime_type(), I32 do
      0x100
    end

    func get_body(), I32 do
      0x200
    end
  end
end

Having to manually allocate and remember each memory offset is a pain, so Orb provides conveniences which are detailed in the next section.

Strings constants

You can use constant strings with the ~S sigil. These will be extracted as initial data definitions at the start of the WebAssembly module, and their memory offsets substituted in their place.

Each string is packed together for maximum efficiency of memory space. Strings are deduplicated, so you can use the same string constant multiple times and a single allocation will be made.

String constants in Orb are nul-terminated.

defmodule MimeTypeStringExample do
  use Orb

  Memory.pages(1)

  wasm do
    func get_mime_type(), I32 do
      ~S"text/html"
    end

    func get_body(), I32 do
      ~S"""
      <!doctype html>
      <meta charset=utf-8>
      <h1>Hello world</h1>
      """
    end
  end
end

Control flow

Orb supports control flow with if, block, and loop statements.

If statements

If you want to run logic conditionally, use an if statement.

if @party_mode? do
  music_volume = 100
end

You can add an else clause:

if @party_mode? do
  music_volume = 100
else
  music_volume = 30
end

If you want a ternary operator (e.g. to map from one value to another), you can use Orb.I32.when?/2 instead:

music_volume = I32.when? @party_mode? do
  100
else
  30
end

These can be written on single line too:

music_volume = I32.when?(@party_mode?, do: 100, else: 30)

Loops

Loops look like the familiar construct in other languages like JavaScript, with two key differences: each loop has a name, and loops by default stop unless you explicitly tell them to continue.

i = 0
loop CountUp do
  i = i + 1

  CountUp.continue(if: i < 10)
end

Each loop is named, so if you nest them you can specify which particular one to continue.

total_weeks = 10
weekday_count = 7
week = 0
weekday = 0
loop Weeks do
  loop Weekdays do
    # Do something here with week and weekday

    weekday = weekday + 1
    Weekdays.continue(if: weekday < weekday_count)
  end

  week = week + 1
  Weeks.continue(if: week < total_weeks)
end

Iterators

Iterators are an upcoming feature, currently part of SilverOrb that will hopefully become part of Orb itself.

Blocks

Blocks provide a structured way to skip code.

defblock Validate do
  break(Validate, if: i < 0)

  # Do something with i
end

Blocks can have a type.

defblock Double, I32 do
  if i < 0 do
    push(0)
    break(Double)
  end

  push(i * 2)
end

Calling other functions

You can Orb.call/1 other functions defined within your module. Currently, the parameters and return type are not checked, so you must ensure you are calling with the correct arity and types.

char = call(:encode_html_char, char)

Composing modules together

Any functions from one module can be included into another to allow code reuse.

When you use Orb, funcp and func functions are defined on your Elixir module for you. Calling these from another module will copy any functions across.

defmodule A do
  use Orb

  wasm do
    func square(n: I32), I32 do
      n * n
    end
  end
end
defmodule B do
  use Orb

  # Copies all functions defined in A as private functions into this module.
  A.funcp()

  wasm do
    func example(n: I32), I32 do
      call(:square, 42)
    end
  end
end

You can pass a name to YourSourceModule.funcp(name) to only copy that particular function across.

defmodule A do
  use Orb

  wasm do
    func square(n: I32), I32 do
      n * n
    end

    func double(n: I32), I32 do
      2 * n
    end
  end
end
defmodule B do
  use Orb

  A.funcp(:square)

  wasm do
    func example(n: I32), I32 do
      call(:square, 42)
    end
  end
end

Importing

Your running WebAssembly module can interact with the outside world by importing globals and functions.

Use Elixir features

  • Piping
  • Module attributes
  • Inline for

Custom types with Access

TODO: extract this into its own section.

Define your own functions and macros

Hex packages

  • GoldenOrb
    • String builder
  • SilverOrb

Running your module

Summary

Functions

Declare a snippet of Orb AST for reuse. Enables DSL, with additions from mode.

Convert Orb AST into WebAssembly text format.

Enter WebAssembly.

Declare a WebAssembly import for a function or global.

Functions

Link to this macro

snippet(mode \\ Orb.S32, locals \\ [], list)

View Source (macro)

Declare a snippet of Orb AST for reuse. Enables DSL, with additions from mode.

Convert Orb AST into WebAssembly text format.

Link to this macro

wasm(mode \\ Orb.S32, list)

View Source (macro)

Enter WebAssembly.

Link to this macro

wasm_import(mod, entries)

View Source (macro)

Declare a WebAssembly import for a function or global.