Building Ruby extensions in C++ using Rice

Add new programming extensions in Ruby

Learn to extend the Ruby programming language using the Rice interface to Ruby's native C API. Using this object-oriented interface, you can create Ruby objects in C++ code, convert data objects between Ruby and C++, and more.

Share:

Arpan Sen, Author, Independent

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.



16 July 2012

Also available in Chinese Russian Japanese Portuguese

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..

Creating a Ruby extension

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:

  1. You have one or multiple C/C++ sources out of which you make a shared library.
  2. If you create an extension using Rice, you need to link the code to both libruby.a and librice.a.
  3. Copy the shared library to some folder, and have that folder as part of the RUBYLIB environment variable.
  4. Use the usual require-based loading in the Interactive Ruby (irb) prompt/ruby script. If the shared library is called rubytest.so, just typing require '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

The Hello World program

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");

Conclusion

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!

Resources

Learn

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

Comments

developerWorks: Sign in

Required fields are indicated with an asterisk (*).


Need an IBM ID?
Forgot your IBM ID?


Forgot your password?
Change your password

By clicking Submit, you agree to the developerWorks terms of use.

 


The first time you sign into developerWorks, a profile is created for you. Information in your profile (your name, country/region, and company name) is displayed to the public and will accompany any content you post, unless you opt to hide your company name. You may update your IBM account at any time.

All information submitted is secure.

Choose your display name



The first time you sign in to developerWorks, a profile is created for you, so you need to choose a display name. Your display name accompanies the content you post on developerWorks.

Please choose a display name between 3-31 characters. Your display name must be unique in the developerWorks community and should not be your email address for privacy reasons.

Required fields are indicated with an asterisk (*).

(Must be between 3 – 31 characters.)

By clicking Submit, you agree to the developerWorks terms of use.

 


All information submitted is secure.

Dig deeper into Open source on developerWorks


static.content.url=http://www.ibm.com/developerworks/js/artrating/
SITE_ID=1
Zone=Open source
ArticleID=825652
ArticleTitle=Building Ruby extensions in C++ using Rice
publish-date=07162012