A Rubyist's Walk Along the C-side (Part 6): Classes & Object Oriented Programming

This is an article in a multi-part series called “A Rubyist’s Walk Along the C-side”

In the previous article, we saw ways to read and write to various types of variables through the C API. In this article, we’ll look at how to define classes and modules and create instances of classes in the Ruby C API.

Defining classes

Defining top-level classes

To define a top-level class, we can use the rb_define_class function. It will either return the existing class if one with the same name is already defined, or create one if it does not exist. However, if a class with the same name is already defined, it will compare the superclass of that class to the value of the super argument. If the two does not match, it will raise a TypeError: superclass mismatch exception. rb_define_class_under accepts two arguments and returns the newly created class (or the existing one if it exists):

  1. name: C string of the class name.
  2. super: The superclass of the class (must be a class and not nil). All classes have a superclass, we can use the rb_cObject C constant to refer to the Object class.
// Function prototype for rb_define_class
VALUE rb_define_class(const char *name, VALUE super);
// Creating class Foo with superclass Object
VALUE cFoo = rb_define_class("Foo", rb_cObject);

Defining namespaced classes

To define a namespaced class, we can use the rb_define_class_under function. It works very similarly to the rb_define_class discussed above. It will either return the existing class if one is already defined or create one otherwise. It will also raise a TypeError: superclass mismatch exception if the existing class’s superclass does not match the super argument passed in. rb_define_class_under accepts three arguments and returns the newly created class (or the existing one if it exists):

  1. outer: The outer class or module to define this class in.
  2. name: C string of the class name.
  3. super: The superclass of the class (must be a class and not nil).
// Function prototype for rb_define_class_under
VALUE rb_define_class_under(VALUE outer, const char *name, VALUE super);
// Creating classes Foo and Foo::Bar
VALUE cFoo = rb_define_class("Foo", rb_cObject);
VALUE cFooBar = rb_define_class_under(cFoo, "Bar", rb_cObject);

Defining modules

Defining modules is very similar to classes, just without the last super argument (since modules don’t have superclasses).

Defining top-level modules

To define a top-level module, we can use the rb_define_module function. Once again, just like rb_define_class, it will return the existing module if one is already defined, or create one otherwise. rb_define_module accepts one argument and returns the newly created module (or the existing one if it exists):

  1. name: C string of the module name.
// Function prototype for rb_define_module
VALUE rb_define_module(const char *name);
// Creating module Foo
VALUE mFoo = rb_define_module("Foo");

Defining namespaced modules

To define a namespaced module, we can use the rb_define_module_under function. At this point, you probably know what I’m going to say next, but I’ll say it again. Just like all the class/module defining functions we’ve seen so far, it will return the existing module if one is already defined, or create one otherwise. rb_define_module_under accepts two arguments and returns the newly created module (or the existing one if it exists):

  1. outer: The outer class or module to define this class in.
  2. name: C string of the module name.
// Function prototype for rb_define_module_under
VALUE rb_define_module_under(VALUE outer, const char *name);
// Creating modules Foo and Foo::Bar
VALUE mFoo = rb_define_module("Foo");
VALUE mFooBar = rb_define_module_under(mFoo, "Bar");

Including modules in classes

To include a module in another class/module, we can use the rb_include_module function. This function accepts two arguments and does not return anything:

  1. klass: The class or module to include the module into.
  2. module: The module to include into klass.
// Function prototype for rb_include_module
void rb_include_module(VALUE klass, VALUE module);
// Include module mFoo into class cBar
rb_include_module(cBar, mFoo);

Extending modules to classes

To extend an object with a module, we can use the rb_extend_object function. This function accepts two arguments and does not return anything:

  1. obj: The object to extend the module into.
  2. module: The module to extend into obj.
// Function prototype for rb_extend_object
void rb_extend_object(VALUE obj, VALUE module);
// Extend module mFoo into class cBar
rb_extend_object(cBar, mFoo);

Since extend is just including into the singleton class, we can read the implementation of rb_extend_object and understand what it’s doing!

Creating objects

To create an instance of a class, we can use the rb_class_new_instance and rb_class_new_instance_kw functions. The only difference between these two functions is that rb_class_new_instance_kw has one extra parameter at the end that allows for keyword arguments. rb_class_new_instance accepts three arguments, rb_class_new_instance_kw accepts four arguments, and both return the newly created object:

  1. argc: The number of arguments.
  2. argv: Pointer to the list of arguments.
  3. klass: The class to create an instance of.
  4. kw_splat (only for rb_class_new_instance_kw): Whether to use the last argument of argv as keyword arguments. Use RB_PASS_KEYWORDS to use the last argument of argv as keyword arguments, and RB_NO_KEYWORDS otherwise. See rb_funcall_kw for more details.
// Function prototypes
VALUE rb_class_new_instance(int argc, const VALUE *argv,
                            VALUE klass);
VALUE rb_class_new_instance_kw(int argc, const VALUE *argv,
                               VALUE klass, int kw_splat);
// Create a new instance of my_class with
// 3 arguments a, b, c
VALUE args[3] = { a, b, c };
VALUE obj = rb_class_new_instance(3, args, my_class);

Conclusion

In this article, we looked at a way to define classes and modules, including and extending modules, and creating instances of classes. In the next article, we’ll look at how to use and define TypedData objects.