Introducing Ruvy

We’ve recently open sourced a project called Ruvy! Ruvy is a toolchain that takes Ruby code as input and creates a WebAssembly module that will execute that Ruby code. There are other options for creating Wasm modules from Ruby code. The most common one is ruby.wasm. Ruvy is built on top of ruby.wasm to provide some specific benefits. We created Ruvy to take advantage of performance improvements from pre-initializing the Ruby virtual machine and Ruby files included by the Ruby script as well as not requiring WASI arguments to be provided at runtime to simplify executing the Wasm module.

WASI is a standardized collection of imported Wasm functions that are intended to provide a standard interface for Wasm modules to implement many system calls that are present in typical language standard libraries. These include reading files, retrieving the current time, and reading environment variables. To provide context for readers not familiar with WASI arguments, WASI arguments are conceptually similar to command line arguments. Code compiled to WASI to read these arguments is the same code that would be written to read command line arguments for code compiled to target machine code. WASI arguments are distinct from function arguments and standard library code uses the WASI API to retrieve these arguments.

Using Ruvy

At the present time, Ruvy does not ship with precompiled binaries so its build dependencies need to be installed and then Ruvy needs to be compiled before it can be used. The details for how to install these dependencies is available in the README.

After building Ruvy, you can run:

The content of ruby_examples/hello_world.rb  is:

When running Ruvy, the first line builds and executes the CLI to take the content of ruby_examples/hello_world.rb and creates a Wasm module named index.wasm that will execute puts “Hello world” when index.wasm’s exported _start function is invoked.

To use additional Ruby files, you can run:

Where the content of ruby_examples/use_preludes_and_stdin.rb is:

And the prelude directory contains two files. One with the content:

And another file with the content:

The preload flag tells the CLI to include each file in the directory specified, in this case prelude, into the Ruby virtual machine, which will make definitions for those files available to the input Ruby file.

What makes Ruvy different from ruby.wasm

Ruby.wasm

Ruby.wasm is a collection of ports of CRuby to WebAssembly targeting different environments such as web browsers through Emscripten and non-web environments through WASI. Ruby.wasm’s WASI ports include a Ruby interpreter that is compiled to a Wasm module and that module can use WASI APIs. For the Ruby interpreter to be useful in most use cases, it needs access to a filesystem to load Ruby files to execute. While it’s possible to ship Ruby files along with the Ruby interpreter Wasm module and specify in a WASI-compatible WebAssembly runtime to allow access to the directory containing those Ruby files from the interpreter’s Wasm instance, there’s a somewhat easier approach. You can use a tool called wasi-vfs (short for WASI virtual file system) to pack the contents of specified directories into a WebAssembly module at build time. This allows the Ruby interpreter to access the contents of the Ruby files without also having to ship the Ruby files separately with your Wasm module.

Using wasi-vfs with ruby.wasm looks like:

Running one of these modules requires providing the path to a Ruby script for the Ruby virtual machine to execute as a WASI argument. You can see that with the -- /src/my_app.rb argument to Wasmtime.

Pre-initializing

When using a ruby.wasm Wasm module built with wasi-vfs (WASI virtual file system), a tool which takes a specified directory and creates a Wasm module containing a collection of specified files in a specified set of paths, the Ruby virtual machine is started during the execution of the Wasm module. Whereas Ruvy pre-initializes the Ruby virtual machine when the Wasm module is built, which improves runtime performance by around 20%.

Here are some benchmark results from timing how long it takes to instantiate and execute a _start function using Wasmtime:

Description

Toolchain

Low

Mid

High

Hello world

Ruby.wasm + wasi-vfs

55.833 ms

56.262 ms

56.730 ms

Ruvy

44.367 ms

44.543 ms

44.739 ms

Includes + logic

Ruby.wasm + wasi-vfs

56.081 ms

56.487 ms

56.932 ms

Ruvy

44.449 ms

44.763 ms

45.216 ms

Execution benchmark results

 

The “Hello world” example is just running puts “Hello world” and the “Includes + logic” example uses a file that is required containing a class that changes some input in a trivial way.

Here are some benchmark results from comparing how long it takes Wasmtime to compile a ruby.wasm module and a Ruvy module from Wasm to native code using the Cranelift compiler:

Description

Toolchain

Low

Mid

High

Hello world

Ruby.wasm + wasi-vfs

1.6351 s

1.6590 s

1.6844 s

Ruvy

439.93 ms

446.31 ms

452.81 ms

Includes + logic

Ruby.wasm + wasi-vfs

1.6227 s

1.6460 s

1.6706 s

Ruvy

442.83 ms

449.40 ms

456.39 ms

Compilation benchmark results

We can see that Ruvy Wasm modules take ~70% less time to compile from Wasm to native code.

No need to specify arguments when executing

Wasm modules created by Ruvy do not require providing a file path as a WASI argument. This makes it compatible with computing environments that cannot be configured to provide additional WASI arguments to start functions, for example various edge computing services.

Why we open sourced Ruvy

We think Ruvy might be useful to the wider developer community by providing a straightforward way to build and execute simple Ruby programs in WebAssembly runtimes. There are a number of improvements that would also be very welcome from external contributors that we’ve documented in our README. Shopify Partners who would prefer to reuse some of their Shopify Scripts Ruby logic in Shopify Functions may be particularly interested in addressing the compatibility with Shopify Functions items that are listed.

Jeff Charles is a Senior Developer on Shopify's Wasm Foundations team. You can find him on GitHub as @jeffcharles or on LinkedIn at Jeff Charles.