Stuck? Need startup advice?

Wrapping up a C library for Ruby. It's actually pretty easy!

Fb613ae74d247c05eba250f575e2c9b0?size=150
by Michiel Sikkes on June 03, 2013 with comments

I am in the progress of writing a Ruby wrapper around the PolarSSL library, which is written in C. I started working on this about 2 weeks ago and tried and discovered the magical world of wrapping. There are all kinds of neat alternatives, which I might write about later. This article tells you how you started wrapping a C library using the "standard" Ruby way. Don't be scared, it's actually fairly easy to do. And I'll give you some pointers and concepts for free!

The basics

What I discovered is that in the literature around, wrapping a C library in Ruby isn't actually called "wrapping". It's called "Extending Ruby with C". This may sound a bit odd because you are making the original library more awesome. It does actually makes sense because the thing you actually do is write some C code that adds modules, classes and methods to Ruby. Therefore, you are extending Ruby.

Let's get down to business. These are the things I did to create my first prototype:

  • Set up an extconf.rb file.
  • Write the desired Ruby code in a test.
  • Write the wrapper code.

Set up extconf.rb

Your wrapper will consist of one or more C files that include your original library and that defines the modules, classes and methods you are going make available to Ruby. These files eventually need to be compiled into a Ruby loadable module, so you can use it in your actual Ruby code via the require statement.

You will create an extconf.rb file that holds the information for Ruby where to find the original libraries files and how to call your wrapper. This is mine for PolarSSL:

require 'mkmf'

LIBDIR      = RbConfig::CONFIG['libdir']
INCLUDEDIR  = RbConfig::CONFIG['includedir']

HEADER_DIRS = [INCLUDEDIR]

LIB_DIRS = [LIBDIR]

dir_config('polarssl', HEADER_DIRS, LIB_DIRS)

unless find_header('polarssl/entropy.h')
  abort "libpolarssl is missing. please install libpolarssl"
end

unless find_library('polarssl', 'entropy_init')
  abort "libpolarssl is missing. please install libpolarssl"
end

create_makefile('polarssl/polarssl')

The file defines where Ruby can find the original library using the dir_config() method. Then it runs a few checks to determine of the library is actually installed with find_header() and find_library(). Finally, it will generate the Makefile for your project using the create_makefile() method

Run

ruby extconf.rb

And the necessary files will be generated!

Write your desired Ruby code

Now, let's define a small piece of our desired result code!

PolarSSL is a library that (amongst other things) you can use to set up secured connections. One of the things you do when creating an SSL connection os to set up an entropy_context and initialize it. This code looks like:

#include <polarssl/entropy.h>

int main()
{
  entropy_context entropy;
    entropy_init(&entropy);
}

entropy_context is the type definition for a C struct. Then the entropy_init(&entropy) sets up a few things for that entropy and takes a pointer to the entropy as only argument.

When we write this down in Ruby it should look like this:

require 'polarssl'

entropy = PolarSSL::EntropyContext.new

Thus, our Ruby extension wrapper should provide the PolarSSL module, an EntropyContext class and the new method should initialize the entropy_context struct and call the entropy_init() function on it. Here's is how we go about it.

Write the wrapper code.

First we need to create the entry point C files that will calls all the functions for setting up our modules and classes. Here is the header file in C:

#ifndef RUBY_POLARSSL
#define RUBY_POLARSSL

#include <ruby.h>;
#include <polarssl/entropy.h>;

#include <entropy_context.h>;

extern VALUE mPolarSSL;

#endif

We include ruby.h so we get access to the functions that will allow us to add modules, classes, methods to Ruby and that provides all kinds of functions to convert C datatypes into Ruby classes and visa-versa.

Then we include polarssl/entropy.h so we get access to the entropy_context and entropy_init() functions from PolarSSL.

Next, we include entropy_context.h, which we are going to make in a second.

Finally, we prepare a extern VALUE-type variable that will use to pass our PolarSSL module to Ruby. Hold your socks, everything will get clear in the following actual C code.

Now. Let's write the C code for the implementation file:

#include <polarssl.h>

VALUE mPolarSSL;

void Init_polarssl()
{
  mPolarSSL = rb_define_module("PolarSSL");

  Init_entropy_context();
}

This is fairly simple. Init_polarssl() is called when your library is loaded. This function does two things. First, it will define a module named PolarSSL for Ruby and stores it into the mPolarSSL variable, the same one we just defined in the header file.

Then it will call the Init_entropy_context() function that we will write next. In this function, the PolarSSL::EntropyContext class will be created. Let's continue with this file.

We will need a header file and an implementation file. Here's the header file:

#ifndef RUBY_ENTROPY_CONTEXT
#define RUBY_ENTROPY_CONTEXT

#include <polarssl.h>

void Init_entropy_context();
static VALUE entropy_context_allocate();
static VALUE entropy_context_initialize();

#endif

And here's the corresponding implementation file:

#include <entropy_context.h>

void Init_entropy_context()
{
  VALUE cEntropyContext = rb_define_class_under(mPolarSSL, "EntropyContext", rb_cObject);

  rb_define_alloc_func(cEntropyContext, entropy_context_allocate);
  rb_define_method(cEntropyContext, "initialize", entropy_context_initialize, 0);
}

static VALUE entropy_context_allocate(VALUE klass)
{
  entropy_context *entropy = malloc(sizeof(entropy_context));

  return Data_Wrap_Struct(klass, NULL, NULL, entropy);
}

static VALUE entropy_context_initialize(VALUE self)
{
  entropy_context *entropy;

    Data_Get_Struct(self, entropy_context, entropy);
    entropy_init(entropy);

    return self;
}

That's it! Before I explain a few parts in what this code does, this is the moment where you can run make and execute the test code you've written:

require_relative './polarssl'

entropy = PolarSSL::EntropyContext.new
p entropy

Back to that C implementation file. The first line of the Init_entropy_context() function tells Ruby that it should make the class EntropyContext available under the module PolarSSL so we can use it like: PolarSSL::EntropyContext in Ruby. This is basically the same as saying the following in Ruby:

module PolarSSL
  class EntropyContext
  end
end

Next, it defines which C-function to use as allocation function. This function will be used internally to allocate enough memory to hold an entropy_context struct in memory. You'll see that the entropy_context_allocate() function allocates a piece of memory using malloc() and returns Data_Wrap_Struct(klass, NULL, NULL, entropy).

This last method is where the magic happens. This function will store the pointer entropy inside a newly created Ruby object with class klass so at a later time, when the user is going to call methods on our object, we can restore the pointer again and use it.

This is precisely what we do in our second function in this file entropy_context_initialize(), which we mapped on the initialize Ruby method. It is standard Ruby protocol to call the initialize method after allocating the object itself.

Thus, when we use

PolarSSL::EntropyContext.new

in our Ruby code, this will first allocate our object, and then will call initialize on it, so our entropy_context_initialize() function in C will be called.

This function returns us to our usage of the PolarSSL C library. The Data_Get_Struct function fetches our original entropy_context pointer from the Ruby object self so we can pass it into the entropy_init() function from C.

That's it!

Yes! There is a lot more to tell and a few other things I learned that I didn't talk about yet, like accessing file pointers from other Ruby objects, or yielding to a block. I will tell you those later.

Thanks to Aaron Patterson for his excellent write up using a different library: WRITING RUBY C EXTENSIONS: PART 2. You'll see this post has about the same structure.

Also, this PDF by Pragmatic Programmers has a tutorial-style explanation, which helped me a lot.

comments powered by Disqus