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.
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
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.
Here are the most significant changes that helped add WASI support to CRuby 3.2:
Adding context switching using Asyncify
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 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:
Saving the current stack pointer and execution state at the call site of setjmp, and then unwinding to the main call stack.
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.
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.
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:
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.
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.
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.
WASI support in Ruby is still in its early stages but has already given rise to some cool projects.
Ruby.wasm Todo list
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!
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.