Skip to content

Registries

Rice maintains several registries to track bindings at runtime. These enable type lookup, method resolution, and introspection.

Registries Singleton

All registries are accessed through a singleton:

class Registries
{
public:
  static Registries& instance();

  TypeRegistry& types();
  NativeRegistry& natives();
  InstanceRegistry& instances();
};

TypeRegistry

Maps C++ types to Ruby classes. The TypeRegistry serves two important purposes:

1. Polymorphism Support

When a C++ method returns a base class pointer that actually points to a derived class, Rice uses RTTI (typeid) to look up the derived type in the registry and wrap it as the correct Ruby class. For example:

class Base { virtual ~Base() = default; };
class Derived : public Base {};

Base* create() { return new Derived(); }  // Returns Derived* as Base*

When create() is called from Ruby, Rice uses typeid(*result) to discover the object is actually a Derived, looks it up in the TypeRegistry, and wraps it as Rb::Derived instead of Rb::Base.

Note: This requires RTTI to be enabled.

2. Unregistered Type Detection

As Rice processes define_class, define_constructor, define_method, define_attr, etc., it tracks every C++ type it encounters. Types that are used but not yet registered are added to an "unverified" list. At the end of extension initialization, Rice can report which types were referenced but never registered with define_class<T>().

This helps developers catch errors early. Without this, an unregistered type would only fail at runtime when Ruby code actually calls a method that uses that type.

class TypeRegistry
{
public:
  // Register a type binding
  void add(std::type_index type, VALUE klass, rb_data_type_t* rbType);

  // Look up Ruby class for a C++ type
  std::pair<VALUE, rb_data_type_t*> lookup(std::type_index type);

  // Check if a type is registered
  bool isDefined(std::type_index type);

  // Verify all encountered types are registered
  void verify();
};

Used when:

  • To_Ruby<T> needs to find the Ruby class to wrap an object
  • From_Ruby<T> validates that a Ruby object is the correct type
  • Polymorphic returns need to find the derived class
  • Introspection APIs list all wrapped classes
  • Extension loading verifies all types are registered

Key design:

  • Uses std::type_index as the key (from typeid(T))
  • Stores both the Ruby class VALUE and the rb_data_type_t pointer
  • Tracks unverified types for developer feedback
  • Thread-safe for reads after initialization

NativeRegistry

Stores all wrapped C++ functions, methods, and attributes.

class NativeRegistry
{
public:
  // Register a native for a class/module
  void add(VALUE klass, std::shared_ptr<Native> native);

  // Get all natives for a method name
  std::vector<Native*> lookup(VALUE klass, std::string_view name);

  // Get all natives for a class (for introspection)
  std::vector<Native*> lookup(VALUE klass);
};

Used when:

  • Native::resolve() finds overload candidates
  • Introspection APIs enumerate methods for a class

Key design:

  • Indexed by (klass, method_name) for fast lookup during calls
  • Also indexed by klass alone for introspection
  • Stores shared_ptr<Native> for shared ownership

InstanceRegistry

Tracks which Ruby objects wrap which C++ instances.

class InstanceRegistry
{
public:
  // Register a Ruby object wrapping a C++ pointer
  void add(void* ptr, VALUE ruby_object);

  // Find Ruby object for a C++ pointer
  std::optional<VALUE> lookup(void* ptr);

  // Remove mapping (when Ruby object is collected)
  void remove(void* ptr);
};

Used when:

  • Returning a C++ object that's already wrapped (avoids double-wrapping)
  • Looking up self for method calls

Key design:

  • Uses raw void* as key (the C++ object address)
  • Weak references to Ruby objects (doesn't prevent GC)
  • Automatically cleaned up when Ruby objects are collected

ModuleRegistry

Tracks defined modules for introspection.

class ModuleRegistry
{
public:
  void add(VALUE module);
  std::vector<VALUE> modules();
};

Used when:

  • Introspection APIs list all defined modules

Lifecycle

Initialization

Registries are populated during extension initialization:

extern "C" void Init_my_extension()
{
  // Each define_class/define_method populates registries
  define_class<Foo>("Foo")
    .define_method("bar", &Foo::bar);
}

After Init_my_extension returns, registries should be treated as read-only.

Thread Safety

  • Writes: Only during initialization (single-threaded)
  • Reads: Thread-safe (multiple threads can call methods)

Ruby's GVL ensures that method calls don't race with each other, but registries are also designed for lock-free reads.

Garbage Collection

InstanceRegistry interacts with Ruby's GC:

  • Entries use weak references
  • When Ruby collects a wrapped object, its entry is removed
  • The C++ destructor (via Wrapper) triggers cleanup

Introspection API

The registries power Rice's Ruby introspection API:

# Access via Rice::Registries singleton
registries = Rice::Registries.instance

# List all wrapped classes
registries.types.klasses.each { |k| puts k.name }

# Get methods for a class
registries.natives.native_methods(MyClass).each do |native|
  puts native.name
end

See Ruby API for the full Ruby API documentation.

See Also