Ruby 3.2's WASI Integration: A Closer Look
An insight into the implementation of WASI support in Ruby 3.2
- By Faraaz
- ·
- Ruby
- WebAssembly
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:
- 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/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:
- 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 ofsetjmp
saved earlier, and then restoring the saved stack pointer.
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.
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:
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.
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 Thread
s 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.
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.
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.