Channels ▼
RSS

Design

A Build System for Complex Projects: Part 5


Ruby Bindings

A Ruby binding is a dynamic library with a C interface that follows some conventions and uses some special data types and functions from the Ruby C API. The end result is a module that can be consumed by Ruby code.

Here is the C code Bob came up with as a pilot. The "ruby.h" header contains the Ruby C API definitions. The Init_hello_ruby_world() is the entry point that Ruby calls when it loads the binding. This function defines a class called HelloWorld that has two methods called get_hello() and get_world(). The temporary implementation just returns the strings "hello" and "world". The final version will link of course to the C++ "Hello, World!" project and utilize its sophisticated services.


#include "ruby.h"
static VALUE get_hello(VALUE self) 
{ 
    VALUE result = rb_str_new2("hello"); 
    return result; 
} 
static VALUE get_world(VALUE self) 
{
    VALUE result = rb_str_new2("world"); 
    return result; 
} 
VALUE cHelloWorld; 
void Init_hello_ruby_world()
{ 
    cHelloWorld = rb_define_class("HelloWorld", rb_cObject); 
    rb_define_method(cHelloWorld, "get_hello", get_hello, 0);
    rb_define_method(cHelloWorld, "get_world", get_world, 0);
} 

To make an actual Ruby binding out of this source file, Bob created a Ruby configuration file called extconf.rb that contains just two lines


require 'mkmf' 
create_makefile("hello_ruby_world")

Next Bob ran the configuration file through Ruby and Ruby generated a Makefile appropriate for the current platform (Mac OS X):


~/Invisible.Build.System/src/ruby/hello_ruby_world > ruby extconf.rb 
creating Makefile

Here is the Makefile:

SHELL = /bin/sh

#### Start of system configuration section. ####

srcdir = .
topdir = /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib/ruby/1.8/universal-darwin9.0
hdrdir = $(topdir)
VPATH = $(srcdir):$(topdir):$(hdrdir)
prefix = $(DESTDIR)/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr
exec_prefix = $(prefix)
sitedir = $(DESTDIR)/Library/Ruby/Site
rubylibdir = $(libdir)/ruby/$(ruby_version)
docdir = $(datarootdir)/doc/$(PACKAGE)
dvidir = $(docdir)
datarootdir = $(prefix)/share
archdir = $(rubylibdir)/$(arch)
sbindir = $(exec_prefix)/sbin
psdir = $(docdir)
localedir = $(datarootdir)/locale
htmldir = $(docdir)
datadir = $(datarootdir)
includedir = $(prefix)/include
infodir = $(DESTDIR)/usr/share/info
sysconfdir = $(prefix)/etc
mandir = $(DESTDIR)/usr/share/man
libdir = $(exec_prefix)/lib
sharedstatedir = $(prefix)/com
oldincludedir = $(DESTDIR)/usr/include
pdfdir = $(docdir)
sitearchdir = $(sitelibdir)/$(sitearch)
bindir = $(exec_prefix)/bin
localstatedir = $(prefix)/var
sitelibdir = $(sitedir)/$(ruby_version)
libexecdir = $(exec_prefix)/libexec

CC = gcc
LIBRUBY = $(LIBRUBY_SO)
LIBRUBY_A = lib$(RUBY_SO_NAME)-static.a
LIBRUBYARG_SHARED = -l$(RUBY_SO_NAME)
LIBRUBYARG_STATIC = -l$(RUBY_SO_NAME)

RUBY_EXTCONF_H = 
CFLAGS   =  -fno-common -arch ppc -arch i386 -Os -pipe -fno-common 
INCFLAGS = -I. -I$(topdir) -I$(hdrdir) -I$(srcdir)
DEFS     = 
CPPFLAGS =   $(DEFS)
CXXFLAGS = $(CFLAGS) 
DLDFLAGS = -L. -arch ppc -arch i386  
LDSHARED = cc -arch ppc -arch i386 -pipe -bundle -undefined dynamic_lookup
AR = ar
EXEEXT = 

RUBY_INSTALL_NAME = ruby
RUBY_SO_NAME = ruby
arch = universal-darwin9.0
sitearch = universal-darwin9.0
ruby_version = 1.8
ruby = /System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/bin/ruby
RUBY = $(ruby)
RM = rm -f
MAKEDIRS = mkdir -p
INSTALL = /usr/bin/install -c
INSTALL_PROG = $(INSTALL) -m 0755
INSTALL_DATA = $(INSTALL) -m 644
COPY = cp

#### End of system configuration section. ####

preload = 

libpath = . $(libdir)
LIBPATH =  -L. -L$(libdir)
DEFFILE = 

CLEANFILES = mkmf.log
DISTCLEANFILES = 

extout = 
extout_prefix = 
target_prefix = 
LOCAL_LIBS = 
LIBS = $(LIBRUBYARG_SHARED)  -lpthread -ldl -lm -lutils -lhello -lworld
SRCS = hello_ruby_world.c
OBJS = hello_ruby_world.o
TARGET = hello_ruby_world
DLLIB = $(TARGET).bundle
EXTSTATIC = 
STATIC_LIB = 

RUBYCOMMONDIR = $(sitedir)$(target_prefix)
RUBYLIBDIR    = $(sitelibdir)$(target_prefix)
RUBYARCHDIR   = $(sitearchdir)$(target_prefix)

TARGET_SO     = $(DLLIB)
CLEANLIBS     = $(TARGET).bundle $(TARGET).il? $(TARGET).tds $(TARGET).map
CLEANOBJS     = *.o *.a *.s[ol] *.pdb *.exp *.bak

all:		$(DLLIB)
static:		$(STATIC_LIB)

clean:
		@-$(RM) $(CLEANLIBS) $(CLEANOBJS) $(CLEANFILES)

distclean:	clean
		@-$(RM) Makefile $(RUBY_EXTCONF_H) conftest.* mkmf.log
		@-$(RM) core ruby$(EXEEXT) *~ $(DISTCLEANFILES)

realclean:	distclean
install: install-so install-rb

install-so: $(RUBYARCHDIR)
install-so: $(RUBYARCHDIR)/$(DLLIB)
$(RUBYARCHDIR)/$(DLLIB): $(DLLIB)
	$(INSTALL_PROG) $(DLLIB) $(RUBYARCHDIR)
install-rb: pre-install-rb install-rb-default
install-rb-default: pre-install-rb-default
pre-install-rb: Makefile
pre-install-rb-default: Makefile
$(RUBYARCHDIR):
	$(MAKEDIRS) $@

site-install: site-install-so site-install-rb
site-install-so: install-so
site-install-rb: install-rb

.SUFFIXES: .c .m .cc .cxx .cpp .C .o

.cc.o:
	$(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) -c $<

.cxx.o:
	$(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) -c $<

.cpp.o:
	$(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) -c $<

.C.o:
	$(CXX) $(INCFLAGS) $(CPPFLAGS) $(CXXFLAGS) -c $<

.c.o:
	$(CC) $(INCFLAGS) $(CPPFLAGS) $(CFLAGS) -c $<

$(DLLIB): $(OBJS)
	@-$(RM) $@
	$(LDSHARED) -o $@ $(OBJS) $(LIBPATH) $(DLDFLAGS) $(LOCAL_LIBS) $(LIBS)

$(OBJS): ruby.h defines.h

With a nice Makefile under his belt, Bob proceeded to build the hello_ruby_world binding:


~/Invisible.Build.System/src/ruby/hello_ruby_world > make
cc -arch ppc -arch i386 -pipe -bundle -undefined dynamic_lookup -o hello_ruby_world.bundle hello_ruby_world.o -L. -L/System/Library/Frameworks/Ruby.framework/Versions/1.8/usr/lib -L. -arch ppc -arch i386    -lruby  -lpthread -ldl -lm 

The result was a hello_ruby_world.bundle file, which is the binding itself (a .dll on Windows, and .so on Linux). Now, Bob invited Issac to examine the new toy. Issac, a big Ruby fan, immediately wrote a ruby test program to make sure the binding is indeed usable from Ruby. The program starts with two require statements (the equivalent of import in Python). Note that the first one requires the new binding hello_ruby_world. Next, it creates a test class that subclasses the standard Ruby Test::Unit::TestCase and defines a method instantiates the HelloWorld class from the binding and exercises its methods.


require 'hello_ruby_world' 
require 'test/unit'

class TestHelloWorld <l Test::Unit::TestCase 
  def test_HelloWorld 
    hw = HelloWorld.new 
    assert_equal(hw.get_hello(), "hello")
    assert_equal(hw.get_world(), "world") 
  end 
end 

Issac executed his test program and was happy with the results:


~/Invisible.Build.System/src/ruby/hello_ruby_world > ruby test_hello_ruby_world.rb 

Loaded suite test_hello_ruby_world
Started

Finished in 0.000405 seconds.

Issac also tried the interactive Ruby interpreter (irb):


~/Invisible.Build.System/src/ruby/hello_ruby_world > irb
>> require "hello_ruby_world"
=> true
>> hw = HelloWorld.new
=> #<HelloWorld:0x3679b4> 
>>  hw.get_hello() + ', ' + hw.get_world() + '!'
=> "hello, world!"
>>  

Bob was satisfied and it was time to integrate the new capability to generate Ruby bindings into ibs. The proper way to do it was to figure out how to create a NetBeans project and a VisualStudio project that contain the various incantations hidden in the Ruby-generated Makefile. But Bob was pressed for time and the Ruby binding was really needed just for the Max OS X platform. Consequently, Bob decided to utilize Python's agility and integrate the Ruby binding building as a standalone Python program that will have to be invoked by the developers or build master after the build of the C++ projects was over. I'll shortly discuss how to integrate ibs into a full-fledged automated software development life-cycle.

For starters, he created a standalone piece of code to build Ruby extensions. He assumed the following:

  • All the Ruby extensions will reside in sub-directories of <root dir>/src/ruby
  • The name of the extension will be the name of the directory it resides in
  • The developers will write the C extension code

The program he came up with automated the entire process. For each Ruby extension it: Generated an extconf.rb configuration file from a template (based on the project path); generated a Makefile from the configuration file; and finally created the extension bundle itself by running 'make'. This code demonstrates one of the simplest ways to invoke external processes like 'ruby' and 'make' from Python using the subprocess module. The subprocess.call() function used here doesn't provide a lot of control or interaction with the launched process, but in this case it's enough. The subprocess modules provides multiple ways to launch and interact with launched processes.

The program is based on the build_ruby_binding() function that accepts a project path (the directory that contains the extension's C code) and eventually creates the Ruby bindings bundle in the same directory. The build_all_ruby_bindings() function just iterates over all the sub-directories of the src/ruby directory and calls build_ruby_binding on each one.


import os
import sys
import subprocess

extconf_template = "require 'mkmf'\n create_makefile(\"%s\")"

def build_ruby_binding(project_path):
  """Build a Ruby binding
  
  - Generate an extconf.rb file (configuration file)
  - Run it through Ruby to generate a Makefile
  - Run the Makefile to build the actual binding
  """
  project_path = os.path.abspath(project_path)
  # Verify the project dir exists
  assert os.path.isdir(project_path)
  name = project_path.split('/')[-1]
  # make sure the binding file exists
  assert os.path.isfile(os.path.join(project_path, name + '.c'))

  save_dir = os.getcwd()
  try:
    os.chdir(project_path)

    # Generate the extconf.rb file
    extconf_rb = extconf_template % name
    open('extconf.rb', 'w').write(extconf_rb)

    # Remove exisitng Makefile
    if os.path.isfile('Makefile'):
      os.remove('Makefile')

    # Invoke the extconf.rb file to generate the Makefile
    subprocess.call(['ruby', 'extconf.rb'])
    assert os.path.isfile('Makefile')

    # Remove exisitng bundle and make a new one
    bundle = name + '.bundle'
    if os.path.isfile(bundle):
      os.remove(bundle)
    subprocess.call(['make'])
    assert os.path.isfile(bundle)

  finally:
    os.chdir(save_dir)

def build_all_ruby_bindings(ruby_dir):
  subdirs = os.walk(ruby_dir).next()[1]
  for s in subdirs:
    build_ruby_binding(s)


if __name__=='__main__':
  ruby_dir = '.'
  build_all_ruby_bindings(ruby_dir)


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.
 

Video