C Extensions in Ruby

Tooo Slooow

I recently wrote some analysis code for MD simulations my group is running, and decided to give it a shot in Ruby, instead of C++ (which a couple of people in my group use) or FORTRAN (which everyone else uses).

The entire experience was pretty nice, other than some meandering about how I wanted the class relationships to be. The ability to easily test things, poking around in the irb interactive shell, and extremely quick tweaks were the nice things. The downside is that it runs slow. Now I know about the dangers of needless speed optimizations, but this code is the core analysis tool for this type of simulation, and forms the base of almost any other calculation done later.

To try to deal with this, I started looking into extensions to ruby, but most of the examples I saw dealt with strings, and only one even mentioned arrays at all. Starting off, I had two goals:

  • Speedup in floating point code (generally slower all around than integer, and what my code uses)
  • How to pass numbers back and forth (single parameters and in an array).

In the future I will hopefully address working with ruby classes from C, but I haven’t gotten that far yet.

Extension basics

Ok, first things first. Getting something to work. Programming Ruby has an example that I started from. There is some information there, but not a ton. You start with two files, the .c code, and a extconf.rb file to produce a makefile.

First, the c file. This one is my_test.c

#include "ruby.h"
static VALUE t_init(VALUE self){
    return self;
}
static VALUE t_two_cubed(VALUE self) {
    int a = 2*2*2;
    return INT2NUM(a);
}
static VALUE t_sum(VALUE self, VALUE arr) {
    int i;
    int length;
    double num;
    double total;   

    total = 0.0;
    for (i=0; i < RARRAY(arr)->len; i++) {
        num = NUM2DBL(RARRAY(arr)->ptr[i]);
        total += 2 * num;
    }
    return rb_float_new(total);
}
VALUE cTest;
void Init_my_test() {
    cTest = rb_define_class("MyTest", rb_cObject);
    rb_define_method(cTest, "initialize", t_init, 0);
    rb_define_method(cTest, "sum", t_sum, 1);
    rb_define_method(cTest, "two_cubed", t_two_cubed, 0);
}

The t_init and Init_ parts will be there in every extension. This is how everything gets set up by ruby. Ruby calls the Init_my_test to initialize the extension (I’m not sure if this is called when the require is run, or the object is created). The Init_my_test function defines the class name the extension will use, in this case it is MyTest. This Init_my_test function also “wires up” the other functions so that they are available from ruby. For some reason this reminds me of doing code behind methods in ASP.NET. The rb_define_method function is passed (among other things) the name for ruby to use as the method name (the one in quotes) and the c function this method should point to (here they all start with t_). The number that is the last parameter in rb_define_method is the number of other parametrs the function is supposed to take (this part is strange.)

For example, the t_sum method is defined with two parametrs, but one is an internal mechanism thing, and one is an array from ruby. So this function takes one parameter in addition to the self that every function needs to have, so the number in rb_define_method is 1. The t_two_cubed method doesn’t take external parameters (only the required self) so rb_define_method uses 0 to show that. Presumably there is no requirement for the ruby name and the function name to be anywhere close to each other, but why would you do that to yourself?

The c code itself is pretty vanilla. The only thing special in the t_two_cubed function is the call to INT2NUM() that performs the translation from a C data type to a Ruby object. For a complete list of these, look in the ruby.h file.

The t_sum function has two of these calls. One for the input and one for output. This function takes an array, doubles each item, and returns the sum. The extension system understands the basic Ruby objects, such as Array, and Hash (in addition to the numeric and string classes.) This is done by building a struct in C that mimics the particular class. The two used most for Array are RARRAY(arr)->len which gives the length of array arr, and RARRAY(arr)->ptr, which gives a pointer to the array data. Items in the array are accessed by RARRAY(arr)->ptr[0], ptr[1], etc. The code above shows the standard way to iterate over all of the items in an array. The call to NUM2DBL translates the array items into C double precision numbers (floats in Ruby are double precision.) and the rb_float_new wraps the output so that Ruby can interperet it as a float. I’m not sure what the discrepancy in naming is between some of the functions (INT2NUM vs rb_float_new) but this is how they are defined. If you get strange numbers (like 2+2=-1860438) this is the first thing to check.

Makefile Generation

To compile and run this thing you need a very simple extconf.rb file. The one I used has two lines

require 'mkmf'
create_makefile("my_test")

There are many more options. The value in parenthesis controls what the compiled binary will be called, and what goes into require to use the extension. In this case you use require ‘my_test’, or -r my_test.
To compile, it is as simple as

ruby extconf.rb
make

then

imac:~/code/cmoduleruby brian$ irb -r my_test
irb(main):001:0> t = MyTest.new
=> #<MyTest:0x1d167c>
irb(main):002:0> t.two_cubed
=> 8
irb(main):003:0> t.sum([2.3,5.0,10.0])
=> 34.6

Hopefully this makes sense. I’ll post a more numerical example and some benchmarks next.

Advertisements

One Response to C Extensions in Ruby

  1. Shadowfiend says:

    Worth looking at is also Ruby Inline (http://rubyforge.org/projects/rubyinline/), which lets you inline C code within your Ruby code.

Leave a Reply

Fill in your details below or click an icon to log in:

WordPress.com Logo

You are commenting using your WordPress.com account. Log Out / Change )

Twitter picture

You are commenting using your Twitter account. Log Out / Change )

Facebook photo

You are commenting using your Facebook account. Log Out / Change )

Google+ photo

You are commenting using your Google+ account. Log Out / Change )

Connecting to %s

%d bloggers like this: