Skip to content

Instantly share code, notes, and snippets.

@kateinoigakukun
Created March 16, 2022 06:32
Show Gist options
  • Star 0 You must be signed in to star a gist
  • Fork 0 You must be signed in to fork a gist
  • Save kateinoigakukun/cf62674daf21e57ec7b8c3fa73ca87df to your computer and use it in GitHub Desktop.
Save kateinoigakukun/cf62674daf21e57ec7b8c3fa73ca87df to your computer and use it in GitHub Desktop.

Final Report: WebAssembly/WASI Support in Ruby

Overview

CRuby is a highly portable C application, and it runs on many platforms (You can see which platforms are tested on CI here). However the "portability" is only at source level, and the built executable is not portable across architectures and system call interfaces.

This project made CRuby portable at executable file level by porting it to a standalone WebAssembly with WASI ABI. Furthermore, we implemented a VFS (Virtual File System) at WASI to package Ruby scripts into a single WebAssembly binary. These improvements make it easier to distribute Ruby scripts.

This is the final report required by the 2021 Ruby Association Grant.

About WebAssembly and WASI

WebAssembly is not only for porting existing projects to the web browser, but also for providing a sandboxed portable executable. It’s not limited to the web, and is being used in a variety of situations such as edge computing, embedded environments, and interfaces for plug-in systems.

The Ruby community has already ported CRuby to WebAssembly using Emscripten, but it depends heavily on the host JavaScript environment. It makes it difficult to use for the above applications.

WASI is an effort to define a standard set of syscalls for WebAssembly modules, allowing WebAssembly modules to not only be portable across architectures but also be portable across environments implementing this standard set of system calls. WASI does not rely on JavaScript at all.

For C and C++ users, WASI SDK, which is a LLVM compiler toolchain with musl and cloudlibc based libc wasi-libc is provided.

Implementation

Now the master branch of CRuby supports WASI as a build target by our several patches. The initial patch set for the support was ruby/ruby#5407 (has been merged)

This feature is tracked on "Proposal to merge WASI based WebAssembly support"

Today's WASI has limited system calls, and WebAssembly itself doesn't have context-switching mechanism. So CRuby needed some workarounds to address them. The notable changes are:

  • Add emulation implementations for some missing functionalities using Asyncify:
    • setjmp/longjmp for Exception
    • ucontext for Fiber
    • register scan for GC
  • Support no thread environment

The current master branch passes basictest, bootstraptest, and ruby/spec except for Thread and process related tests. And also it supports extention libraries linked statically.

Emulations powered by Asyncify

As mentioned above, the current WebAssembly has no context switching feature1, but there is an userland technique to pause and resume a WebAssembly process by binary transformation. This technique is called Asyncify.

Asyncify provides two operations to control program execution.

  1. Stop the current execution and unwind to the root call frame while writing out execution state and function-local registers (a.k.a Wasm locals) to memory
  2. Rewind to the stopped call frame while restoring saved exeuction state and registers.

You can see these emulation implementations under the wasm directory in the ruby/ruby repository.

setjmp/longjmp

Thanks to these magical operations, setjmp and longjmp can be simply emulated like following procedure:

  1. setjmp saves the current stack pointer and exeuction state by unwinding to the main, then rewinds to the call-site of setjmp.
  2. longjmp unwinds to the main, but discards the collected execution state and rewinds to the call-site of setjmp saved at step 1, then restore the saved stack pointer.

However, this setjmp emulation gets slower and slower as the call stack gets deeper! In addition, setjmp is used in the core of the Ruby VM. So we mitigated the performance penalty by avoiding rewinds to the main in the core implementation2.

We've also considered using exception-handling proposal and it's 1.88 times faster than Asyncify approach in microbenchmark. However we found that it requires quite a few changes to rewrite all use of setjmp/longjmp with try-catch style at C level. We decided to use Asyncify approach for now to minimize the effects of our patches rather than performance gain.

Fiber (ucontext)

