A Rubyist's Walk Along the C-side (Part 3): Calling 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 define Ruby methods in C extensions. In this article, we’ll explore how to call Ruby methods.

Calling methods

We’ve already seen this in the very first article, where we used rb_funcall to call Kernel#puts. rb_funcall accepts three required arguments and returns the return value of the Ruby method:

  1. recv: Receiver object that we are calling on.
  2. mid: Method name as an ID symbol.
  3. n: The number of arguments.
  4. The arguments we are passing into the Ruby method (where the number of these is specified by the previous argument n).
// Function prototype for rb_funcall
VALUE rb_funcall(VALUE recv, ID mid, int n, ...);
// Calling Kernel#puts with three arguments a, b, and c
rb_funcall(rb_mKernel, rb_intern("puts"), 3, a, b, c);

Calling methods with variable arguments

rb_funcall is fairly easy to use, but it has a caveat: we must know the number of arguments we are passing in ahead of time. If we want to pass a dynamic number of arguments, we can use the rb_funcallv function. This function accepts exactly four arguments and returns the return value of the Ruby method:

  1. recv: Receiver object that we are calling on.
  2. mid: Method name as an ID symbol.
  3. argc: The number of arguments.
  4. argv: Pointer to the list of arguments.
// Function prototype for rb_funcallv
VALUE rb_funcallv(VALUE recv, ID mid, int argc, const VALUE *argv);
// Calling Kernel#puts with three arguments in a C array
// allocated on the stack
VALUE argv[3] = { a, b, c };
rb_funcallv(rb_mKernel, rb_intern("puts"), 3, argv);

Alternatively, if we have a Ruby array, we can use rb_apply to call a method with the array splatted. This function accepts exactly three arguments and returns the return value of the Ruby method:

  1. recv: Receiver object that we are calling on.
  2. mid: Method name as an ID symbol.
  3. args: The Ruby array of arguments to be passed in splatted.
// Function prototype for rb_apply
VALUE rb_apply(VALUE recv, ID mid, VALUE args);
// Calling Kernel#puts with my_array splatted
// Assume my_array is a Ruby array
// We haven't seen how to create Ruby arrays yet
VALUE my_array = ...;
rb_apply(rb_mKernel, rb_intern("puts"), my_array);

Calling methods with keyword arguments

To call a method with keyword arguments, we can use rb_funcallv_kw. This is very similar to rb_funcall but with an extra argument at the end:

  1. kw_splat: 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 (which will cause it to behave like rb_funcallv).
// Function prototype for rb_funcallv_kw
VALUE rb_funcallv_kw(VALUE recv, ID mid, int argc,
                     const VALUE *argv, int kw_splat);
// Calling Kernel#puts with two positional arguments and
// keyword arguments
// Assume my_hash is a Ruby hash
// We haven't seen how to create Ruby hashes yet
VALUE my_hash = ...;
VALUE argv[3] = { a, b, my_hash };
rb_funcallv_kw(rb_mKernel, rb_intern("puts"), 3, argv,
               RB_PASS_KEYWORDS);

Calling methods with a block

To call a method with a block, we can use the rb_block_call function. The first four parameters are the same as rb_funcallv and has two additional parameters:

  1. bl_proc: The C function that implements the block. It must have signature like the following:
    VALUE block_callback(VALUE yielded_arg, VALUE callback_arg,
                         int argc, const VALUE *argv, VALUE blockarg)
    
    1. yielded_arg: The first argument that is yielded to the block.
    2. callback_arg: The value that is passed into data2 during the rb_block_call call.
    3. argc: The number of arguments yielded into the block (including the first argument in yielded_arg).
    4. argv: The C array of arguments yielded into the block (including the first argument in yielded_arg).
    5. blockarg: The block that is passed into this block. This can happen when the caller of this block uses Proc#call with a block.
  2. data2: Any data that we want to pass into our block. Unlike what we’ve seen so far, this is a case where we use the flexibility of the VALUE type (remember, VALUE is just an unsigned long) to store anything, not just a Ruby object. So what is passed here could be anything and does not have to be a Ruby object. For example, we could pass a pointer here (of course, with the appropriate casting so the compiler is happy).
// Function prototype for rb_block_call
VALUE rb_block_call(VALUE obj, ID mid, int argc, const VALUE *argv,
                    rb_block_call_func_t bl_proc, VALUE data2);
// Call Array#each on my_array with block
VALUE array_each_i(VALUE yielded_arg, VALUE callback_arg, int argc,
                   const VALUE *argv, VALUE blockarg) {
    // Block implementation goes here
}
rb_block_call(my_array, rb_intern("each"), 0, NULL,
              array_each_i, 0);

To call a method with both keyword arguments and a block, we can use rb_block_call_kw. Exploring this is left as an exercise for the reader.

Exercise for the reader

Here’s an exercise for you to practice everything you’ve learned up to now. Your task is to implement Array#puts_every_other that puts every other element in the array. Hint: you should use Array#each to implement it. The Ruby implementation looks like the following.

class Array
  def puts_every_other
    puts_curr = true
    each do |e|
      puts e if puts_curr
      puts_curr = !puts_curr
    end
  end
end

# Test cases
["one", "two", "three", "four", "five"].puts_every_other #=> one three five
["one", "two", "three", "four"].puts_every_other #=> one three
[].puts_every_other #=> empty output

After you have implemented your solution, you can compare it with the one I’ve implemented.

Conclusion

In this article, we took a more in-depth look at how to call Ruby methods using the C API, including passing a variable number of arguments to methods and calling methods with a block. In the next article, we’ll look at the primitive data types in Ruby.