TL;DR: here is an example of a gem written in Rust.

Why?

Ruby is slow, right? Most of the time we don’t care, but sometimes we do. And when we do, there are not so many options: C, C++, microservices/rpc/jruby, … . C is good enough to shot yourself in the foot. C++, well, just C++. Other options are to comprehensive for a simple task, like CPU intensive algorithm. You can probably use Go, but it has its own runtime with garbage collector which adds more overhead.

Rust is another option because of:

  • safety
  • speed
  • no runtime

There are couple of challenges with Ruby-Rust integration (actually, for Ruby-whatever integration). This post is about them.

Passing data between Ruby and Rust

This is relatively easy when you are passing simple integers, but if you need to pass complex objects there is a lot of headache. You can avoid it by using JSON and passing it as string. If you need to call a function in Rust and get result of its CPU intensive calculation, then it’s OK to have JSON overhead.

One gotcha is that you have to deallocate memory allocated for char * inside Rust after it has been returned to Ruby. That’s why we have rust_free function in Rust, which is a wrapper for libc’s free:

require 'rustygem/version'
require 'fiddle'
require 'json'

module Rustygem
  @lib = Fiddle.dlopen("#{File.dirname(__FILE__)}/../rust/target/release/librustygem.so")
  @rust_perform = Fiddle::Function.new(@lib['rust_perform'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOIDP)
  @rust_free = Fiddle::Function.new(@lib['rust_free'], [Fiddle::TYPE_VOIDP], Fiddle::TYPE_VOID)

  def self.perform(arg)
    ptr = @rust_perform.call(arg.to_json) # do the actual work
    result = ptr.to_s
    @rust_free.call(ptr) # char* was allocated in Rust, so don't forget to free it
    JSON.parse(result)
  end
end
#[no_mangle]
pub extern "C" fn rust_free(c_ptr: *mut libc::c_void) {
    unsafe {
        libc::free(c_ptr);
    }
}

Check out rustygem for more details.

Building dynamic library when installing the gem

Ruby gems have builtin mechanism for building native extensions in C. But what about Rust? Actually, we don’t need any external tools to build Rust library because in Rust we have Cargo which is very similar to bundler. So, the only thing we need to do is call cargo build inside Rust project. But how to do this when installing the gem? Turns out this is very simple. Just put Makefile along with empty extconf.rb and add extconf.rb to gemspec.

# rust/Makefile:
all:
	cargo build --release

clean:
	rm -rf target

install: ;
# gemspec
spec.extensions = Dir['rust/extconf.rb']

Optionally, you can put some checks into extconf.rb:

# rust/extconf.rb
raise 'You have to install Rust with Cargo (https://www.rust-lang.org/)' if !system('cargo --version') || !system('rustc --version')

Conclusion

There is an alternative to C. And it’s not so hard to use Rust in Ruby project.

This post illustrates very basic approach to integration Rust with Ruby. There is Helix project which is much more comprehensive. Check it out if you want less boilerplate and more nice API.