Packaging a Ruby C Extension as a Gem

Recently, I created my first ruby gem. It also happened to be a C extension. While there is a fairly good deal of information available on the web on how to package a basic gem, there isn't very much information about how to package a gem that includes a C extension. Bundler has a nice feature that creates a project skeleton for a gem, so it is easiest to start there:
bundle gem hello_world
That command should produce the following output:
      create  hello_world/Gemfile
      create  hello_world/Rakefile
      create  hello_world/.gitignore
      create  hello_world/hello_world.gemspec
      create  hello_world/lib/hello_world.rb
      create  hello_world/lib/hello_world/version.rb
Initializating git repo in /home/matt/hello_world
Now we need to create a directory where the extension will live:
mkdir -p hello_world/ext/hello_world
Inside this directory, we need to create our C extension, in our case, hello_world.c, and also an extconf.rb that ruby uses to create a makefile. The first file, hello_world/ext/hello_world/hello_world.c:
#include "ruby.h"
#include <stdio.h>

static VALUE method_hello_world(VALUE self)
{
        printf("Hello World!\n");
        return Qnil;
}

VALUE HelloWorldModule;
VALUE HelloWorldClass;

void Init_hello_world()
{
        HelloWorldModule = rb_define_module("HelloWorld");
        HelloWorldClass = rb_define_class_under(HelloWorldModule, "HelloWorld", rb_cObject);
        rb_define_method(HelloWorldClass, "hello_world", method_hello_world, 0);
}
This should be a fairly straightforward C extension, it's basically just a module with a class and a method that lives inside that class that prints "Hello World!". The next file, hello_world/ext/hello_world/extconf.rb:
require 'mkmf'

create_makefile("hello_world")
Next, we need to load the extension from the hello_world.rb file located in the lib directory. Basically, all we need to do is add a require statement: hello_world/lib/hello_world.rb:
require 'hello_world/hello_world'

module HelloWorld
  # Your code goes here...
end
The require statement seems a little redundant, but the .so file that is built actually gets stored in hello_world/lib/hello_world. The next step is to modify the Rakefile in the root directory. Note that in order to build a gem that includes a C extension, we are going to need the rake extensiontask gem, so if you don't have that, now would be a good time to install it. Modify your hello_world/Rakefile as follows:
require 'rubygems'
require 'rake'
require 'rake/extensiontask'
require 'bundler'

Rake::ExtensionTask.new("hello_world") do |extension|
  extension.lib_dir = "lib/hello_world"
end

task :chmod do
  File.chmod(0775, 'lib/hello_world/hello_world.so')
end
task :build => [:clean, :compile, :chmod]

Bundler::GemHelper.install_tasks
Next, we need to modify the hello_world.gemspec. Bundler creates a nice skeleton for us, the only thing we need to do is fill in the information for author, email, homepage, summary, and description, and add one line that points to the extconf.rb file, the s.extensions line. Modify the hello_world/hello_world.gemspec as follows, replacing your info with mine:
# -*- encoding: utf-8 -*-
$:.push File.expand_path("../lib", __FILE__)
require "hello_world/version"

Gem::Specification.new do |s|
  s.name        = "hello_world"
  s.version     = HelloWorld::VERSION
  s.platform    = Gem::Platform::RUBY
  s.authors     = ["Matthew Downey"]
  s.email       = ["mattddowney@gmail.NOSPAM.com"]
  s.homepage    = "http://www.writehack.com"
  s.summary     = %q{Hello World!}
  s.description = %q{Gem that prints Hello World!}

  s.rubyforge_project = "hello_world"

  s.files         = `git ls-files`.split("\n")
  s.test_files    = `git ls-files -- {test,spec,features}/*`.split("\n")
  s.executables   = `git ls-files -- bin/*`.split("\n").map{ |f| File.basename(f) }
  s.require_paths = ["lib"]
  s.extensions = ["ext/hello_world/extconf.rb"]
end
Next, build the extension, which should compile the .so file for us:
rake build
If you notice, the s.files line in the gemspec gets the files for the project from a git repository. It's probably a good idea to create a .gitignore file, but we won't cover that here. Just create an empty git repository, add everything, and make your initial commit:
git init
git add .
git commit -m 'initial commit'
Now, all thats left to do is install and test the gem!
rake install
If everything went well, we should no be able to fire up irb and test our gem:
ruby-1.9.2-p136 :001 > require 'rubygems'
 => true 
ruby-1.9.2-p136 :002 > require 'hello_world'
 => true 
ruby-1.9.2-p136 :003 > hello = HelloWorld::HelloWorld.new
 => #<HelloWorld::HelloWorld:0x000000027d7c40> 
ruby-1.9.2-p136 :004 > hello.hello_world
Hello World!
 => nil 
ruby-1.9.2-p136 :005 > 
Hopefully this helps someone that is trying to package a C extension as a gem. I wrestled with this for hours.
blog comments powered by Disqus  -  Home