⚡ 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.
👾 experimental
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
- Extend Cry::Wasm module to your Ruby class.
- Write Crystal type signatures for Ruby methods. The syntax is
[arg_t1, arg_t2], ret_t
(Symbol or String). - Crystal compiler compile the Ruby methods into WebAssembly as Crystal functions.
- Finally, call the wasm function!
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)
- 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)
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]
- Extend the Cry::Wasm module to the target class.
- Write the type information just before the method.
- Use the
cry
method to restrict argument types and return types.
- Use the
- Once the method is defined, Cry::Wasm captures the source code.
- Ripper converts source code to S-expression.
- Extracts the S-expression of the target method from the S-expression.
- Sorcerer recovers the Ruby source code of the target method from the S-expression.
- Add Crystal type restrictions to the Ruby source code to generate a Crystal code block.
- Cry::Wasm stores the Crystal code block.
- The Crystal compiler and wasm-ld compile the Crystal code into WebAssembly.
- Call the
cry_build
method to build the crystal code blocks.
- Call the
- The compiled byte_code is read, and an instance of Wasmer/Wasmtime is created.
- The target methods are dynamically redefined to call Wasmer/Wasmtime functions.
- 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.
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 |
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 |
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
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.
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.
Requirements
- Crystal - Follow the installation instructions here for your platform.
- Rust - Rust is required to compile the wasmer-ruby or wasmtime-rb.
- LLVM for macOS:
- Install LLVM by running
brew install llvm
- Find the path to wasm-ld by running
brew ls llvm | grep wasm-ld
. - Set the PATH environment variable so that
wasm-ld
can be called.
- Install LLVM by running
- LLD for Ubuntu:
- Install LLD by running
sudo apt install lld
. - Find the path to wasm-ld by running
dpkg -L lld | grep wasm-ld
. - If necessary, create a symbolic link for
wasm-ld-9
orwasm-ld-10
.
- Install LLD by running
- WebAssembly Libs for WASI
- Use the
rake vendor:wasi_libs
task to download the libs to the vendor directory. - If you install the libs outside the given directory, set the
CRYSTAL_LIBRARY_PATH
environment variable.
- Use the
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.
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.
MIT