All Posts

Ruby 3.2's WASI Integration: A Closer Look

Ruby 3.2's WASI Integration: A Closer Look

Ruby 3.2 was released on 25 December, 2022. It added many features and performance improvements, the most noteworthy being YJIT and WASI support. While YJIT helps us run Ruby a lot faster, WASI support makes Ruby a lot more portable, bringing us one step closer to "Write once, run anywhere".

It enables the CRuby interpreter to run on a web browser, a serverless edge environment, or other kinds of WebAssembly/WASI embedders. In this blog post, we will discover how it was added.

What is WASM

WebAssembly (WASM) is a binary instruction format designed to be a portable target for compiling high-level languages like C, C++, and Rust. With WASI support, it is now possible to use Ruby to write code that can run on the web at near-native speed, making it a viable option for web-based applications and client-side scripting.

What is WASI

WebAssembly Standard Interface (WASI) is an effort to define a set of standard syscalls for WebAssembly modules, allowing them to be compatible betweeen multiple architectures and environments.

What can WASI help you achieve? Without getting too deep into WASI's value proposition, here are the top-level goals of WASI:

  • Cross-platform applications: You can have a single binary or executable that can be run on any platform that has a WebAssembly runtime.
  • Code re-use between platforms and use cases: Just like how it's possible to use JavaScript on multiple platforms (frontend, backend, embedded systems, etc.), WASI makes it possible to do so using any language.
  • Single runtime for all languages: Instead of having multiple language-specific runtimes, you could compile all of your different projects to the same target, and have a single runtime run them all!
  • Package applications into a single target binary: An application along with its dependencies can be compiled into a single target of one or more WebAssembly files. This would not be a replacement for containerization, but could be a convenient option for applications.

How Ruby added WASI support

CRuby has already supported compiling to WebAssembly target for a while. This is done using Emscripten, but Emscripten heavily depends on JavaScript to emulate some missing features in WebAssembly itself. However, this approach is not feasible for environments that don't have or provide JavaScript runtimes, e.g., edge computing platforms, IoT devices, etc.

Therefore, WASI support was added to CRuby to allow it to run on non-JS environments. Furthermore, a Virtual File System was implemented for WASI to package multiple Ruby scripts into a single WASM binary.

This project was the result of fantastic work done by Yuta Saito as a part of the 2021 Ruby Association Grant.

Here are the most significant changes that helped add WASI support to CRuby 3.2:

  1. Adding context switching using Asyncify
  2. Implementing a Virtual File System (VFS)

Context switching using Asyncify

Pausing and resuming code can be helpful for various things, like implementing coroutines, async/await, limiting how much CPU time untrusted code gets, and so forth. WebAssembly does not support context switching out of the box, so a userspace technique called Asyncify is used to make WASM code asynchronous.

The basic capabilities we need in something like Asyncify are:

  • Unwind and rewind the call stack
  • Jump back to the right place in the middle of the function in each case
  • Preserve locals while doing these jumps

setjmp/longjmp

setjmp and longjmp are a pair of C functions that are commonly used to implement exception handling, by facilitating the cross-procedure transfer of control. WebAssembly cannot support this out of the box since it does not support context switching, and therefore can not handle exceptions natively. However, this limitation is overcome with the help of Asyncify.

Asyncify allows for the emulation of these functions by:

  1. Saving the current stack pointer and execution state at the call site of setjmp, and then unwinding to the main call stack.
  2. In the case of longjmp, discarding the collected execution state, rewinding to the call-site of setjmp saved earlier, and then restoring the saved stack pointer.
Asyncify setjmp implementation diagram

Source: An Update on WebAssembly/WASI Support in Ruby

Fiber (coroutines)

Fibers are Ruby's implementation of coroutines, i.e., their execution can be manually paused and resumed. Similar to setjmp/longjmp, Fiber on WASI exploits Asyncify. It simply switches the execution state by unwinding/rewinding and swapping stack pointers of each Fiber.

Asyncify fiber implementation diagram

Source: An Update on WebAssembly/WASI Support in Ruby

The garbage collection problem

WASM doesn't support garbage collection(GC) out of the box, so languages that don't have GC — C, C++, and Rust — were among the first to provide WASM support. Languages that use GC currently have a more challenging time compiling to WASM, but there is a proposal to add GC to it.

Ruby is a garbage-collected language that uses the mark and sweep algorithm, which marks pointer-like values by scanning value spaces to find living objects. You can learn more about how Ruby's garbage collection works from a fantastic series of blog posts by Jemma Issroff.

While running a WebAssembly program, a Ruby object can be stored in:

  1. WASM Stack
  2. Function-local Registers
  3. WASM Linear Memory

Unlike the normal memory space, Linear Memory, WASM Stack, and Function-local Registers cannot be dynamically scanned. Fortunately, Asyncify stores local virtual registers and WASM Stacks as execution states, so the GC unwinds and rewinds with Asyncify and scans the execution states stored in the linear memory.

Virtual File System for WASI

To package multiple Ruby scripts into a single WASM binary, a Virtual File System (VFS) was needed. This was created as a separate library so that any application that utilizes wasi-libc can take advantage of wasi-vfs, not just Ruby.

WASI-VFS diagram

Source: An Update on WebAssembly/WASI Support in Ruby

Caveats

Lack of threads support

Although WebAssembly supports Threads in browsers through Web Workers, WASI does not provide a spec for managing threads on the environments outside the browsers. Hence, it is not possible to use Ruby Threads when compiling to a WASI target.

There is now a proposal in place defining how to manage threads outside the browser.

Register operations

We've seen how we had to jump through hoops to implement simple context switching using Asyncify. This had to be done because WebAssembly doesn't give direct access to the program counter, preventing us from jumping directly to instruction.

Even wasi-libc does not provide such implementations, so it had to be done manually. However, like other things in this list, native support for context-switching is also on the roadmap.

Cool projects

WASI support in Ruby is still in its early stages but has already given rise to some cool projects.

Ruby.wasm Todo list

Using the js gem that implements a lot of the JavaScript functionality needed to interact with the DOM, Adam Hess created a to-do list in Ruby, and you can change the way it works and see the changes instantly.

You can check it out here.

Ruby.wasm Todo Screenshot

irb.wasm

Built by Yuta Saito, this is the Interactive Ruby Shell (irb) that runs in your browser with the help of WASM. Since its been compiled to WASM, it doesn't need a backend server to run it - it all happens in the frontend!

You can check it out here.

IRB WASM Screenshot

Conclusion

The addition of WASI support in Ruby 3.2 opens up a whole new world of possibilities for Ruby developers. Whether you're building web-based applications, creating tools for the web, or working with embedded systems, WASI support makes it easier than ever to harness the power of WebAssembly with Ruby.

Get started with DeepSource

DeepSource is free forever for small teams and open-source projects. Start analyzing your code in less than 2 minutes.

Newsletter

Read product updates, company announcements, how we build DeepSource, what we think about good code, and more.

Thank you! Your submission has been received!
Oops! Something went wrong while submitting the form.