The letter A styled as Alchemists logo. lchemists
Published January 15, 2023 Updated May 22, 2023
Cover
Ruby WebAssembly

WebAssembly (WASM) allows you to build and run Ruby programs with a strong focus on speed and security using a CRuby binary that can run within a web browser, serverless environment, or any kind of WASM/WebAssembly System Interface (WASI) embedded environment.

The goal of this article is to get you up and running with WASM, quickly, so you can experiment with building your own Ruby WASM applications.

History

For context, WebAssembly — and the corresponding WebAssembly System Interface — is new to the scene and first became supported in the release of Ruby 3.2.0.

Browser

The easiest way to experiment with WASM is within your browser so you can interact with the following UI:

Cover

Start by copying the following HTML code snippet, saving as index.html, and opening index.html in your default browser. This will allow you to dynamically generate random numbers for which you can experiment further. Here’s the code:

<!DOCTYPE html>

<html>
  <head>
    <meta charset="utf-8">
    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">

    <title>Ruby WebAssembly Demo</title>
    <meta name="description" content="An example of a Ruby WebAssembly application.">
    <meta name="author" content="Alchemists">

    <style type="text/css">
      html {
        font-family: Verdana;
      }

      body {
        margin: 0;
        padding: 0;
      }

      .portal {
        background-color: #1f2329;
      }

      .body {
        display: flex;
        height: 100vh;
        justify-content: center;
      }

      .interface {
        align-items: center;
        color: white;
        display: flex;
        flex-direction: column;
        justify-content: center;
      }

      .label {
        margin-bottom: 2rem;
      }

      .button {
        background-color: white;
        cursor: pointer;
        padding: 0.5rem;
        border-radius: 0.5rem;
        font-size: 1rem;
      }

      #output {
        font-size: 1.5rem;
      }
    </style>

    <script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/browser.umd.js">
    </script>

    <script>
      const { DefaultRubyVM } = window["ruby-wasm-wasi"];

      const main = async () => {
        const response = await fetch(
          "https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@latest/dist/ruby.wasm"
        );

        const buffer = await response.arrayBuffer();
        const module = await WebAssembly.compile(buffer);
        const { vm } = await DefaultRubyVM(module);

        const result = vm.eval(`
          generator = -> numbers = (1..100).to_a { numbers.sample }
          generator.call
        `);

        const output = document.getElementById("output");
        output.innerText = result.toString();
      };
    </script>
  </head>

  <body class="portal">
    <main class="body">
      <section class="interface">
        <h1 class="label">Ruby WebAssembly Demo</h1>
        <button type="button" onclick="main()" class="button">Generate Number</button>
        <p id="output">?</p>
      </section>
    </main>
  </body>
</html>

The above is a slightly modified version from the Ruby WebAssembly project. The project is a bit messy to navigate but you can find the original demo in the NPM Packages folder for which you can find additional instructions. You’ll notice that my demonstration uses the following modifications:

// Evaluate the random number generator (lambda) and store the result.
const result = vm.eval(`
  generator = -> numbers = (1..100).to_a { numbers.sample }
  generator.call
`);

// Find the output element by ID.
const output = document.getElementById("output");

// Render the result at text.
output.innerText = result.toString();
<!-- Upon each click of the button, we call the main function documentated above. -->
<button type="button" onclick="main()" class="button">Generate Number</button>

Feel free to experiment further, though.

System Interface

Should you want to use Ruby via the WebAssembly System Interface then the following will guide you through the process.

Quick Start

In case you want to quickly be up and running with WASM/WASI, use the bash script below. This script will build a demo project for you with a working Ruby WASM application.

For further details, read on to learn more about how this script works.

#! /usr/bin/env bash

set -o nounset
set -o errexit
set -o pipefail
IFS=$'\n\t'

# Save as `build`, then `chmod 755 build`, and run as `./build`.

WASI_RUBY_URL="https://github.com/ruby/ruby.wasm/releases/download/2.0.0/ruby-head-wasm32-unknown-wasi-full.tar.gz"
WASI_PRESET_URL="https://github.com/kateinoigakukun/wasi-preset-args/releases/download/v0.1.1/wasi-preset-args-x86_64-apple-darwin.zip"
WASI_VFS_URL="https://github.com/kateinoigakukun/wasi-vfs/releases/download/v0.2.0/wasi-vfs-cli-aarch64-apple-darwin.zip"

if [[ -d ./demo ]]; then
  printf "%s\n" "A 'demo' directory exists. Please move or delete directory first."
  exit 1
fi

if ! command -v wasmtime > /dev/null; then
  brew install wasmtime
fi

printf "%s\n" "Building demo application structure..."
mkdir -p demo/bin demo/lib demo/build
cd demo

printf "%s\n" "Installing WASI Ruby..."
curl --location \
     --fail \
     --silent \
     --show-error \
     "$WASI_RUBY_URL" > wasi.tar.gz

mkdir wasi
tar --extract --gzip --directory wasi --strip-components 1 --file wasi.tar.gz
rm -f wasi.tar.gz
mv wasi/usr/local/bin/ruby bin/ruby.wasm

printf "%s\n" "Installing WASI Preset..."
curl --location \
     --fail \
     --silent \
     --show-error \
     "$WASI_PRESET_URL" > wasi_preset.zip

unzip -q -d bin wasi_preset.zip
rm -f wasi_preset.zip
mv bin/wasi-preset-args bin/preset

printf "%s\n" "Installing WASI VFS..."
curl --location \
     --fail \
     --silent \
     --show-error \
     "$WASI_VFS_URL" > vfs.zip

