Memory Management#

The trickiest part of wrapping a C++ API is correctly managing memory shared between C++ and Ruby. It is critical to get this right - otherwise your program will crash. The key to getting it right is being crystal clear on who owns each piece of memory.

Rice divides native types into Builtin types and external types. Builtin types are copied between C++ and Ruby while external types are wrapped. For additional information about builtin types please refer to the Type Conversions section.

The rest of this section discusses how to manage memory of external types.

C++ to Ruby#

By default Rice assumes that C++ instances passed to Ruby continue to be owned by C++. Thus there is no transfer of ownership. In this case, the transfer follows these rules:

Method Return Type (T)

C++ to Ruby

Cleanup

Value (T)

Copy constructor

Ruby frees the copy, C++ the original

Reference (T&)

No copy

C++ frees the C++ instance

Pointer (T*)

No copy

C++ frees the C++ instance

However, many APIs transfer ownership of returned values to the caller. Thus Ruby take ownership of the value. In this case the transfer follows these rules:

Method Return Type (T)

C++ to Ruby

Cleanup

Value (T)

Copy constructor

Ruby frees the copy, C++ the original

Reference (T&)

Move constructor

Ruby frees the C++ instance

Pointer (T*)

No copy

Ruby frees the C++ instance

Use Return to tell Rice that Ruby should take ownership of a returned value. Let’s look at an example:

class MyClass
{
}

class Factory
{
public:
  static MyClass* create()
  {
    return new MyClass();;
  }
}

extern "C"
void Init_test()
{
  Data_Type<MyClass> rb_cMyClass = define_class<MyClass>("MyClass");

  Data_Type<Factory> rb_cFactory = define_class<Factory>("Factory")
      .define_function("create", &Factory::create); <--- WRONG, results in memory leak
}

Each time Factory#create is called from Ruby, a new C++ instance of MyClass will be created. Using Rice’s default rules, this will result in a memory leak because those instance will never be freed.

1_000.times do
  my_class = Factory.create
end

To fix this, you need to tell Rice that it should take ownership of the returned instance:

define_function("create", &Factory::create, Return().takeOwnership());

Notice the addition of the Return().takeOwnership(), which creates an instance of the Return class and tells it to take ownership of the instance returned from C++.

Ruby to C++#

Sometimes it is necessary to tie the lifetime of one Ruby object to another. This often times happens with containers. For example, imagine we have a Listener and a ListenerContainer class.

class Listener
{
};

class ListenerContainer
{
  public:
    void addListener(Listener* listener)
    {
      mListeners.push_back(listener);
    }

    int process()
    {
      for(const Listener& listener : mListeners)
      {
      }
    }

  private:
    std::vector<Listener*> mListeners;
};

Assuming these classes are wrapped with Rice, the following code crash:

@handler = ListenerContainer.new
@handler.add_listener(Listener.new)
GC.start
@handler.process !!!! crash !!!!!

The Ruby garbage collector will notice that the Listener.new object is orphaned and will free it. That it turn frees the underlying C++ Listener object resulting in a crash when process is called.

To prevent this, we want to tie the lifetime of the Ruby listener instance to the container. This is done by calling keepAlive() in the argument list:

define_class<ListenerContainer>("ListenerContainer")
  .define_method("add_listener", &ListenerContainer::addListener, Arg("listener").keepAlive())

With this change, when a listener is added to the container, the container keeps a reference to it and will call rb_gc_mark to keep it alive. This is exactly the same thing Ruby’s collection classes, such as Arrays and Hashes, do. The Listener object will not be freed until the container itself goes out of scope.

Another example is when a returned object is dependent upon the original object. For example:

class Column;

class Database
{
public:
  Database()
  {
    // connect to Database
  }

  ~Database()
  {
    // disconnect from database
  }

  Column getColumn(uint32_t index)
  {
     return Column(*this, index);
  }

  std::string looupName(uint32_t index)
  {
    return some_name;
  }
};

class Column
{
public:
  Column(Database& database, uint32_t index): database_(database), index_(index)
  {
  }

  Column getName()
  {
    return this->database.lookupName(this->index_):
  }

private:
  Database& database_;
  uint32_t index_;
};

Assuming these classes are wrapped with Rice, then the following Ruby code will crash:

def get_column(column_index)
  database = Database.new(...)
  column = database.get_column(column_index)
end

column = get_column(0)
puts column.name

The problem is that the instance of the Database class created in get_column will likely be garbage collected when the method returns. As a result, when Column#name is called it will have a dangling reference to the no longer valid database object.

Obviously this code could be rewritten to make sure the database object remains alive throughout the program. Alternatively, you can tell Rice that to tie the lifetime of the Database object to the Column object so that it will not be freed until the Column is freed:

define_class<Database>("Database")
  .define_method("get_column", &Database::getColumn, Return().keepAlive())

Note that Return().keepAlive() will work with external types only. An attempt to use it with builtin type will result in runtime exception.

C++ Referencing Ruby Objects#

When reference Ruby objects from C++, you need to let Ruby know about them so they are not prematurely garbage collected.

There are several ways this can happen:

Stack#

If you are working with VALUEs or Objects stored on the stack, the Ruby garbage collector will try to find them automatically. However, optimizing compilers may prevent them from doing so. Thus you may need to use Ruby’s RB_GC_GUARD macro

Heap#

If you allocate an Object on the heap or if it is a member of an object that might be allocated on the heap, use Rice::Address_Registration_Guard to register the object with the garbage collector.

Member Variables#

If you create classes or structures that reference Ruby objects, you need to implement a custom ruby_mark function:

class MyClass
{
  VALUE value_;
}

namespace Rice
{
  template<>
  ruby_mark(const MyClass* myClass)
  {
    rb_gc_mark(myClass->value_);
  }
}

Data_Type<MyClass> class = define_class<MyClass>("MyClass")
          .define_constructor(Constructor<MyClass>());