ruby C++ extensions

J

Jeff Mitchell

The recent C++/ruby post has reminded me to say a few words about my
travails in ruby C++ extensions, if only to possibly help some poor
soul googling in the future.

First, require 'mkmf';create_makefile("cxxruby") will use
CONFIG['LDSHARED'] to create your extension, which is most likely
wrong. For gcc this results in the error

undefined symbol: __gxx_personality_v0

when you attempt to load the module. The linker must be C++ aware due
to static C++ objects: exceptions, iostream, rtti, etc. I solved this
by setting CONFIG['LDSHARED'] = "g++ -shared".

This solution is nonportable -- perhaps someone can suggest a better
way? To be consistent, ruby could detect the presence/absence of a
C++ compiler during ./configure, setting CONFIG['CXX'] and
CONFIG['CXX_LDSHARED']. Also, mkmf should know to use CXX_LDSHARED
when C++ sources exist.

Second, ruby's exception handling via setjmp/longjmp effectively means
you should never construct a C++ object with a nontrivial destructor
on the stack. If ruby longjmps out of your code, your destructors
will not be called.

Therefore if a ruby method you are implementing requires the use of
temporary C++ objects, you must construct those objects on the heap.

The easiest and saftest technique I found is to write three C
functions for a ruby method: the actual ruby hook, the body, and the
ensure:

#include "ruby.h"

class CXXRuby { } ;

static VALUE rb_cxxruby_superfunk_body( void** args ) ;
static VALUE rb_cxxruby_superfunk_ensure( void** args ) ;

static VALUE rb_cxxruby_superfunk( int argc,
VALUE* argv,
VALUE self )
{
// construct all C++ objects on the heap

CXXRuby* cxx_obj = 0 ;

void* args[4] ;
args[0] = reinterpret_cast<void*>(argc) ;
args[1] = static_cast<void*>(argv) ;
args[2] = reinterpret_cast<void*>(self) ;
args[3] = static_cast<void*>(&cxx_obj) ;

try
{
cxx_obj = new CXXRuby() ;
}
catch(...)
{
rb_cxxruby_superfunk_ensure(args) ;
rb_raise(rb_eRuntimeError,
"caught exception from an unfunky constructor") ;
}

return rb_ensure(
reinterpret_cast<VALUE(*)(...)>(rb_cxxruby_superfunk_body),
reinterpret_cast<VALUE>(args),
reinterpret_cast<VALUE(*)(...)>(rb_cxxruby_superfunk_ensure),
reinterpret_cast<VALUE>(args)) ;
}

static VALUE rb_cxxruby_superfunk_body( void** args )
{
int argc = reinterpret_cast<int>(args[0]) ;
VALUE* argv = static_cast<VALUE*>(args[1]) ;
VALUE self = reinterpret_cast<VALUE>(args[2]) ;
CXXRuby* cxx_obj = *static_cast<CXXRuby**>(args[3]) ;

try
{
VALUE get_out ;
VALUE the_funk ;

if( rb_scan_args(argc, argv, "11",
&get_out, &the_funk) == 1 )
{
the_funk = Qtrue ;
}

// ...
}
catch(...)
{
rb_raise(rb_eRuntimeError, "insufficient funk levels") ;
}

return self ;
}

static VALUE rb_cxxruby_superfunk_ensure( void** args )
{
// C++ cleanup

CXXRuby* cxx_obj = *static_cast<CXXRuby**>(args[3]) ;
delete cxx_obj ;
return Qnil ;
}

static VALUE cCXXRuby ;

extern "C" {

void Init_cxxruby()
{
cCXXRuby = rb_define_class("CXXRuby", rb_cObject) ;
rb_define_singleton_method(
cCXXRuby,
"superfunk",
reinterpret_cast<VALUE(*)(...)>(rb_cxxruby_superfunk),
-1) ;

rb_define_global_const("OperationSupergroove", cCXXRuby) ;
}

} // extern "C"
 
N

nobu.nokada

Hi,

At Fri, 14 May 2004 16:43:54 +0900,
Jeff Mitchell wrote in [ruby-talk:100255]:
This solution is nonportable -- perhaps someone can suggest a better
way? To be consistent, ruby could detect the presence/absence of a
C++ compiler during ./configure, setting CONFIG['CXX'] and
CONFIG['CXX_LDSHARED']. Also, mkmf should know to use CXX_LDSHARED
when C++ sources exist.

Indeed. I'll consider about it.
The easiest and saftest technique I found is to write three C
functions for a ruby method: the actual ruby hook, the body, and the
ensure:

You can use rb_protect() instead, and should use a struct
rather than ugly enumerated reinterpret_casts I guess.


#include "ruby.h"

class CXXRuby {
};

