Ruby Adds Support for WebAssembly: What Does This Mean for Ruby Developers?
Ruby has joined the ranks of languages capable of targeting WebAssembly with its latest 3.2 release. This seemingly minor update might be the biggest thing that has happened to the language since Rails, as it lets Ruby developers go beyond the backend. By porting their code to WebAssembly, they can run it anywhere: on the frontend, on embedded devices, as serverless functions, in place of containers, or on the edge. WebAssembly has the potential to make Ruby a universal language.
What is WebAssembly?
WebAssembly (commonly shortened as Wasm) is a binary low-level instruction format that runs on a virtual machine. The language was designed as an alternative to JavaScript. Its aim is to run applications on any browser at near-native speeds. Wasm can be targeted from any high-level language like C, Go, Rust, and now also Ruby.
Wasm became a W3C standard in 2019, opening the path to writing high-performing applications for the Web. The standard itself is still evolving, and its ecosystem is growing. Currently, this technology is receiving a lot of focus from the Cloud Native Computing Foundation (CNCF), with several projects under development.
Wasm’s design sits on two pillars: portability and security. The Wasm binary can run on any modern browser, even mobile devices. For security, Wasm programs run in a sandboxed, memory-safe VM. As such, they cannot access any system resources: they can’t change the filesystem or access the network or memory.
bAssembly brings portability to the next level
Let’s say you want to build an application targeting many systems, e.g. Linux, Windows, and macOS. What are your options?
You could use a compiled language like C and build a binary for each target.
Or, if you can rely on having the appropriate runtime installed you could choose an interpreted language like JavaScript or one that compiles to bytecode like Java.
What if you have a container runtime in the client? In that case, you could build a Docker image for each platform type.
For Ruby developers historically, the only option was to distribute the code. That meant that users had to install the Ruby interpreter (or developers had to package the interpreter along with the application) to run the application.
All these mechanisms provide portability, but at a cost: you must build, test, and distribute many images. Sometimes, you must also ship a suitable runtime with the release or tell the user to install it independently.
WebAssembly (shortened as Wasm) takes portability to the next level: it allows you to build ONE binary and run it in any modern browser.
The ability to run code at native speed has allowed developers to build sites like [Figma], and Google Earth or even run Vim in the browser.
Ruby adds support for WebAssembly
The latest Ruby release ships with a Wasm port of the interpreter. Therefore, we can run Ruby code directly in the browser without the need for a backend.
As you can see in the example below, all it takes to get started with the Ruby Wasm port is a couple of lines. The script downloads ruby.wasm
and instantiates the interpreter in the browser. After that, it takes the text of text/ruby
type and feeds it into the WebAssembly program.
<html>
<script src="https://cdn.jsdelivr.net/npm/ruby-head-wasm-wasi@0.5.0/dist/browser.script.iife.js"></script>
<script type="text/ruby">
puts "Hello, world!"
</script>
</html>
You can confirm that Ruby is running from the browser, i.e. not connecting with a backend, by opening the developers’ tools. Here, you’ll find once ruby.wasm
is downloaded, no further connections are needed.
You can even see the contents of ruby.wasm
disassembled into text format in the “Sources” tab:
You can check out the Wasm port online at the Ruby playground.
king with the sandbox
As said, Wasm programs run in a sandboxed VM that lacks access to the rest of the system. Therefore, Wasm applications do not have access to the browser, filesystem, memory or the network. We’ll need some JavaScript code to send and receive data from the sandbox.
The following example shows how to read the output of a Ruby program and make changes to the page using the ruby-head-wasm-wasi NPM package:
<html>
<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);
vm.printVersion();
vm.eval(`
require "js"
luckiness = ["Lucky", "Unlucky"].sample
JS::eval("document.body.innerText = '#{luckiness}'")
`);
}; main();
</script>
<body></body>
</html>
The same package can also run Ruby code inside a Node project, allowing you to mix Ruby and JavaScript on the backend. You’ll need to install the NPM package ruby-head-wasm-wasi
for the example to work:
import fs from "fs/promises";
import { DefaultRubyVM } from "ruby-head-wasm-wasi/dist/node.cjs.js";
const main = async () => {
const binary = await fs.readFile(
// Tips: Replace the binary with debug info if you want symbolicated stack trace.
// (only nightly release for now)
// "./node_modules/ruby-head-wasm-wasi/dist/ruby.debug+stdlib.wasm"
"./node_modules/ruby-head-wasm-wasi/dist/ruby.wasm"
);
const module = await WebAssembly.compile(binary);
const { vm } = await DefaultRubyVM(module); vm.eval(`
luckiness = ["Lucky", "Unlucky"].sample
puts "You are #{luckiness}"
`);
};main();
Running Ruby WebAssembly outside the browser
While Wasm’s primary design goal is running binary code in the browser, developers quickly realized the potential of a fast, safe, and universally portable binary format for software delivery. Wasm has the potential to become as big a Docker, greatly simplifying application deployment for embedded systems, serverless functions, edge computing, or as a replacement for containers on Kubernetes.
Running a Wasm application outside the browser requires an appropriate runtime that implements the WebAssembly VM and provides interfaces to the underlying system. There are a few competing solutions in this field, the most popular being wasmtime, wasmer, and WAMR.
The Ruby repository provides a complete example for bundling your application code into a custom Ruby image.
Ruby WebAssembly Limitations
Let’s remember that this is all cutting-edge tech. The whole Wasm ecosystem is moving fast. Right now, Ruby Wasm has a few limitations which significantly limit its usability in big projects:
- No thread support.
- Spawning processes does not work.
- No network support.
- The garbage collector can create memory leaks.
- Gems and modules are unavailable unless you build a custom Wasm image.
The future is bright
WebAssembly opens a world of exciting possibilities. It allows Ruby developers to escape the backend. As tooling around WebAssembly improves, Ruby will be able to reach new frontiers: the browser is no longer off-limits, and there will be new opportunities to run Ruby on the edge and as serverless applications.
With the latest release, Ruby developers can begin experimenting with WebAssembly. It’s the first step, for sure, and there is much more work to do before we see complex Ruby applications running in with this technology.
Thanks for reading, and happy assembling!
Originally published at https://semaphoreci.com on February 9, 2023.