Inheritance#

Rice supports creating child classes via the define_class method:

class Base
{
public:
  virtual void foo();
};

class Derived
  : public Base
{
};

extern "C"
void Init_test()
{
  Data_Type<Base> rb_cBase =
    define_class<Base>("Base")
    .define_method("foo", &Base::foo);

  Data_Type<Derived> rb_cDerived =
    define_class<Derived, Base>("Derived");
}

This is done by adding a second template parameter to define_class that specifies that Derived inherits from Base.

Rice does not support multiple inheritance.

Directors#

Inheritance becomes much more complex if you want to create Ruby classes that inherit from wrapped C++ classes. This introduces several problems:

  • Ruby classes should be able to override C++ virtual methods

  • Overriden virtual methods should be able to call super and invoke the overriden C++ method

  • C++ code calling the virtual methods should invoke the overriden version in Ruby

Rice supports these use cases through the use of Director classes. Directors are proxies that can correctly dispatch method invocations up or down a Class hierarchy.

Note

The name Director comes from SWIG - for more information see https://www.swig.org/Doc4.0/SWIGPlus.html#SWIGPlus_director_classes_introduction.

Take the following class:

class VirtualBase
{
  public:
    VirtualBase();
    virtual int doWork();
    virtual int processWorker() = 0;
};

Due to the abstract nature of this class, we cannot directly wrap it in Rice, as any C++ compiler will complain about trying to instantiate a virtual class. Even without the pure virtual function, any call to VirtualBase::doWork will stop at the C++ level and execution will not pass down into any Ruby subclasses.

To properly wrap both of these methods, use a Rice::Director subclass as a proxy and use this new proxy class as the type to wrap with define_class:

#include <rice/rice.hpp>

class VirtualBaseProxy : public VirtualBase, public Rice::Director
{
  public:
    VirtualBaseProxy(Object self) : Rice::Director(self)
    { }

    virtual int doWork()
    {
      int result = getSelf().call("do_work");
      return detail::From_Ruby<int>().convert(result);
    }

    int default_doWork()
    {
      return VirtualBase::doWork();
    }

    virtual int processWorker()
    {
      int result = getSelf().call("process_worker");
      return detail::From_Ruby<int>().convert(result);
    }

    int default_processWorker()
    {
      raisePureVirtual();
    }
};

There is a lot going on here, so we’ll go through each part.

class VirtualBaseProxy : public Virtualbase, public Rice::Director { }

First, the class needs to subclass both the virtual class in question and Rice::Director.

public:
  VirtualBaseProxy(Object self) : Rice::Director(self) { }

For Rice::Director to work its magic, every instance of this class needs to have a handle to its Ruby instance. The constructor must take a Rice::Object as the first argument and pass it to Rice::Director.

Next we implement doWork. The director class overrides its by forwarding the invocation to the Ruby instance.

virtual int doWork()
{
  int result = getSelf().call("do_work");
  return detail::From_Ruby<int>().convert(result);
}

int default_doWork()
{
  return VirtualBase::doWork();
}

It director also implements default_doWork which enables Ruby to call the overriden virtual C++ method. The default_ prefix is a naming convention to help keep straight which methods perform which functions.

If Ruby should never call the C++ method then the default_ implementation should call raisePureVirtual():

int default_processWorker()
{
  raisePureVirtual();
}

The method raisePureVirtual() exists to allow wrapping a pure virtual method into Ruby (and ensuring compilation is possible) while also making sure any users of this extension are informed quickly if there is nothing callable on the C++ side.

Once the Director class is built, it’s time to wrap it into Ruby:

extern "C"
void Init_virtual() {
  define_class<VirtualBase>("VirtualBase")
    .define_director<VirtualBaseProxy>()
    .define_constructor(Constructor<VirtualBaseProxy, Rice::Object>())
    .define_method("do_work", &VirtualBaseProxy::default_doWork)
    .define_method("process_worker", &VirtualBaseProxy::default_processWorker);
}

There are couple of new things in this code.

First, is the addition of the define_director call which takes the VirtualBaseProxy as a template parameter.

Second, the Constructor template parameter must also be the VirtualBaseProxy to allow proper object construction and destruction of the derived objects.

Third, the define_method calls should point to the default_* implementations.