Packaging Ruby C extensions in nice gems

In the first part of this series on Ruby and C, we’ve seen how easy it is to write a C extension and use it in Ruby environment.

However, in real world problems, a Ruby library will have several C extensions, bundled together with other Ruby libraries.
Packaging and distributing Ruby gems containing C extensions is fairly easy, and this post explains how.

Here we will build a complete Ruby gem (named mygem), containing an executable with several Ruby libraries and C extensions.

You can get the full source code of this tutorial here.

Our example Ruby gem structure

By convention, Ruby libraries are in a lib/ folder, and C extensions are in a ext/ folder.

Here is the structure of our Ruby gem, respecting conventions:

.
+- bin/
|  +- exec.rb
+- lib/
|  +- mygem/
|     +- myfirstrubylib.rb
|     +- mysecondrubylib.rb
+- ext/
   +- mygem/
      +- myfirstcext.c
      +- extconf.rb
      +- mycsubmodule/
         +- mysecondcext.c
         +- extconf.rb

For our example, running our executable calls the 2 Ruby libraries, that respectively call their corresponding C extension. Each C extension defines 1 method displaying a little message.

Please note the 2 extconf.rb files, responsible for compiling their respective C extension.

The code

Each C extension defines a function displaying a little message:

#include <ruby.h>

static VALUE object_first_cext_call(VALUE rb_self) {
  printf("First C extension call\n");
}

void Init_myfirstcext() {
  // Define method Object#first_cext_call, corresponding to our C function object_first_cext_call; it has 0 argument.
  rb_define_method(rb_cObject, "first_cext_call", object_first_cext_call, 0);
}
#include <ruby.h>

static VALUE object_second_cext_call(VALUE rb_self) {
  printf("Second C extension call\n");
}

void Init_mysecondcext() {
  // Define method Object#second_cext_call, corresponding to our C function object_second_cext_call; it has 0 argument.
  rb_define_method(rb_cObject, "second_cext_call", object_second_cext_call, 0);
}

Do not forget the corresponding extconf.rb files:

require 'mkmf'
create_makefile('myfirstcext')
require 'mkmf'
create_makefile('mysecondcext')

Each Ruby library calls its corresponding C extension:

require "mygem/myfirstcext"

def first_ruby_call
  puts 'First Ruby call'
  first_cext_call
end
require "mygem/mycsubmodule/mysecondcext"

def second_ruby_call
  puts 'Second Ruby call'
  second_cext_call
end

Finally, our executable calls our 2 libraries:

#!/bin/env ruby
require 'mygem/myfirstrubylib'
require 'mygem/mysecondrubylib'

first_ruby_call
second_ruby_call

Test mygem locally

First, we compile our 2 C extensions:

ext/mygem> ruby extconf.rb
creating Makefile

ext/mygem> make
compiling myfirstcext.c
linking shared-object myfirstcext.so
ext/mygem/mycsubmodule> ruby extconf.rb
creating Makefile

ext/mygem/mycsubmodule> make
compiling mysecondcext.c
linking shared-object mysecondcext.so

Then we run our executable. Do not forget to include both lib/ and ext/ in your Ruby path (using -I).

>ruby -Ilib -Iext bin/exec.rb
First Ruby call
First C extension call
Second Ruby call
Second C extension call

So now that you have a running Ruby application, let’s package it into a nice gem.

Packaging Ruby C extensions

As C extensions need to be compiled to be executed, a Ruby gem including C extensions has 2 packaging possibilities: either including the compiled C extension, or including the C extension’s source files.

Including the compiled C extension means that the resulting Ruby gem will be platform dependent. In other words, you’ll need to package several gems: 1 for each platform you want it to run on.
Including the C extension source produces a platform independent Ruby gem (cool), but every time a user will install it, the Ruby gem installation process will try to compile it (not cool). This means that your users must have a C development environment on their host, and this can be a real pain, especially on Windows platforms.

Platform-dependent Ruby gem

When shipping the compiled C extension, you have to take care of 3 things when writing your GemSpec:

  • Add your compiled C extension files (exactly the same way you do for normal Ruby files)
  • Add ext directory to the required paths
  • Set the platform as current

Here is the gemspec file packaging our example gem in a platform-dependent way:

