Your first Ruby native extension: C

A few months back I released faye-websocket 0.4, my first gem that contained native code. After a few mis-step releases I got a working build for MRI and JRuby, but getting there was a little tricky. What follows is a quick how-to from someone who knows barely any C or Java, to explain how to wire your code up for release.

This only covers one possible use case for native code: rewriting a pure function in native code to make it faster. It does not cover binding to native libraries, or FFI, just writing some vanilla C/Java to make some hot code faster. In faye-websocket’s case, this is the function in question:

bytes = [72, 101, 108, 108, 111, 44, 32, 119, 111, 114, 108, 100, 33]
mask  = [23, 142, 94, 24]

Faye::WebSocket.mask(bytes, mask)
# => [95, 235, 50, 116, 120, 162, 126, 111, 120, 252, 50, 124, 54]

It takes an arbitrary list of bytes, and a list of four bytes, and XORs the first set using the second set (this is part of how data is encoded in WebSocket frames). You’d implement it in Ruby like this:

def mask(payload, mask)
  result = []
  payload.each_with_index do |byte, i|
    result[i] = byte ^ mask[i % 4]
  end
  result
end

It turns out Ruby’s quite slow at doing this, and you get a big performance boost by writing in C. Remember that what we want to do is define a singleton method called mask(payload, mask) on the module Faye::WebSocket. I’ll just show you all the code for doing that in C, which we’re going to save in ext/faye_websocket/faye_websocket.c. All I know about the Ruby C APIs I got through Google, and I’ve annotated this code with some useful tips.

// ext/faye_websocket/faye_websocket.c

#include <ruby.h>

// Allocate two VALUE variables to hold the modules we'll create. Ruby values
// are all of type VALUE. Qnil is the C representation of Ruby's nil.
VALUE Faye = Qnil;
VALUE FayeWebSocket = Qnil;

// Declare a couple of functions. The first is initialization code that runs
// when this file is loaded, and the second is the actual business logic we're
// implementing.
void Init_faye_websocket();
VALUE method_faye_websocket_mask(VALUE self, VALUE payload, VALUE mask);

// Initial setup function, takes no arguments and returns nothing. Some API
// notes:
// 
// * rb_define_module() creates and returns a top-level module by name
// 
// * rb_define_module_under() takes a module and a name, and creates a new
//   module within the given one
// 
// * rb_define_singleton_method() take a module, the method name, a reference to
//   a C function, and the method's arity, and exposes the C function as a
//   single method on the given module
// 
void Init_faye_websocket() {
  Faye = rb_define_module("Faye");
  FayeWebSocket = rb_define_module_under(Faye, "WebSocket");
  rb_define_singleton_method(FayeWebSocket, "mask", method_faye_websocket_mask, 2);
}

// The business logic -- this is the function we're exposing to Ruby. It returns
// a Ruby VALUE, and takes three VALUE arguments: the receiver object, and the
// method parameters. Notes on APIs used here:
// 
// * RARRAY_LEN(VALUE) returns the length of a Ruby array object
// * rb_ary_new2(int) creates a new Ruby array with the given length
// * rb_ary_entry(VALUE, int) returns the nth element of a Ruby array
// * NUM2INT converts a Ruby Fixnum object to a C int
// * INT2NUM converts a C int to a Ruby Fixnum object
// * rb_ary_store(VALUE, int, VALUE) sets the nth element of a Ruby array
// 
VALUE method_faye_websocket_mask(VALUE self, VALUE payload, VALUE mask) {
  int n = RARRAY_LEN(payload), i, p, m;
  VALUE unmasked = rb_ary_new2(n);
  
  int mask_array[] = {
    NUM2INT(rb_ary_entry(mask, 0)),
    NUM2INT(rb_ary_entry(mask, 1)),
    NUM2INT(rb_ary_entry(mask, 2)),
    NUM2INT(rb_ary_entry(mask, 3))
  };
  
  for (i = 0; i < n; i++) {
    p = NUM2INT(rb_ary_entry(payload, i));
    m = mask_array[i % 4];
    rb_ary_store(unmasked, i, INT2NUM(p ^ m));
  }
  return unmasked;
}

Now we’ve got our C code done, we need some glue to compile it and load it from Ruby. I use rake-compiler for this. Your project needs an extconf.rb, in the same directory as the C code:

# ext/faye_websocket/extconf.rb

require 'mkmf'
extension_name = 'faye_websocket'
dir_config(extension_name)
create_makefile(extension_name)

Now let’s make a skeleton gemspec and Rakefile for the project. For a C extension, you ship the source code as part of the gem and it gets compiled on site using the extensions field from the gemspec.

# faye-websocket.gemspec

Gem::Specification.new do |s|
  s.name    = "faye-websocket"
  s.version = "0.4.0"
  s.summary = "WebSockets for Ruby"
  s.author  = "James Coglan"
  
  s.files = Dir.glob("ext/**/*.{c,rb}") +
            Dir.glob("lib/**/*.rb")
  
  s.extensions << "ext/faye_websocket/extconf.rb"
  
  s.add_development_dependency "rake-compiler"
end
# Rakefile

require 'rake/extensiontask'
spec = Gem::Specification.load('faye-websocket.gemspec')
Rake::ExtensionTask.new('faye_websocket', spec)

So the project looks like this at this point:

ext/
    faye_websocket/
        extconf.rb
        faye_websocket.c
faye-websocket.gemspec
Rakefile

This is now enough to run rake compile, wherein rake-compiler works its magic. This will create a Makefile, and some *.o and *.so files. You should not check these into git, or ship them as part of the gem – these compilation artifacts are created on install.

$ rake compile
mkdir -p lib
mkdir -p tmp/x86_64-linux/faye_websocket/1.9.3
cd tmp/x86_64-linux/faye_websocket/1.9.3
/home/james/.rbenv/versions/1.9.3-p194/bin/ruby -I. ../../../../ext/faye_websocket/extconf.rb
creating Makefile
cd -
cd tmp/x86_64-linux/faye_websocket/1.9.3
make
compiling ../../../../ext/faye_websocket/faye_websocket.c
linking shared-object faye_websocket.so
cd -
install -c tmp/x86_64-linux/faye_websocket/1.9.3/faye_websocket.so lib/faye_websocket.so

The file lib/faye_websocket.so is directly loadable from Ruby – let’s try it out:

$ irb -r ./lib/faye_websocket
>> Faye::WebSocket.mask [1,2,3,4], [5,6,7,8]
=> [4, 4, 4, 12]

So the final part of the process is to load this file from our main library code, for example:

# lib/faye/websocket.rb

require File.expand_path('../../faye_websocket', __FILE__)

module Faye
  module WebSocket
    # all your Ruby logic
  end
end

And that’s all there is to it. Just a few points to remember:

  • By convention, rake-compiler expects extensions to be in ext/
  • Make sure the C source and extconf.rb are included in the gemspec
  • Don’t put compilation output in the gem or in source control
  • Remember to recompile between editing C code and running tests

In the next article I’ll show you how to add JRuby support to your native gem.