struct superfunk_args {
int argc;
VALUE *argv;
VALUE self;
CXXRuby cxx_obj;
};

static VALUE rb_cxxruby_superfunk_body(VALUE);

static VALUE
rb_cxxruby_superfunk(int argc, VALUE * argv, VALUE self)
{
int status = 0;
VALUE result;

try {
struct superfunk_args args = {argc, argv, self};
result = rb_protect(rb_cxxruby_superfunk_body,
reinterpret_cast<VALUE>(&args), &status);
}
catch(...) {
rb_raise(rb_eRuntimeError,
"caught exception from an unfunky constructor");
}
if (status)
rb_jump_tag(status);
return result;
}

static VALUE
rb_cxxruby_superfunk_body(VALUE args)
{
struct superfunk_args *const argp =
reinterpret_cast<struct superfunk_args *>(args);
int argc = argp->argc;
VALUE *argv = argp->argv;
VALUE self = argp->self;
CXXRuby &cxx_obj = argp->cxx_obj;

VALUE get_out;
VALUE the_funk;

if (rb_scan_args(argc, argv, "11", &get_out, &the_funk) == 1) {
the_funk = Qtrue;
}
// ...

return self;
}

static VALUE cCXXRuby;

extern "C" void
Init_cxxruby(void)
{
cCXXRuby = rb_define_class("CXXRuby", rb_cObject);
rb_define_singleton_method(cCXXRuby, "superfunk",
RUBY_METHOD_FUNC(rb_cxxruby_superfunk), -1);

rb_define_global_const("OperationSupergroove", cCXXRuby);
}
 
P

Paul Brannan

Second, ruby's exception handling via setjmp/longjmp effectively means
you should never construct a C++ object with a nontrivial destructor
on the stack. If ruby longjmps out of your code, your destructors
will not be called.

It's worse than that; if ruby longjmps over the destruction of an
automatic object, the program has undefined behavior. And if a C++
exception ever leaves C++ code and goes into Ruby code, the result will
also be undefined.

C99 programmers also have to be careful; longjmping over the destruction
of a variable-length array can result in a memory leak.
Therefore if a ruby method you are implementing requires the use of
temporary C++ objects, you must construct those objects on the heap.

If you construct the object on the heap, it's useful to register the
object with the Ruby GC so it will get destroyed and not leaked. An
alternative is to translate the exception to a C++ exception and then
back into a ruby exception. I support both techniques in excruby
(http://excruby.sourceforge.net):

// Register with the garbage collector
class Foo
{
};
Foo * f = new Foo;
REGISTER_WITH_RUBY_GC(Foo, f);

// Allocate on the stack
RUBY_TRY
{
Foo f;
// rb_cpp_funcall translates Ruby exceptions into the C++ exception
// Ruby_Exception; RUBY_CATCH will translate it back for me:
rb_cpp_funcall(xyz, rb_intern("foo"), Qtrue);
}
RUBY_CATCH

I've also been playing with an idea somewhat borrowed from
boost::python (I haven't released any of this code yet):

rb_cFoo = define_class("Foo")
.define_creation_funcs(
default_allocation_func<Foo>,
&Foo::initialize)
.define_method("foo", &Foo::foo);

The above code works as is, but eventually I'll also be able to write:

class My_Exception_Handler
: public Exception_Handler
{
Std_Exception_Handler(Exception_Handler const * next_exception_handler)
: Exception_Handler(next_exception_handler)
{
}

virtual void handle_exception() const
{
try
{
call_next_exception_handler();
}
catch(My_Exception const & ex)
{
throw Ruby_Exception(rb_cMy_Exception, ex.what());
}
};

// My_Exception_Handler will get used for any exceptions thrown inside
// functions in class Foo:
rb_cFoo = define_class("Foo")
.add_exception_handler<My_Exception_Handler>()
.define_creation_funcs(
default_allocation_func<Foo>,
&Foo::initialize)
.define_method("foo", &Foo::foo);

Also note that SWIG (http://www.swig.org) has support for exceptions,
and makes translating C++ exceptions into Ruby exceptions easy (or at
least easier than they otherwise would be). Swig also allocates all its
objects on the heap, so you don't have to worry about Ruby exceptions
unless you explictly call back into Ruby code from your C++ code:

%exception {
try {
$action
}
catch(std::exception const & ex)
{
rb_raise(rb_eCPlusPlusException, ex.what());
}
}

And note too that excruby is not designed to replace SWIG; it's
designed so you can use it right alongside SWIG:

%exception {
RUBY_TRY {
$action
}
RUBY_CATCH
}

Paul
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Members online

No members online now.

Forum statistics

Threads
473,769
Messages
2,569,582
Members
45,067
Latest member
HunterTere

Latest Threads

Top