«
5 minute read

HOWTO: Ruby C Extension with a static Library

How to wrap a static C library with Ruby

Several months ago I was tasked with implementing an appliance into our Ruby on Rails infrastructure. I could have either chosen to do it via cURL calls and pure C, or wrap the static C libraries the vendor supplied with ruby and build it into a gem that could easily be installed in any one of our apps. I chose to do the latter as it would be portable as well as more easily includable into other apps that we have here at ID.me.

Problem

In the beginning stages of this project, I was unable to locate exactly how to handle wrapping a static C library. However, I was able to find many blog posts on how to create a C extension.

Creating a simple static library

Hello_user.c is the class file we will be using to compile our static library for this example. As you can see, it defines a function called greeting that accepts a char *. Within the function, it does a simple strcat to build out a string it will return to the originating function.

static_libs/hello_user.c

/* 
 * hello_user.c
 */

#include <stdio.h>
#include <string.h>
#include <stdlib.h>

/*
 * Function that will be compiled into ./libexample.a
 */

char str[1024];
const char * greeting(char *name) {
  strcat(str, "Hello ");
  strcat(str, name);
  strcat(str, "! How are you?");

  return str;
}

prog.c is the example C program that will be compiled against the static library created by the previous code block. Within int main(), a variable of type char * is set to string "Zac". It then passes name to the greeting interface instantiated by char * greeting(char*).

static_libs/prog.c

/*
 * prog.c 
 */

#include <stdio.h>

char * greeting(char*);

int main() {
  char * name = "Zac";
  printf("%s\n",greeting(name));

  return 0;
}

In order to create the static library, first hello_user.c will need to be compiled into its object file (e.g. hello_user.o).

To compile hello_user.c into hello_user.o, from

wrap_c_example/static_libs/ $ cc -Wall -c hello_user.c
wrap_c_example/static_libs/ $ ls
hello_user.c hello_user.o 

Now that we have the class file compiled into an object file, we then need to pack it into a static library (e.g. lib example.a)

wrap_c_example/static_libs/ $ ar rcs libexample.a hello_user.o
wrap_c_example/static_libs/ $ ls
libexample.a

Creating the C extension

extconf.rb setup

First step in creating the C extension is to make a directory under the gem root directory that will hold all of the ruby and C code for the extension.

wrap_c_example/ $ tree ext/
ext/
└── wrap_c_example
    ├── extconf.rb
    ├── lib
    │   └── libexample.a
    └── wrap_c_example.c

ext/lib/ holds all 3rd party libraries, in this case our libexample.a that was created in the previous step. The main difference in a simple C extension and one that handles wrapping a static library is the setup of extconf.rb. As you can see below, the main things that need to be setup are the LIB_DIRS variable, as well as the libs array / appending each object in the libs array to the $LOCAL_LIBS array.

require "mkmf"

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

HEADER_DIRS = [INCLUDEDIR]

# setup constant that is equal to that of the file path that holds that static libraries that will need to be compiled against
LIB_DIRS = [LIBDIR, File.expand_path(File.join(File.dirname(__FILE__), "lib"))]

# array of all libraries that the C extension should be compiled against
libs = ['-lexample']

dir_config('wrap_c_example', HEADER_DIRS, LIB_DIRS)

# iterate though the libs array, and append them to the $LOCAL_LIBS array used for the makefile creation
libs.each do |lib|
  $LOCAL_LIBS << "#{lib} "
end

create_makefile('wrap_c_example_c')

C extension

As you can see in the following source code for ext/wrap_c_example/wrap_c_example.c the setup for the greeting(char *) function call matches that of static_libs/prog.c

#include <stdlib.h>
#include <ruby.h>

static VALUE rb_mWrapCExample;
static VALUE rb_cGreeting;

static VALUE
greeting_hello(VALUE self) {
  /*
   * Setup for function located in ../../static_libs/hello_user.c
   */
  char *  greeting(char *);

  /*
   * return a string VALUE from a char * that ruby can handle and assign to variables
   */
  return rb_str_new2(greeting(RSTRING_PTR(rb_iv_get(self, "@name"))));
}

static VALUE
greeting_init(VALUE self, VALUE name) {
  rb_iv_set(self, "@name", name);

  return self;
}

void
Init_wrap_c_example_c() {
  rb_mWrapCExample = rb_define_module("WrapCExample");
  rb_cGreeting     = rb_define_class_under(rb_mWrapCExample, "Greeting", rb_cObject);

  rb_define_method(rb_cGreeting, "initialize", greeting_init, 1);
  rb_define_method(rb_cGreeting, "hello", greeting_hello, 0);
}

Conclusion

At the end of this, we now have a ruby gem that extends a static library. As you can see, it is very similar to writing a standard C extension, minus the differences in ext/wrap_c_example/extconf.rb and a standard extconf.rb.

Sources:

Github Repository

Basic C Extension Blog

Share Comment on Twitter