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.
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.
/* * 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*).
/* * 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
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')
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); }
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.