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:
recv
: Receiver object that we are calling on.mid
: Method name as anID
symbol.n
: The number of arguments.- 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:
recv
: Receiver object that we are calling on.mid
: Method name as anID
symbol.argc
: The number of arguments.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:
recv
: Receiver object that we are calling on.mid
: Method name as anID
symbol.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:
kw_splat
: Whether to use the last argument ofargv
as keyword arguments. UseRB_PASS_KEYWORDS
to use the last argument ofargv
as keyword arguments, andRB_NO_KEYWORDS
otherwise (which will cause it to behave likerb_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:
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)
yielded_arg
: The first argument that is yielded to the block.callback_arg
: The value that is passed intodata2
during therb_block_call
call.argc
: The number of arguments yielded into the block (including the first argument inyielded_arg
).argv
: The C array of arguments yielded into the block (including the first argument inyielded_arg
).blockarg
: The block that is passed into this block. This can happen when the caller of this block usesProc#call
with a block.
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 theVALUE
type (remember,VALUE
is just anunsigned 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.