A Rubyist's Walk Along the C-side (Part 2): Defining Methods

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

In the previous article, we saw how to set up and write our very first C extension. In this article, we’ll explore how to define Ruby methods.

Defining methods

The simplest way to define a method is to use the rb_define_method class of functions. These functions require four arguments:

  1. klass: The class on which we are defining the method on.
  2. name: The name of the method as a C string.
  3. func: The implementation function (described in further detail below).
  4. argc: The number of arguments the Ruby method accepts. Pass in -1 for a variable number of arguments in a C array, and -2 for a variable number of arguments in a Ruby array.

The function signature for rb_define_method looks like the following.

void rb_define_method(VALUE klass, const char *name,
                      VALUE (*func)(ANYARGS), int argc);

For example,

// Defines method `my_method` in the Integer class with 2 arguments
rb_define_method(rb_cInteger, "my_method", my_method, 2);

In addition to rb_define_method, we can also use the following functions to define methods (these are used in the same way as rb_define_method):

  • rb_define_protected_method: Defines a protected method.
  • rb_define_private_method: Defines a private method.
  • rb_define_singleton_method: Defines a method on the singleton class.

Implementing methods with a fixed number of arguments

To define an implementation function for a Ruby method with a known number of arguments, we can directly specify the arguments in the function. The first argument is always self, which is the object the method is being called on. The function must also have a return value, which is the value returned by the Ruby method. All of these are of type VALUE (which, if you recall from part 1, is the type used to represent Ruby objects in C). An example is shown below.

VALUE my_method(VALUE self, VALUE arg1, VALUE arg2)
{
    // Your implementation goes here
}

Implementing methods with a variable number of arguments

Defining methods with a variable number of arguments is a little more tricky. There are two ways to do this, using a C array and the other using a Ruby array (which one to use is determined by whether -1 or -2 was passed to argc in rb_define_method).

Option 1: C array

To get the arguments in a C array, we must pass -1 to argc when calling rb_define_method. The function will accept three arguments and return a VALUE that is returned by the Ruby method. The arguments are as follows:

  1. argc: The number of arguments passed in.
  2. argv: Pointer to C array of arguments.
  3. self: The object this method is called on.
VALUE my_method(int argc, VALUE* argv, VALUE self)
{
    // Your implementation goes here
}

Option 2: Ruby array

To get the arguments in a Ruby array, we must pass -2 to argc when calling rb_define_method. The function will accept two arguments and return a VALUE that is returned by the Ruby method. The arguments are as follows:

  1. self: The object this method is called on.
  2. args: The arguments in a Ruby array.

We haven’t discussed how to read a Ruby array in C which will be covered in a later article (of course, a perfectly valid way is to just call the Array#[] method, but there’s a more efficient way).

VALUE my_method(VALUE self, VALUE args)
{
    // Your implementation goes here
}

Blocks

To yield a block passed into our method, we can use the rb_yield_values function inside our method. rb_yield_values will accept the number of arguments, followed by a list of arguments, and will return the return value of the block.

// Yielding block with no arguments
VALUE block_ret = rb_yield_values(0);

// Yielding block with two arguments
VALUE block_ret = rb_yield_values(2, val1, val2);

Checking for blocks

Sometimes we don’t always want to yield the block, but only when it’s present. Just like how we can use Kernel#block_given? in Ruby code, we can use the corresponding rb_block_given_p function in C.

if (rb_block_given_p()) {
    // Block given
} else {
    // No block given
}

Requiring blocks

If we call yield in Ruby or rb_yield_values without a block present, we get a LocalJumpError: no block given. This is usually fine, but sometimes we want to be more defensive and check that a block is passed in earlier in the code, especially if we do mutations before we call yield and the exception might cause us to enter a bad state. To check that the block exists, there’s a handy function rb_need_block that will raise a LocalJumpError if no block is passed in. The implementation is fairly simple, so you should be able to understand more or less what it’s doing with your current knowledge!

Putting it all together

You can find the accompanying source code in the GitHub repository peterzhu2118/ruby-c-ext-code

Here we see all the ways to define Ruby methods. We define a helper function kernel_puts that calls Kernel#puts on the Ruby object passed in. We also define Object#my_fixed_args_method, Object#my_var_args_c_array_method, and Object#my_var_args_rb_array_method methods that show the three ways of defining Ruby methods using C. These methods prints self on the first line, followed by the arguments on each line. We also define a method Object#my_method_with_required_block that demos how to yield a block and capture the return value of the block. You can see a demo of these methods in methods.rb.

#include <ruby.h>

static ID id_puts;

static void kernel_puts(VALUE val)
{
    rb_funcall(rb_mKernel, id_puts, 1, val);
}

static VALUE my_fixed_args_method(VALUE self, VALUE arg1, VALUE arg2)
{
    kernel_puts(self);
    kernel_puts(arg1);
    kernel_puts(arg2);

    return Qnil;
}

static VALUE my_var_args_c_array_method(int argc, VALUE* argv, VALUE self)
{
    kernel_puts(self);

    for (int i = 0; i < argc; i++) {
        kernel_puts(argv[i]);
    }

    return Qnil;
}

static VALUE my_var_args_rb_array_method(VALUE self, VALUE args)
{
    kernel_puts(self);
    kernel_puts(args);

    return Qnil;
}

static VALUE my_method_with_required_block(VALUE self)
{
    VALUE block_ret = rb_yield_values(0);
    kernel_puts(block_ret);

    return Qnil;
}

void Init_methods(void)
{
    id_puts = rb_intern("puts");

    rb_define_method(rb_cObject, "my_fixed_args_method", my_fixed_args_method, 2);
    rb_define_method(rb_cObject, "my_var_args_c_array_method", my_var_args_c_array_method, -1);
    rb_define_method(rb_cObject, "my_var_args_rb_array_method", my_var_args_rb_array_method, -2);

    rb_define_method(rb_cObject, "my_method_with_required_block", my_method_with_required_block, 0);
}

Conclusion

In this article, we saw how to implement Ruby methods using the C API. We saw the basic way of defining methods with a static number of parameters and the two ways to define methods with a dynamic number of parameters. We also saw how to handle blocks in methods. In the next article, we’ll take a more in-depth look at how to call Ruby methods using the C API.