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:
klass
: The class on which we are defining the method on.name
: The name of the method as a C string.func
: The implementation function (described in further detail below).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:
argc
: The number of arguments passed in.argv
: Pointer to C array of arguments.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:
self
: The object this method is called on.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.