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
andextconf.rb
files only. No need forMakefile
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!
Pingback: How to write C extensions easily in Ruby | Muriel's Tech Blog
Pingback: The Ruby C API – Basics | Muriel's Tech Blog