
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:

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:
-
MDN WebAssembly: Provides an overview complete with tutorials and JavaScript APIs.
-
An Update on WebAssembly/WASI Support in Ruby: Provides the final report on adding WASM to Ruby as work done for the 2021 Ruby Association Grant.
-
Ruby on WebAssembly: This talks about the
wasm
gem but is four years old and hasn’t been maintained. -
WebAssembly / WASI port of Ruby: Provides a bit more information on cross-building.
-
WASI VFS: Provides source code to the Ruby WASI VFS.
-
How to use Bundler and RubyGems on WebAssembly: Provides a gist for showing how to bundle gem dependencies for use with WASM.
-
WebAssembly JavaScript Promise Integration API (JSPI): Discusses the introduction of this new V8 API for asynchronous communication.
-
Component Model Explainer: A lower-level dive into how components work and are organized within WASM.
-
A Closer Look: Not a deep dive, as the title might suggest, but a look at a few more capabilities.
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!