Phil Gengler


on software, development, and anything else

Wrapping a C API in Ruby

Here's the situation I found myself in earlier today: write a program that makes use of an API written in C, but do a bunch of additional setup & processing. The thought of writing all the code to open/read a file, connect to the database, and all that other stuff didn't really appeal to me. Since I only needed a couple of functions out of the API, this seemed like a great case for wrapping the API and writing the rest of the program in Ruby.

There's a lot out there about how to do this, but I didn't find any one place that put it all together neatly. So I decided to write up how I went about it in the hope that this may be useful to someone.

For this explanation let's say we want to wrap the (fictional) C library libfoo, which has these functions/types that we're interested in:

typedef struct foo_tag* foo_handle;
foo_handle foo_create(void);
void foo_load(foo_handle, const char* filename);
void foo_destroy(foo_handle);
const char *foo_process(foo_handle, const char* name);

The first step is to create a file called extconf.rb. This uses the "mkmf" gem and specifies the basic setup for our wrapper, like the name of it and where to find the headers and libraries we need.

# Loads mkmf which is used to make makefiles for Ruby extensions
require 'mkmf'

pkgconfig = with_config('pkg-config') || 'pkg-config'
pkgconfig = find_executable(pkgconfig) or abort "Couldn't find your pkg-config binary"

$LDFLAGS << ' ' + `#{pkgconfig} --libs-only-L foo`.chomp
$CFLAGS << ' ' + `#{pkgconfig} --cflags foo`.chomp

find_header('foo.h') or abort "Missing foo.h"
find_library('foo', 'foo_create') or abort "Missing foo library"

# Give it a name
extension_name = 'ruby_foo'

# The destination
dir_config(extension_name)

# Do the work
create_makefile(extension_name)

If we run ruby extconf.rb we should see output like this:

checking for foo.h... yes
checkout for foo_init... -lfoo
creating Makefile

We'll also see it created a new file called "Makefile". If we run "make" right now nothing will happen, since we haven't created any source files yet.

Because we used "ruby_foo" as the name for our extension in extconf.rb, let's create a new file called ruby_foo.c. This is where we'll define the interface between Ruby and the C library.

At its simplest, our file should look like this:

#include "ruby.h"

#include "foo.h"

VALUE our_module;
VALUE our_class;

void Init_ruby_foo()
{
    our_module = rb_define_module("OurModule");
    our_class = rb_define_class_under(our_module, "Foo", rb_cObject);
}

This defines a Ruby module called OurModule, containing a single Ruby class called Foo (we tell Ruby that this class inherits from Object). What we have so far is equivalent to:

module OurModule
   class Foo
   end
end

It's important that you name your function properly. Ruby expects the function to be called "Init_", where is the same name specified in extconf.rb.

So far, of course, our code doesn't do anything yet. Let's start adding some library calls.

Since our library has a foo_init function that returns a handle that we need to use for later calls, we'll need to make sure that we save this inside the Ruby object. To do this, we add an "allocator" function that calls foo_create and uses the Data_Wrap_Struct macro to

VALUE ruby_foo_new(VALUE klass)
{
   foo_handle handle = foo_create();
   return Data_Wrap_Struct(klass, 0, foo_destroy, handle);
}

void Init_ruby_foo()
{
    // ...
    rb_define_alloc_func(our_class, ruby_foo_new);
}

The middle two arguments of Data_Wrap_Struct are a "mark" function and a "free" function. The mark function is useful if the object you're creating contains one or more Ruby objects; since we're not in this example, we can pass 0 here. The "free" function is called when the Ruby garbage collector tears down an instance of our class. It's useful for freeing any memory that we allocated; in this case, we just use the library's foo_destroy function, though if we had more complicated logic we could create our own function and specify that here. Both the "mark" and "free" functions receive the pointer we're wrapping (the fourth argument to Data_Wrap_Struct).

Of course, at this point, our class only creates a handle to the library but doesn't finish the additional setup it needs (the foo_load function). This requires us to specify a 'filename' parameter, which we can do from Ruby. Let's define an initialize method for our new class:

VALUE ruby_foo_initialize(VALUE self, VALUE filename)
{
    // make sure 'filename' is a string; raises an exception if it's not
    Check_Type(filename, T_STRING); 

    // convert the Ruby string to a C-style string
    const char* filename_cstr = StringValueCStr(filename);

    // get the foo_handle we initialized in 'new'
    foo_handle handle;
    Data_Get_Struct(self, struct foo_tag, handle);

    foo_load(handle, filename);

    return self;
}

void Init_ruby_foo()
{
    // ...
    rb_define_method(our_class, "initialize", ruby_foo_initialize, 1);
} 

The last argument to rb_define_method is the number of parameters the method requires; in this case, we only need the filename.

Now we can create an instance of our new class like this:

require_relative './ruby_foo'
obj = OurModule::OurClass.new("/tmp/ourfile")

Lastly, let's create a new method on our class that uses the foo_process function from our library.

void ruby_foo_process(VALUE self, VALUE name)
{
    // make sure 'name' is a string, and get a C-style string from the Ruby string
    Check_Type(name, T_STRING);
    const char *name_str = StringValueCStr(name);

    // get our library handle
    foo_handle handle;
    Data_Get_Struct(self, struct foo_tag, handle);

    // call the library function
    const char *value = foo_process(handle, name);

    return rb_str_new2(value);
}

void Init_ruby_foo()
{
    // ...
    rb_define_method(our_class, "process", ruby_foo_process, 1);
}

This is very similar to what we did for the initialize method. The main differences are that we call foo_process instead of foo_load, and instead of returning self we use the rb_str_new2 function to convert the C-style string we get from the library into a Ruby string and return that from our method.

Now we can write our Ruby code to use our library:

require_relative './ruby_foo'

obj = OurModule::OurClass.new('/tmp/ourfile')
puts obj.process('bar')

For comparison, the equivalent C code would look like:

#include <stdio.h>
#include "foo.h"

int main()
{
    foo_handle handle = foo_create();
    foo_load(handle, "/tmp/ourfile");
    const char *value = foo_process("bar");
    printf("%s\n", value);
    return 0;
}

Now we have a wrapped C API! I left out error checking/exceptions in the interest of simplicity. In another post, I'll explore this, along with how you can use more of Ruby's built-in types (arrays, hashes, etc.) from inside of C code.