unzip -q -d bin vfs.zip
rm -f vfs.zip
mv bin/wasi-vfs bin/vfs

printf "%s\n" "Creating application..."
cat << BODY > lib/app.rb
# frozen_string_literal: true

puts "Hello, World!"
BODY

printf "%s\n" "Adding application presets..."
bin/preset bin/ruby.wasm -o bin/ruby.wasm -- /src/app.rb

printf "%s\n" "Building application..."
bin/vfs pack bin/ruby.wasm \
                  --mapdir /usr::wasi/usr/ \
                  --mapdir /src::lib/ \
                  --output build/application.wasm

printf "%s\n" "Cleaning artifacts..."
rm -rf wasi

printf "%s\n" "Running application..."
wasmtime run build/application.wasm

Script

To learn more about how the above script works, let’s break down each section into blocks for explanation starting with the top of the script where we set safe Bash defaults as provided by the Bashsmith project:

#! /usr/bin/env bash

set -o nounset
set -o errexit
set -o pipefail
IFS=$'\n\t'

Next, you’ll want to use constants to define the URLs to the WASM/WASI files we need to build our Ruby application. These are provided at the top of the script so you can easily update the URLs accordingly since — after the time of this writing — new versions of WASM/WASI will most likely be released.

WASI_RUBY_URL="https://github.com/ruby/ruby.wasm/releases/download/2.0.0/ruby-head-wasm32-unknown-wasi-full.tar.gz"
WASI_PRESET_URL="https://github.com/kateinoigakukun/wasi-preset-args/releases/download/v0.1.1/wasi-preset-args-x86_64-apple-darwin.zip"
WASI_VFS_URL="https://github.com/kateinoigakukun/wasi-vfs/releases/download/v0.2.0/wasi-vfs-cli-aarch64-apple-darwin.zip"

As a safety precaution the following conditions are used to ensure you don’t already have a demo project in your current directory and that you have wasmtime installed:

if [[ -d ./demo ]]; then
  printf "%s\n" "A 'demo' directory exists. Please move or delete directory first."
  exit 1
fi

if ! command -v wasmtime > /dev/null; then
  brew install wasmtime
fi

Next, the demo project structure is built for you:

printf "%s\n" "Building demo application structure..."
mkdir -p demo/bin demo/lib demo/build
cd demo

Once the demo project structure is created, we start downloading the various components necessary for building a WASM application. The first is obtain WASI support for Ruby which installs ruby.wasm in your bin folder.

printf "%s\n" "Installing WASI Ruby..."
curl --location \
     --fail \
     --silent \
     --show-error \
     "$WASI_RUBY_URL" > wasi.tar.gz

mkdir wasi
tar --extract --gzip --directory wasi --strip-components 1 --file wasi.tar.gz
rm -f wasi.tar.gz
mv wasi/usr/local/bin/ruby bin/ruby.wasm

Next, we repeat the above process but for WASI preset support:

printf "%s\n" "Installing WASI Preset..."
curl --location \
     --fail \
     --silent \
     --show-error \
     "$WASI_PRESET_URL" > wasi_preset.zip

unzip -q -d bin wasi_preset.zip
rm -f wasi_preset.zip
mv bin/wasi-preset-args bin/preset

While preset support isn’t required (is more optional), it does make running the Ruby application easier to do later. Now we can install the WASI Virtual File System necessary for packaging our Ruby application.

printf "%s\n" "Installing WASI VFS..."
curl --location \
     --fail \
     --silent \
     --show-error \
     "$WASI_VFS_URL" > vfs.zip

unzip -q -d bin vfs.zip
rm -f vfs.zip
mv bin/wasi-vfs bin/vfs

With WASI fully installed and configured, we can now build a simple Hello World Ruby application in our lib directory:

printf "%s\n" "Creating application..."
cat << BODY > lib/app.rb
# frozen_string_literal: true

puts "Hello, World!"
BODY

With our Ruby application in hand, we are now able to configure our WASM image with source file presets. Applying presets is totally optional but doing so allows us to not have to keep passing the path to our source file when running our Ruby application (which we’ll do in a moment):

printf "%s\n" "Adding application presets..."
bin/preset bin/ruby.wasm -o bin/ruby.wasm -- /src/app.rb

After we have configured our presets, we can then build our application as application.wasm in our build folder as a virtual file system that maps our user and source directories:

printf "%s\n" "Building application..."
bin/vfs pack bin/ruby.wasm \
                  --mapdir /usr::wasi/usr/ \
                  --mapdir /src::lib/ \
                  --output build/application.wasm

As one last step before running our application, we’ll clean up any artifacts in our project:

printf "%s\n" "Cleaning artifacts..."
rm -rf wasi

Finally, after all of this setup, we can run our application:

printf "%s\n" "Running application..."
wasmtime run build/application.wasm

Now that you understand how the script works, when you run it, you’ll see the following output:

Building demo application structure...
Installing WASI Ruby...
Installing WASI Preset...
Installing WASI VFS...
Creating application...
Adding application presets...
Building application...
Cleaning artifacts...
Running application...
Hello, World!

Only the last line shows your Ruby application being run. Everything else is informational setup. Congratulations, you’ve build your first WASM Ruby application. 🎉

Resources

These are additional resources which may be of interest:

Conclusion

As you can see, it is early days for WebAssembly and WebAssembly System Interface support in Ruby but hopefully this tutorial gets you setup so you can experiment and do more interesting things. Enjoy!