Mildly Internet
February 3, 2016 | code

Rust with Ruby

I’ve been writing a bit of Rust lately and I was super curious to see if I could embed Rust within Ruby to optimize some hot spots in a few programs I’d written. To test this out I implemented a Sudoku solver in Ruby using a simple brute force approach - very slow and very memory intensive. Nothing fancy or elegant. But an excellent candidate to see how much faster I could make it go.

A Simple Example

We’ll start by writing a simple Rust function to add two numbers together.

 #[no_mangle]
 pub extern "C" fn add(a: i32, b: i32) -> i32 {
    a + b
 }

The no_mangle attribute is required here. From the official Rust documentation:

When you create a Rust library, it changes the name of the function in the compiled output … This attribute turns that behavior off.

We don’t want the compiler changing the name of this function otherwise our Ruby program won’t be able to find it.

The pub declaration means that this function is callable from outside this module. extern "C" tells the compiler that it should be callable from C. From there it is just a normal Rust function called add which takes two variables and returns their sum.

Modify Cargo.toml to specify that we are creating a dynamic library.

[lib]
name = "add"
crate-type = ["dylib"]

Go ahead and compile this program and hop over to Ruby. To use this Rust function within our Ruby code we need to install the FFI gem.

Ruby-FFI is a ruby extension for programmatically loading dynamic libraries, binding functions within them, and calling those functions from Ruby code.

Within our program we create a module which will serve as the attachment point for our Rust program. We extend FFI and then give it the path to the compiled program. Then attach the function add from our Rust program to this module and specify the function signature - parameters first then return value.

require "ffi"

module RustAdd
  extend FFI::Library
  ffi_lib "target/release/libadd.dylib"
  attach_function :add, [:int, :int], :int 
end

puts RustAdd.add(1336, 1)
# => 1337

Note: if you are using Linux then you will want to use the .so extension instead of the .dylib

More Advanced Example

Going back to my Sudoku program - it takes a string and returns a string back out. Passing those through FFI is a little bit more work. Since strings don’t really exist in C (they are really arrays) we can’t just pass a Ruby string straight through to Rust. We have to deal with the pointer to the string. Yikes.

The solve function below takes a variable called c_ptr which is a C pointer. We’ll use std::ffi::CStr to wrap the pointer since it could be NULL, then convert it to a string slice so we can use it in our Rust program. On the way out the function uses CString to turn the solution back into a C pointer.

extern crate libc;

use libc::c_char;
use std::ffi::CStr;
use std::ffi::CString;

#[no_mangle]
pub extern "C" fn solve(c_ptr: *const c_char) -> *const c_char {
    let c_str = unsafe {
        assert!(!c_ptr.is_null());

        CStr::from_ptr(c_ptr)
    };

    let ruby_str = str::from_utf8(c_str.to_bytes()).unwrap();

    let mut sudoku = Sudoku::from_str(ruby_str).unwrap();
    sudoku.brute_force_solve();

    let solution = CString::new(format!("{}", sudoku)).unwrap();
    solution.into_raw()
}

On the Ruby side:

require "ffi"

class Sudoku
  def initialize(puzzle)
    @puzzle = puzzle
  end

  module RustSudoku
    extend FFI::Library
    ffi_lib "target/release/libsudoku.dylib"
    attach_function :solve, [:string], :string
  end


  def solve
    RustSudoku.solve(@puzzle)
  end
end

Now my brute force solver runs in about a hundredth of a second.

rspec sudoku_spec.rb
.

Finished in 0.0118 seconds
1 example, 0 failures

More Reading