Gem::Specification.new do |spec|
  spec.name = 'mygem'
  spec.version = '0.1'
  spec.summary = 'Summary'
  spec.description = 'Description'
  spec.email = 'myemail@provider.com'
  spec.homepage = 'http://myapp.com'
  spec.author = 'me'
  spec.bindir = 'bin'
  spec.executable = 'exec.rb'
  spec.files = Dir['lib/**/*.rb'] + Dir['bin/*'] + Dir['ext/**/*.so'] + Dir['ext/**/*.dll']
  spec.platform = Gem::Platform::CURRENT
  spec.require_paths = [ 'lib', 'ext' ]
end

And now we can generate the gem:

> gem build mygem.platformdependent.gemspec.rb 
  Successfully built RubyGem
  Name: mygem
  Version: 0.1
  File: mygem-0.1-x86-cygwin.gem

You will notice that your gem has been packaged for your current platform only (here: x86-cygwin). If you want it on another platform, you will have to first compile it on the other platform, and then build a new gem using this gemspec on the same platform.

Now we can try to install it:

> gem install mygem-0.1-x86-cygwin.gem
Successfully installed mygem-0.1-x86-cygwin
1 gem installed
Installing ri documentation for mygem-0.1-x86-cygwin...
Installing RDoc documentation for mygem-0.1-x86-cygwin...

And run it:

> exec.rb
First Ruby call
First C extension call
Second Ruby call
Second C extension call

Everything works like a charm, but on your platform only.

Platform-independent Ruby gem

When shipping the C extension source files, you have to take care of 4 things when writing your GemSpec:

  • Add your C extension source files only: that is the .c and extconf.rb files only. No need for Makefile and compiled files.
  • Add ext directory to the required paths
  • Keep the platform as Ruby (independent)
  • Register the C extensions you are including: this will tell RubyGems that there is a compilation step upon installation. This is done by giving all the paths to extconf.rb files.

Here is the gemspec file packaging our example gem in a platform-independent way:

Gem::Specification.new do |spec|
  spec.name = 'mygem'
  spec.version = '0.1'
  spec.summary = 'Summary'
  spec.description = 'Description'
  spec.email = 'myemail@provider.com'
  spec.homepage = 'http://myapp.com'
  spec.author = 'me'
  spec.bindir = 'bin'
  spec.executable = 'exec.rb'
  spec.files = Dir['lib/**/*.rb'] + Dir['bin/*'] + Dir['ext/**/*.c'] + Dir['ext/**/extconf.rb']
  spec.platform = Gem::Platform::RUBY # This is the default
  spec.require_paths = [ 'lib', 'ext' ]
  spec.extensions = Dir['ext/**/extconf.rb']
end

And now we can generate the gem:

> gem build mygem.platformindependent.gemspec.rb 
  Successfully built RubyGem
  Name: mygem
  Version: 0.1
  File: mygem-0.1.gem

You will notice that your gem has been packaged for no specific platform, and will be installable on all platforms.

Now we can try to install it (first uninstall the previous one if needed using gem uninstall mygem):

> gem install mygem-0.1.gem 
Building native extensions.  This could take a while...
Successfully installed mygem-0.1
1 gem installed
Installing ri documentation for mygem-0.1...
Installing RDoc documentation for mygem-0.1...

Here you will notice that RubyGems has compiled the C extensions as the first installation step. This you you can see with the line Building native extensions. This could take a while...: it runs all the extconf.rb scripts in the background. Therefore, platforms that won’t have a C development environment won’t be able to install your gem.

Now we can run it:

> exec.rb
First Ruby call
First C extension call
Second Ruby call
Second C extension call

Everything works like a charm once again, using a platform-independent gem this time.

Next step

We have seen how to write and package C extensions.
Next step goes into learning the C API!.
Don’t be afraid, it’s easy.

Stay tuned!

Share

About Muriel Salvan

I am a freelance project manager and polyglot developer, expert in Ruby and Rails. I created X-Aeon Solutions and rivierarb Ruby meetups. I also give trainings and conferences on technical topics. My core development principles: Plugins-oriented architectures, simple components, Open Source power, clever automation, constant technology watch, quality and optimized code. My experience includes big and small companies. I embrace agile methodologies and test driven development, without giving up on planning and risks containment methods as well. I love Open Source and became a big advocate.
C, Howto, New gem, Ruby , , , , , , , ,

3 comments


  1. Pingback: How to write C extensions easily in Ruby | Muriel's Tech Blog

  2. Pingback: The Ruby C API – Basics | Muriel's Tech Blog

  3. How do you view a list of the comments?

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <strike> <strong>