Scarlet Industries

Scribbler

Run WebAssembly on the BEAM. Scribbler reads a .wasm module and hands it to Carder, which turns it into a module the BEAM can load.

Where it fits

Rust compiles to WebAssembly, and so do Java and JavaScript with the right tools, so running WebAssembly on the BEAM lets Erlang, Elixir and Gleam call code written in any of them. Scribbler decodes and validates the module and lowers it to Carder's IR, and Carder does the rest.

Carder builds every module with its safe defaults, so code you did not write cannot crash the node or reach anything you did not give it.

Run a module

Scribbler builds with Gleam 1.16 on Erlang/OTP 29.

git clone https://github.com/scarletindustries/scribbler
cd scribbler
gleam run -- run test/scribbler/conformance/corpus/add.wasm add 3 5

That calls the add export of this module with 3 and 5, and prints 8:

test/scribbler/conformance/corpus/add.wat
(module
  (func (export "add") (param i32 i32) (result i32)
    (i32.add (local.get 0) (local.get 1)))
  (func (export "mul") (param i32 i32) (result i32)
    (i32.mul (local.get 0) (local.get 1))))

gleam run -- help lists every command.

Inside your program

A BEAM application that runs WebAssembly it has been given, such as a plugin, depends on both packages. scribbler/embed compiles the bytes and carder/embed runs them:

import carder/embed
import scribbler/embed as wasm

let assert Ok(compiled) = wasm.compile_named(bytes, "plugin")
let assert Ok(instance) = embed.instantiate(compiled, host)
let result = embed.invoke(instance, "add", [3, 5])
// -> Ok([8])
embed.stop(instance)

Give each guest a name of its own, because a node holds one module of each name and a second guest with the same name replaces the first.

Status

Scribbler is experimental. It runs the official WebAssembly spec suite and matches wasmtime bit for bit on 3 Rust crates, but the code it compiles runs slowly for now, so do not put it in production.