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.