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 |
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.