View Source Orb (Orb v0.1.1)
Write WebAssembly modules with Elixir.
WebAssembly is a low-level language. You work with integers and floats, can perform operations on them like adding or multiplication, and then read and write those values to a block of memory. There’s no concept of a “string” or an “array”, let alone a “hash map” or “HTTP request”.
That’s where a library like Orb can help out. It takes full advantage of Elixir’s language features by becoming a compiler for WebAssembly. You can define WebAssembly modules in Elixir for “string” or “hash map”, and compose them together into a final module.
That WebAssembly module can then run in every major application environment: browsers, servers, the edge, and mobile devices like phones, tablets & laptops. This story is still being developed, but I believe like other web standards like JSON, HTML, and HTTP, that WebAssembly will become a first-class citizen on any platform. It’s Turing-complete, designed to be backwards compatible, fast, and works almost everywhere.
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
global do
@count 0
@tally 0
end
defw insert(element: I32) do
@count = @count + 1
@tally = @tally + element
end
defw calculate_mean(), I32 do
@tally / @count
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)
defw insert(element: I32) do
Orb.Instruction.Global.Set.new(Orb.I32, :count, I32.add(Orb.Instruction.Global.Get.new(Orb.I32, :count), 1))
Orb.Instruction.Global.Set.new(Orb.I32, :tally, I32.add(Orb.Instruction.Global.Get.new(Orb.I32, :tally), element))
end
defw calculate_mean(), I32 do
I32.div_s(Orb.Instruction.Global.Get.new(Orb.I32, :tally), Orb.Instruction.Global.Get.new(Orb.I32, :count))
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 defw/2
and defwp/2
.
Consumers of your WebAssembly module will only be able to call exported functions defined using defw/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:
defw example() do
1
2
3
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:
defw example(), {I32, I32, I32} do
1
2
3
end
If you prefer, Orb allows you to be explicit with your stack pushes with Orb.Stack.push/1
:
defw example(), {I32, I32, I32} do
Orb.Stack.push(1)
Orb.Stack.push(2)
Orb.Stack.push(3)
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
defw validate(num: I32), I32, under?: I32, over?: I32 do
under? = num < 1
over? = num > 255
not (under? or over?)
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.
defmodule GlobalExample do
use Orb
global do # :mutable by default
@some_internal_global 99
end
global :readonly do
@some_internal_constant 99
end
global :export_readonly do
@some_public_constant 1001
end
global :export_mutable do
@some_public_variable 42
end
# You can define multiple globals at once:
global do
@magic_number_a 99
@magic_number_b 12
@magic_number_c -5
end
end
You can read or write to a global within defw
using the @
prefix:
defmodule Counter do
use Orb
global do
@counter 0
end
defw increment() do
@counter = @counter + 1
end
end
When you use Orb.global/1
an Elixir module attribute with the same name and initial value is also defined for you:application
defmodule DeepThought do
use Orb
global do
@meaning_of_life 42
end
def get_meaning_of_life_elixir() do
@meaning_of_life
end
defw get_meaning_of_life_wasm(), I32 do
@meaning_of_life
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)
defw get_int32(), I32 do
Memory.load!(I32, 0x100)
end
defw set_int32(value: I32) do
Memory.store!(I32, 0x100, value)
end
end
Initializing memory with data
You can populate the initial memory of your module using Orb.Memory.initial_data!/2
. This accepts an memory offset and the string to write there.
defmodule MimeTypeDataExample do
use Orb
Memory.pages(1)
Memory.initial_data!(0x100, "text/html")
Memory.initial_data!(0x200, """
<!doctype html>
<meta charset=utf-8>
<h1>Hello world</h1>
""")
defw get_mime_type(), I32 do
0x100
end
defw get_body(), I32 do
0x200
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 directly. 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)
defw get_mime_type(), Str do
"text/html"
end
defw get_body(), Str do
"""
<!doctype html>
<meta charset=utf-8>
<h1>Hello world</h1>
"""
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 if
when both clauses push the same type:
music_volume = if @party_mode? do
i32(100)
else
i32(30)
end
These can be written on single line too:
music_volume = I32.if(@party_mode?, do: i32(100), else: i32(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.
Control.block Validate do
Validate.break(if: i < 0)
# Do something with i
end
Blocks can have a type.
Control.block Double, I32 do
if i < 0 do
push(0)
Double.break()
end
push(i * 2)
end
Calling other functions
When you use defw
, a corresponding Elixir function is defined for you using def
.
defw magic_number(), I32 do
42
end
defw some_example(), n: I32 do
n = magic_number()
end
Composing modules with Orb.include/1
The WebAssembly functions from one module reused in another using Orb.include/1
.
Here’s an example of module A’s square function being included by module B:
defmodule A do
use Orb
defw square(n: I32), I32 do
n * n
end
end
defmodule B do
use Orb
# Copies all WebAssembly functions defined in A into this module.
Orb.include(A)
defw example(n: I32), I32 do
# Now we can call functions on A.
A.square(42)
end
end
Summary
Functions
Makes the globals inside exported.
Declare WebAssembly globals.
Declare WebAssembly globals.
Replaced by Orb.Import.register/1
.
Copy WebAssembly functions from one module into the current module.
Adds a prefix to all functions within this module. This namespacing helps avoid names from clashing.
Declare a snippet of Orb AST for reuse. Enables DSL, with additions from mode
.
Convert Orb AST into WebAssembly binary format.
Convert Orb AST into WebAssembly text format.
Functions
Makes the globals inside exported.
Declare WebAssembly globals.
mode
can be :readonly, :mutable, :export_readonly, or :export_mutable. The default is :mutable.
Examples
defmodule GlobalExample do
use Orb
global do # :mutable by default
@some_internal_global 99
end
global :readonly do
@some_internal_constant 99
end
global :export_readonly do
@some_public_constant 1001
end
global :export_mutable do
@some_public_variable 42
end
# You can define multiple globals at once:
global do
@magic_number_a 99
@magic_number_b 12
@magic_number_c -5
end
end
Declare WebAssembly globals.
mode
can be :readonly, :mutable, :export_readonly, or :export_mutable. The default is :mutable.
Examples
defmodule GlobalExample do
use Orb
global do # :mutable by default
@some_internal_global 99
end
global :readonly do
@some_internal_constant 99
end
global :export_readonly do
@some_public_constant 1001
end
global :export_mutable do
@some_public_variable 42
end
# You can define multiple globals at once:
global do
@magic_number_a 99
@magic_number_b 12
@magic_number_c -5
end
end
Replaced by Orb.Import.register/1
.
Copy WebAssembly functions from one module into the current module.
Examples
defmodule Math do
use Orb
defw square(n: I32), I32 do
n * n
end
end
defmodule SomeOtherModule do
use Orb
Orb.include(Math)
defw magic(), I32 do
Math.square(3)
end
end
Adds a prefix to all functions within this module. This namespacing helps avoid names from clashing.
Declare a snippet of Orb AST for reuse. Enables DSL, with additions from mode
.
Convert Orb AST into WebAssembly binary format.
Convert Orb AST into WebAssembly text format.