Bending the Curve: Writing Safe & Fast Native Gems With Rust

Last week at GoGaRuCo, I talked about how Rust bends the performance/safety curve and enables a whole new generation of high-level programmers to become systems-level programmers.

That tradeoff loomed large when we started building Skylight.

We knew that we wanted to process high-fidelity traces of every request, in order to provide you a detailed picture of your production application.

But as our customer base grew, and as we added more features, we saw the limits of using Ruby to do all of the heavy lifting on the client. We had some ambitious features in mind, and even though we know Ruby really well, we could see that we wouldn't be able to do everything we wanted to do in pure Ruby without introducing unacceptable amounts of overhead.

At first, we thought we would move some of our agent gem to C++. Carl even went as far as to write an experimental version of parts of the gem in C++, but I didn't feel comfortable maintaining a growing codebase written in an unsafe language that ran inside of our customers' applications.

As Benjamin Franklin said, "those who would give up memory safety for performance deserve neither."

At the time, Rust was still in a lot of flux, but the idea of a language that could give us high performance and low memory usage, with guaranteed safety (no segfaults!) made us consider it seriously. Before long, we shipped a version of the Ruby gem with parts written in Rust, and haven't looked back.

Today, I want to show you how you can write some code in Rust and use it from Ruby. I won't teach you Rust, Steve Klabnik's excellent official guide is a very thorough introduction to Rust.

A Simple Rust Program

Let's start with a simple Rust library, that provides Point and Line structs. The Line struct allows us to calculate the length of the line.

use std::num::pow;
 
pub struct Point { x: int, y: int }
struct Line  { p1: Point, p2: Point }
 
impl Line {
  pub fn length(&self) -> f64 {
    let xdiff = self.p1.x - self.p2.x;
    let ydiff = self.p1.y - self.p2.y;
    ((pow(xdiff, 2) + pow(ydiff, 2)) as f64).sqrt()
  }
}

For this simple example, we would like to do is enable a Ruby program to create two Points and compute the distance between them.

In order to do this, we are going to expose two extern functions from Rust. First, let's create a function that takes two integers and returns a Point. We'll call this file points.rs.

#[no_mangle]
pub extern "C" fn make_point(x: int, y: int) -> Box<Point> {
    box Point { x: x, y: y }
}

Looking at this definition carefully:

  • #[no_mangle] means that this function is exposed as a global function. Normally, Rust functions are "mangled" internally, so they don't conflict with other global C symbols. Later, this will allow us to see this function in Ruby.
  • extern "C" means that the function is exposed as if it was a C function. This tells Rust to make sure to use the C ABI, so that other programming languages will be able to call it.
  • Box<Point> means that we return this point as a heap allocated pointer. This will allow us to pass it around inside of Ruby.

Next, let's compile points.rs into a dynamic library:

$ rustc points.rs --crate-type=dylib
$ ls
points.rs   libpoints.so

Next, let's use Ruby's fiddle API to bind to this function. Fiddle allows us to call C functions from Ruby, and work with the results.

// points.rb

require "fiddle"
require "fiddle/import"

module RustPoint
  extend Fiddle::Importer
  
  dlload "./libpoints.so"
  
  extern "Point* make_point(int, int)"
end

This creates a RustPoint.make_point method that you can call with two Ruby integers:

> require "./points"
 => true
> RustPoint::make_point(10, 10)
 => #<Fiddle::Pointer:0x007fefcc80e530 ptr=0x0000010240a060 size=0 free=0x00000000000000>

We now have a way to ask Rust for a Point object. Now let's create a function in Rust that takes two points and returns the distance:

#[no_mangle]
pub extern "C" fn get_distance(p1: &Point, p2: &Point) -> f64 {
    Line { p1: *p1, p2: *p2 }.length()
}

This function takes two borrowed Points, which means that, conceptually, the Ruby code still owns the Points. By taking a borrowed pointer, the Rust compiler will guarantee that our Rust code doesn't try to hang on to the pointer or deallocate it.

One way to think about this is that when we pass a Box pointer to Ruby, we are handing Ruby ownership of the pointer (in terms of the semantics of Rust). In the future, we'll lend the pointer back to Rust, which ensure that the pointer only sticks around in Rust for as long as that call.

Now that we wrote get_distance, let's use it:

require "fiddle"
require "fiddle/import"

module RustPoint
  extend Fiddle::Importer

  dlload "./libpoints.dylib"

  extern "Point* make_point(int, int)"
  extern "double get_distance(Point*, Point*)"
end
> p1 = RustPoint::make_point(10, 10)
 => #<Fiddle::Pointer:0x007fc303746f20 ptr=0x0000010840a060 size=0 free=0x00000000000000>
> p2 = RustPoint::make_point(20, 20)
 => #<Fiddle::Pointer:0x007fc3035bec50 ptr=0x0000010840a070 size=0 free=0x00000000000000>
> RustPoint::get_distance(p1, p2)
 => 14.142135623730951

This is just a taste of how to use Rust from Ruby. I didn't cover freeing memory or exception safety, which will be the subject of future posts (soon!).

If you have a Rails app and want to get deeper knowledge about its performance, sign up for Skylight. You get 30 days to try it out, no credit card required.

For now, happy Rusting!