cry-wasm

:zap: cry-wasm speeds up Ruby code.

By applying simple type restrictions to Ruby code, convert it to Crystal code, compile it to WebAssembly, and call it with Wasmer or Wasmtime.

:space_invader: experimental

Quick Start

require 'cry/wasm'

class Fibonacci
  extend Cry::Wasm            # (1) Extend your class

  cry [:Int32], :Int32        # (2) Write type signatures
  def fib(n)
   return 1 if n <= 2
    fib(n - 1) + fib(n - 2)
  end

  cry_build                   # (3) Compile Wasm
end

Fibonacci.new.fib(40)         # (4) Call Wasm Function
  1. Extend Cry::Wasm module to your Ruby class.

  2. Write Crystal type signatures for Ruby methods. The syntax is [arg_t1, arg_t2], ret_t (Symbol or String).

  3. Crystal compiler compile the Ruby methods into WebAssembly as Crystal functions.

  4. Finally, call the wasm function!

Benchmark

fib_bench.rb - 10 x faster on the Fibonacci benchmark.

user     system      total        real
ruby     fib(40)   5.305503   0.000000   5.305503 (  5.305696)
wasmtime fib(40)   0.462232   0.000000   0.462232 (  0.462247)
wasmer   fib(40)   0.381384   0.000000   0.381384 (  0.381401)

<img src=“raw.githubusercontent.com/kojix2/cry-wasm/main/doc/benchmark.svg” width=“40%” height=“40%”><img src=“” width=25% height=“25%”>

  • In this benchmark, Wasmer is about 10% faster than Wasmtime as of December 2022.

  • Both Wasmer and Wasmtime tend to take a little longer for the first call. (see line graph at n=1)

  • Wasm is only about twice as slow as native functions, making it highly efficient. (according to my measurements)

How does this work?

flowchart LR
style id1 fill:#c5c,stroke:#f66,stroke-width:1px,color:#fff
style id2 fill:#555,stroke:#3ff,stroke-width:1px,color:#fff
style id3 fill:#66f,stroke:#f66,stroke-width:1px,color:#fff
style id4 fill:#c5c,stroke:#ff1,stroke-width:1px,color:#fff
    id1(Ruby Methods) -- Ripper + Sorcerer --> id2(Crystal Functions) -- Crystal Compiler --> id3[WebAssembly]
    id4(Ruby Code) <-- Wasmer/Wasmtime --> id3[WebAssembly]
  1. Extend the Cry::Wasm module to the target class.

  2. Write the type information just before the method.

  3. Use the cry method to restrict argument types and return types.

  4. Once the method is defined, Cry::Wasm captures the source code.

  5. Ripper converts source code to S-expression.

  6. Extracts the S-expression of the target method from the S-expression.

  7. Sorcerer recovers the Ruby source code of the target method from the S-expression.

  8. Add Crystal type restrictions to the Ruby source code to generate a Crystal code block.

  9. Cry::Wasm stores the Crystal code block.

  10. The Crystal compiler and wasm-ld compile the Crystal code into WebAssembly.

  11. Call the cry_build method to build the crystal code blocks.

  12. The compiled byte_code is read, and an instance of Wasmer/Wasmtime is created.

  13. The target methods are dynamically redefined to call Wasmer/Wasmtime functions.

Usage

It define crystal functions, not Crystal methods

  • Default arguments, keyword arguments, and block arguments are not available.

  • Instance variables and class variables are not available on the top level function.

  • To use your own Crystal class, use cry_load(path) to pre-load your crystal source code.

Type conversion

Arguments ( Ruby –> Crystal )

Ruby class Crystal class
‘Integer` ‘UInt8` `Int8` `UInt16` `Int16` `UInt32` `Int32` `UInt64` `Int64`
‘Float` ‘Float32` `Float64`
‘Array<Integer>` ‘UInt8*` `Int8*` `UInt16*` `Int16*` `UInt32*` `Int32*` `UInt64*` `Int64*`
‘Array<Integer>` ‘Array(UInt8)` `Array(Int8)` `Array(UInt16)` `Array(Int16)` `Array(UInt32)` `Array(Int32)` `Array(UInt64)` `Array(Int64)`
‘Array<Float>` ‘Float32*` `Float64*`
‘Array<Float>` ‘Array(Float32)` `Array(Float32)`
‘String` ‘String`

Return values ( Crystal –> Ruby )

Crystal class Ruby class
‘UInt8` `Int8` `UInt16` `Int16` `UInt32` `Int32` `UInt64` `Int64` ‘Integer`
‘Float32` `Float64` ‘Float`
‘UInt8*` `Int8*` `UInt16*` `Int16*` `UInt32*` `Int32*` View object of Wasmer (wasmer only)
‘Array(UInt8)` `Array(Int8)` `Array(UInt16)` `Array(Int16)` `Array(UInt32)` `Array(Int32)` `Array(UInt64)` `Array(Int64)` ‘Array<Integer>`
‘Array(Float32)` `Array(Float32)` ‘Array<Float>`
‘String` ‘String`
‘Void` ‘Nil`

Why is Symbol not supported?

In the Crystal language, Symbol is converted to an integer at compile time, so there is no way to get Symbol from a String; use String instead of Symbol.

Cry::Numeric uses Refinement to add methods to Ruby’s numeric classes

Cry::Numeric can use Refinements to add methods such as to_i8, to_u8, and to_f32 to Ruby’s numeric classes. These methods are the same as to_i and to_f (the range of values is not checked). These are useful if you want to prevent errors when running your code as Ruby and get the same results as if you had run it as Crystal.

Why is it very slow to return arrays?

Currently reading memory in wasm and converting it to Ruby arrays takes quite a bit of time. As a result, it may take longer to run with cry-wasm than when run as pure Ruby. Also note that currently (2022/12) wasmtime-rb is faster than wasmer-ruby when it comes to reading memory. If you are interested in improving these issues, please consider contributing to wasmer-ruby or wasmtime-rb.

Installation

Requirements

  1. Crystal - Follow the installation instructions here for your platform.

  2. Rust - Rust is required to compile the wasmer-ruby or wasmtime-rb.

  3. LLVM for macOS:

  4. Install LLVM by running brew install llvm

  5. Find the path to wasm-ld by running brew ls llvm | grep wasm-ld.

  6. Set the PATH environment variable so that wasm-ld can be called.

  7. LLD for Ubuntu:

  8. Install LLD by running sudo apt install lld.

  9. Find the path to wasm-ld by running dpkg -L lld | grep wasm-ld.

  10. If necessary, create a symbolic link for wasm-ld-9 or wasm-ld-10.

  11. WebAssembly Libs for WASI

  12. Use the rake vendor:wasi_libs task to download the libs to the vendor directory.

  13. If you install the libs outside the given directory, set the CRYSTAL_LIBRARY_PATH environment variable.

Installation

bundle install
bundle exec rake vendor:wasi_libs
bundle exec rake install

Please note that cry-wasm depends on the latest API of wasmer-ruby and wasmtime-rb, so we have to use the GitHub master rather than the stable version.

Tested on macOS and Ubuntu using Github Actions. Windows is not yet supported.

Development

git clone https://github.com/kojix2/cry-wasm
cd cry-wasm
bundle install
bundle exec rake vendor:wasi_libs
bundle exec rake spec
  • Trying out WASM Support - A thread in the Crystal Forum on how to compile a wasm from crystal.

  • wasm-libs - WebAssembly Libs for WASI. You need to download the compiled wasm library.

Even small improvements like fixing typos are welcome! Please feel free to send us your PR.

license

MIT