One of the coolest features of Ruby is that you can extend it with an application
programming interface (API) defined in C/C++. Ruby provides
the C header ruby.h, which comes with a whole host of
functions for creating Ruby classes, modules, and more. In addition to the Ruby-supplied
header, several other high-level abstractions are available to extend Ruby that are built
on top of the native ruby.h—one that this article investigates is the Ruby Interface
for C++ Extensions, or Rice..
Before you jump into any of Ruby's C API or Rice
extensions, I want to clearly describe the standard process of creating the extension:
- You have one or multiple
C/C++sources out of which you make a shared library. - If you create an extension using Rice, you need to link the code to both libruby.a and librice.a.
- Copy the shared library to some folder, and have that folder as part of the RUBYLIB environment variable.
- Use the usual
require-based loading in the Interactive Ruby (irb) prompt/ruby script. If the shared library is called rubytest.so, just typingrequire 'rubytest'loads the shared library.
Suppose the header ruby.h resides in /usr/lib/ruby/1.8/include, the Rice headers reside in /usr/local/include/rice/include, and the extension code is in the file rubytest.cpp. Listing 1 shows how you would compile and load the code.
Listing 1. Compiling and loading a Ruby extension
bash# g++ -c rubytest.cpp –g –Wall -I/usr/lib/ruby/1.8/include \
-I/usr/local/include/rice/include
bash# g++ -shared –o rubytest.so rubytest.o -L/usr/lib/ruby/1.8/lib \
-L/usr/local/lib/rice/lib -lruby –lrice –ldl -lpthread
bash# cp rubytest.so /opt/test
bash# export RUBYLIB=$RUBYLIB:/opt/test
bash# irb
irb> require 'rubytest'
=> true
|
Now, you're ready to create your first Hello World program using Rice. You create
a class using the Rice API called Test with a method hello
that displays the string "Hello, World!" When the Ruby interpreter loads the extension,
it calls the function Init_<shared library name>. For
the rubytest extension from Listing 1,
this call implies that rubytest.cpp has a function Init_rubytest
defined. Rice lets you create your own class using the API define_class.
Listing 2 shows the code.
Listing 2. Creating a class using the Rice API
#include "rice/Class.hpp"
extern "C"
void Init_rubytest( ) {
Class tmp_ = define_class("Test");
}
|
When you compile and load the code in Listing 2 in irb, you should get the output in Listing 3.
Listing 3. Testing the class created using Rice
irb> require ‘rubytest’
=> true
irb> a = Test.new
=> #<Test:0x1084a3928>
irb> a.methods
=> ["inspect", "tap", "clone", "public_methods", "__send__",
"instance_variable_defined?", "equal?", "freeze", …]
|
Note that several predefined class methods such as inspect
are available. That happens because the Test class you
defined is derived implicitly from the Object class
(every Ruby class is derived from Object; in fact,
everything in Ruby, including numbers, is an object that has Object
as the base class).
Now, add a method to the Test class. Listing 4
shows the code.
Listing 4. Adding a method to the Test class
void hello() {
std::cout << "Hello World!";
}
extern "C"
void Init_rubytest() {
Class test_ = define_class("Test")
.define_method("hello", &hello);
}
|
Listing 4 uses the define_method API to add a method to
the Test class. Note that define_class
is a function returning an object of type Class;
define_method is a member function of the class
Module_Impl, which is the base class of Class.
Here's the Ruby test verifying that everything is indeed fine:
irb> require ‘rubytest’ => true irb> Test.new.hello Hello, World! => nil |
Passing arguments from Ruby to C/C++ code
Now that your Hello World program is up, try to pass an argument from Ruby to
the hello function and have the function display the
same to standard output (sdtout). The simplest way to do so is just to add a string
argument to the hello function:
void hello(std::string args) {
std::cout << args << std::endl;
}
extern "C"
void Init_rubytest() {
Class test_ = define_class("Test")
.define_method("hello", &hello);
}
|
In the Ruby world, this is how you would invoke the hello
function:
irb> a = Test.new <Test:0x0145e42112> irb> a.hello "Hello World in Ruby" Hello World in Ruby => nil |
The best thing about using Rice is that you don't need to do anything specific to convert
a Ruby string to std::string.
Now, try to use an array of strings in the hello function,
and then check how you would pass information from Ruby to the
C++ code. The simplest way to do so is to use the
Array data type that Rice provides. Defined in the
header rice/Array.hpp, using Rice::Array is similar
to using a Standard Template Library (STL) container. The usual STL style
iterators and so on are defined as part of the Array
interface. Listing 5 shows the count
routine, which takes a Rice Array as an argument.
Listing 5. Displaying a Ruby array
#include "rice/Array.hpp"
void Array_Print (Array a) {
Array::iterator aI = a.begin();
Array::iterator aE = a.end();
while (aI != aE) {
std::cout << "Array has " << *aI << std::endl;
++aI;
}
}
|
Now here's the beauty of this solution: Suppose you have an
std::vector<std::string> as the
Array_Print argument. Here's the error that Ruby throws:
>> t = Test.new
=> #<Test:0x100494688>
>> t.Array_Print ["g", "ggh1", "hh1"]
ArgumentError: Unable to convert Array to std::vector<std::string,
std::allocator<std::string> >
from (irb):3:in `hello'
from (irb):3
|
However, with the Array_Print routine shown here, Rice
takes care of the conversion from a Ruby array to the C++ Array
type. Here's a sample run:
>> t = Test.new => #<Test:0x100494688> >> t.Array_Print ["hello", "world", "ruby"] Array has hello Array has world Array has ruby => nil |
Now try it the other way round now, passing an array from C++
to the Ruby world. Note that in Ruby, array elements might not necessarily be of the
same type. Listing 6 shows the code.
Listing 6. Passing an array from C++ to Ruby
#include "rice/String.hpp"
#include "rice/Array.hpp"
using namespace rice;
Array return_array (Array a) {
Array tmp_;
tmp_.push(1);
tmp_.push(2.3);
tmp_.push(String("hello"));
return tmp_;
}
|
Listing 6 clearly shows that you can create a Ruby array with different types right
inside C++. Here's the test code in Ruby:
>> x = t.return_array => [1, 2.3, "hello"] >> x[0].class => Fixnum >> x[1].class => Float >> x[2].class => String |
What if I don't have the flexibility to change a C++ argument list?
More common than not, you'll find that the Ruby interface is meant to translate the
data to C++ functions whose signature you can't change.
For example, consider a case where you need to pass an array of strings from Ruby
to C++. The C++ function
signature looks like:
void print_array(std::vector<std::string> args) |
In effect, what you're looking for here is some sort of from_ruby
function that takes in a Ruby array and converts it to
std::vector<std::string>. That's exactly what Rice
provides—a from_ruby function with the following
signature:
template <typename T> T from_ruby(Object ); |
For every Ruby data type you need to convert to a C++ type,
you need to template-specialize the from_ruby routine.
For example, if you pass the Ruby array to the process function shown above,
Listing 7 shows how you should define the
from_ruby function.
Listing 7. Converting ruby array to std::vector<std::string>
template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o) {
Array a(o);
std::vector<std::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(((String)*aI).str());
return v;
}
|
Note that the from_ruby function doesn't need to be
explicitly invoked. When an array of string is passed as
the function argument from the Ruby world, from_ruby
converts it into std::vector<std::string>. The code
in Listing 7 is not perfect, though, and you have already seen that arrays in Ruby
can have different types. In contrast, you have made a call to
((String)*aI).str() to get a std::string
from Rice::String. (str is a
method of Rice::String: Check String.hpp for more details.)
If you were to handle the most generic case, Listing 8 shows how
the code would look.
Listing 8. Converting a ruby array to std::vector<std::string> (generic case)
template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o) {
Array a(o);
std::vector<std::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(from_ruby<std::string> (*aI));
return v;
}
|
Because each element of the Ruby array is also a Ruby object of type
String and you are betting on Rice having a
from_ruby method defined for converting that type into
std::string, nothing else need be done. If such is not the
case, you need to provide a from_ruby method for the
conversion. Here's the from_ruby method from
to_from_ruby.ipp in the Rice sources:
template<>
inline std::string from_ruby<std::string>(Rice::Object x) {
return Rice::String(x).str();
}
|
Test this code from the Ruby world. Begin by passing an array of all strings, as in Listing 9.
Listing 9. Validating from_ruby functionality
>> t = Test.new => #<Test:0x10e71c5c8> >> t.print_array ["aa", "bb"] aa bb => nil >> t.print_array ["aa", "bb", 111] TypeError: wrong argument type Fixnum (expected String) from (irb):4:in `print_array' from (irb):4 |
As expected, the first invocation of print_array went
fine. Because there's no from_ruby method to convert
Fixnum to std::string, the
second invocation results in the Ruby interpreter throwing up
TypeError. There are several ways to fix this
error—for example, during the Ruby invocation, pass only strings as part of
the array (such as t.print_array["aa", "bb", 111.to_s]),
or inside the C++ code, make a call to
Object.to_s. The to_s method
is a part of the Rice::Object interface and returns a
Rice::String, which has a predefined str
method returning an std::string. Listing 10
uses the C++ approach.
Listing 10. Using Object.to_s to populate the vector of strings
template<>
std::vector<std::string> from_ruby< std::vector<std::string> > (Object o) {
Array a(o);
std::vector<std::string> v;
for(Array::iterator aI = a.begin(); aI != a.end(); ++aI)
v.push_back(aI->to_s().str());
return v;
}
|
In general, the code in Listing 10 will be more involved, because you need to handle custom string representations for user-defined classes.
Creating a complete class with variables using C++
You have already seen how to create a Ruby class and associated functions inside
C++ code. For a more generic class, you need a way
to define instance variables as well as provide for an initialize
method. To set and get values of a Ruby object's instance variables, you use the
Rice::Object::iv_set and Rice::Object::iv_get
methods, respectively. Listing 11 shows the code.
Listing 11. Defining an initialize method in C++
void init(Object self) {
self.iv_set("@intvar", 121);
self.iv_set("@stringvar", String("testing"));
}
Class cTest = define_class("Test").
define_method("initialize", &init);
|
When a C++ function is declared as a Ruby class method
using the define_method API, you have the option of
declaring the first argument of the C++ function as
Object, and Ruby fills in this Object
with a reference to the calling instance. You then invoke iv_set
on the Object to set the instance variables. Here's how
the interface looks in the Ruby world:
>> require 'rubytest' => true >> t = Test.new => #<Test:0x1010fe400 @stringvar="testing", @intvar=121> |
Likewise, to return an instance variable, the returning function needs to take an
Object that refers to the object in Ruby and invoke
iv_get on it. Listing 12 shows a
snippet.
Listing 12. Retrieving values from a Ruby object
void init(Object self) {
self.iv_set("@intvar", 121);
self.iv_set("@stringvar", String("testing"));
}
int getvalue(Object self) {
return self.iv_get("@intvar");
}
Class cTest = define_class("Test").
define_method("initialize", &init).
define_method("getint", &getvalue);
|
Morphing a C++ class into a Ruby type
So far, you have wrapped free functions (that is, non-class methods) as Ruby
class methods. You have passed references to the Ruby object by declaring the
C functions with the first argument Object.
This approach works, but isn't good enough to wrap a C++
class as a Ruby object. To wrap a C++ class, you still
use the define_class method, except that you now
"templatize" it with the C++ class type. The code in
Listing 13 wraps a C++ class as
a Ruby type.
Listing 13. Wrapping a C++ class as a Ruby type
class cppType {
public:
void print(String args) {
std::cout << args.str() << endl;
}
};
Class rb_cTest =
define_class<cppType>("Test")
.define_method("print", &cppType::print);
|
Note that define_class is templatized, as discussed. Not
all is well with this class, though. Here's the log from the Ruby interpreter when you
try to instantiate an object of type Test:
>> t = Test.new TypeError: allocator undefined for Test from (irb):3:in `new' from (irb):3 |
What just happened? Well, you need to explicitly bind the constructor to a Ruby type.
(It's one of those Rice quirks.) Rice provides you with a define_constructor
method to associate a constructor for the C++ type. You
also need to include the header Constructor.hpp. Note that you must do so even when
you don't have an explicit constructor in your code. Listing 14
provides the sample code.
Listing 14. Associating a C++ constructor with a Ruby type
#include "rice/Constructor.hpp"
#include "rice/String.hpp"
class cppType {
public:
void print(String args) {
std::cout << args.str() << endl;
}
};
Class rb_cTest =
define_class<cppType>("Test")
.define_constructor(Constructor<cppType>())
.define_method("print", &cppType::print);
|
It is also possible to associate a constructor with an argument list using the
define_constructor method. The Rice way to do so is to
add the argument types to the template list. For example, if cppType
has a constructor that accepts an integer, then you must call
define_constructor as
define_constructor(Constructor<cppType, int>()).
One caveat here: Ruby types don't have multiple constructors. So, if you have a
C++ type with multiple constructors and you associate them
all using define_constructor, then from the Ruby world,
you can instantiate the type with arguments (or not) as defined by the last
define_constructor in the source code. Listing 15
explains everything just discussed.
Listing 15. Associating constructors with arguments
class cppType {
public:
cppType(int m) {
std::cout << m << std::endl;
}
cppType(Array a) {
std::cout << a.size() << std::endl;
}
void print(String args) {
std::cout << args.str() << endl;
}
};
Class rb_cTest =
define_class<cppType>("Test")
.define_constructor(Constructor<cppType, int>())
.define_constructor(Constructor<cppType, Array>())
.define_method("print", &cppType::print);
|
Here's the log from the Ruby world. Note that the constructor associated last is the one Ruby understands:
>> t = Test.new 2 TypeError: wrong argument type Fixnum (expected Array) from (irb):2:in `initialize' from (irb):2:in `new' from (irb):2 >> t = Test.new [1, 2] 2 => #<Test:0x10d52cf48> |
Defining a new Ruby type as part of a module
Defining a new Ruby module from C++ boils down to
making a call to define_module. To define a class
that's available only as part of said module, you use define_class_under
instead of the usual define_class method. The first
argument to define_class_under is the module object.
From Listing 14, if you were to define cppType
as part of a Ruby module named types,
Listing 16 shows how you would do it.
Listing 16. Declaring a type as part of a module
#include "rice/Constructor.hpp"
#include "rice/String.hpp"
class cppType {
public:
void print(String args) {
std::cout << args.str() << endl;
}
};
Module rb_cModule = define_module("Types");
Class rb_cTest =
define_class_under<cppType>(rb_cModule, "Test")
.define_constructor(Constructor<cppType>())
.define_method("print", &cppType::print);
|
And here is how you use the same in Ruby:
>> include Types => Object >> y = Types::Test.new [1, 1, 1] 3 => #<Types::Test:0x1058efbd8> |
Note that module names and class names must begin with an uppercase letter in Ruby. Rice doesn't error out if, say, you name the module types instead of Types.
Creating a Ruby struct using C++ code
You use the struct construct in Ruby to quickly create a
boilerplate Ruby class. Listing 17 shows the typical Ruby way
of creating a new class of type NewClass with three
variables named a, ab, and
aab.
Listing 17. Using a Ruby Struct to create a new class
>> NewClass = Struct.new(:a, :ab, :aab) => NewClass >> NewClass.class => Class >> a = NewClass.new => #<struct NewClass a=nil, ab=nil, aab=nil> >> a.a = 1 => 1 >> a.ab = "test" => "test" >> a.aab = 2.33 => 2.33 >> a => #<struct NewClass a=1, ab="test", aab=2.33> >> a.a.class => Fixnum >> a.ab.class => String >> a.aab.class => Float |
To code the equivalent of Listing 17 in C++, you need to use
the define_struct( ) API declared in the header
rice/Struct.hpp. This API returns a Rice::Struct. You
associate the Ruby class that this struct creates and the
module of which the class will be part. That's what the initialize
method is for. The individual class members are defined using the
define_member function call. Note that you have created
a new Ruby type, except that you have not associated any C++
type or functions with it. Here's the code to create a class called
NewClass:
#include "rice/Struct.hpp"
…
Module rb1 = define_module("Types");
define_struct().
define_member("a").
define_member("ab").
define_member("aab").
initialize(rb1, "NewClass");
|
This article covered a fair bit of ground—creating Ruby objects in C++
code, associating C-style functions as Ruby object
methods, converting data types between Ruby and C++,
creating instance variables, and wrapping a C++ class as
a Ruby type. It is possible to achieve all of this work using the ruby.h header and
libruby, but you would have to do a lot of boilerplate
coding to make all ends work. Rice makes all that work easier. Here's wishing you
a lot of fun writing new extensions in C++ for the Ruby
world!
Learn
- rice.rubyforge.org is your home for Rice and comes with excellent doxygen-style documentation.
- Popularly known as the Pickaxe book, Programming Ruby: The Pragmatic Programmers' Guide (Dave Thomas, Chad Fowler, and Andy Hunt; 2nd edition) is a Ruby must-read.
- Another invaluable Ruby resource is The Ruby Programming Language by Yukihiro "Matz" Matsumoto (Ruby's creator) and David Flanagan (O'Reilly, 2008).
- To Ruby From C and C++ is a great site for
C/C++programmers who want to learn Ruby. - The Open Source developerWorks zone provides a wealth of information on open source tools and using open source technologies.
- Stay current with developerWorks technical events and webcasts focused on a variety of IBM products and IT industry topics.
- Attend a free developerWorks Live! briefing to get up-to-speed quickly on IBM products and tools, as well as IT industry trends.
- Watch developerWorks on-demand demos ranging from product installation and setup demos for beginners, to advanced functionality for experienced developers.
- Follow developerWorks on Twitter.
Get products and technologies
- Evaluate IBM products in the way that suits you best: Download a product trial, try a product online, use a product in a cloud environment, or spend a few hours in the SOA Sandbox learning how to implement Service Oriented Architecture efficiently.
Discuss
- Check out developerWorks blogs and get involved in the developerWorks community.
- Get involved in the developerWorks community. Connect with other developerWorks users while exploring the developer-driven blogs, forums, groups, and wikis.
Arpan Sen is a lead engineer working on the development of software in the electronic design automation industry. He has worked on several flavors of UNIX, including Solaris, SunOS, HP-UX, and IRIX, as well as Linux and Microsoft Windows for several years. He takes a keen interest in software performance-optimization techniques, graph theory, and parallel computing. Arpan holds a post-graduate degree in software systems.