Similar to setjmp/longjmp, Fiber on WASI exploits Asyncify. It simply switches the execution state by unwinding/rewinding and swaps stack pointers.

Scanning registers (Wasm locals) for GC

CRuby uses conservative garbage collection, which marks pointer-like values by scanning some value spaces to find living objects.

While running WebAssembly program, Ruby object (VALUE) can be put in:

  1. Wasm Stack: Defined in Wasm spec
  2. Function-local Registers (Wasm Locals): Defined in Wasm spec
  3. C Stack: Allocated in linear memory

Unlike the normal memory space like 3. C Stack, 1. Wasm Stack and 2. Function-local Registers cannot be scanned dynamically. Fortunately, Asyncify writes Wasm Locals and Wasm Stacks out as execution states, so GC unwinds and rewinds by Asyncify and scans the execution states stored in the linear memory.

Virtual File System for WASI

We implemented a VFS (Virtual File System) at WASI, named wasi-vfs, to package Ruby scripts into a single WebAssembly binary. It's not only for Ruby, but also for any application using wasi-libc.

Usage

You can package directories with the same options as when running in wasmtime or wasmer.

# Without packing
$ wasmtime run ruby.wasm --mapdir /::./lib -- /irb.rb
irb(main):001:0>
...
# Packing with wasm-vfs
$ wasm-vfs pack ruby.wasm --mapdir /::./lib -o irb.wasm
$ wasmtime run irb.wasm -- /irb.rb
irb(main):001:0>
...

Note that it currently requires to link libwasi_vfs.a because it depends on linker's symbol resolution mechanism to hook WASI system calls, and also needs to merge data sections. However this limitation will be removed after module-linking will be in place.

How does it work?

The wasi-vfs pack command is a wrapper of Wizer, which is a pre-initializer of Wasm applications. See also Bytecode Alliance: Making JavaScript run fast on WebAssembly.

The initialization process in wasi-vfs scans the mapped directories, then copies them into in-memory virtual filesystem. Then Wizer take a snapshot of that state, and save it as a Wasm file.

Interoperation with JavaScript

We've also implemented a Ruby <-> JavaScript interop library as a npm package for browser and Node.js. It's already used in try.ruby-lang.org.

const vm = await RubyVM.default();
vm.eval(`
  luckiness = ["Lucky", "Unlucky"].sample
  JS::eval("document.body.innerText = '#{luckiness}'")
`);

Demo projects

Performance benchmark

Here is a performance benchmark report of Optcarrot comparing with native CRuby and other Ruby implementations. (bigger is better)

master (wasm32-wasi) and opal use Node.js v16.14.0 as their JS and Wasm runtime.

summary full

Performance profiler reports similar trends for heaviest functions, so the bottleneck is not in CRuby implementation but Wasm runtime is slower than native. Also, it includes Asyncify overheads as we mentioned above.

Other trivial works

  • ruby.wasm: Nightly release of prebuilt Wasm target Ruby
  • wasi-preset-args: A tool to preset command-line arguments to a WASI module
    • This is useful to set script path passed to Ruby.
    • Some platforms like Compute@Edge cannot give arguments, so this tool is essential
  • optcarrot.wasm: A game emulator written in Ruby on browser powered by WebAssembly
  • irb.wasm: IRB on browser powered by WebAssembly

Future work ideas

  • Snapshot an initialized VM state as an executable Wasm file
  • Compress files embedded by wasi-vfs
  • RDoc integration to run sample code in browser
  • Extend RubyGems to allow linking extension library statically

Conclusion

We have shown how we ported CRuby to WebAssembly with WASI ABI, and introduced related tools. This achievement will promote the use of Ruby in more environments.

If no major issues are found, it will be released as Ruby 3.2.

Footnotes

  1. spec is under discussion: https://github.com/WebAssembly/stack-switching

  2. [wasm] vm.c: stop unwinding to main for every vm_exec call by setjmp by kateinoigakukun · Pull Request #5502 · ruby/ruby

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment