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)



