Metaprogramming in the Ruby C API: Part One: Blocks

This is the first in a series of articles on Metaprogramming in the C API. This series will explain how to implement dynamic method definition, eigenclass and metaclass manipulation, and ultimately DSL construction in pure C. This particular article will discuss Ruby blocks, and how we use them in C.

Blocks

In Ruby, blocks hold a central role in Metaprogramming and this is equally true in the C API. Blocks can be passed to methods both implictly:

 def my_method     if block_given? then         yield     else         raise ArgumentError, "a block is required"     end end 

And explictly:

 def my_method(&block)     if block then         block.call     else         raise ArgumentError, "a block is required"     end end 

Implicit Blocks

We will examine the implicit case first from the C perspective. Here is the corresponding code in C:

 static VALUE my_method(VALUE self) { if(rb_block_given_p()) rb_yield(Qnil); else rb_raise(rb_eArgError, "a block is required"); return Qnil; } 

Looking over the C and the Ruby code it should strike you how similar they are and it should be immediately obvious that rb_block_given_p() and rb_yield() are the C counterparts to Ruby’s block_given? and yield. The prototypes for the two C functions are:

 VALUE rb_block_given_p(); VALUE rb_yield(VALUE argv); 

The return values for rb_block_given_p() and rb_yield() are identical to their Ruby equivalents; rb_block_given_p() returns a boolean true or false (Qtrue and Qfalse in the C API) and rb_yield() returns the value returned by the block. No surprises here.

Now let’s look at another example of implicit block passing, converting an implicit block into a proc:

 def my_method     if block_given? then         p = Proc.new p.call     else         raise ArgumentError, "a block is required"     end end 

Here is the corresponding C code:

 static VALUE my_method(VALUE self) {     VALUE p; if(rb_block_given_p()) p = rb_block_proc(); rb_funcall(p, rb_intern("call"), 0); else rb_raise(rb_eArgError, "a block is required"); return Qnil; } 

Similar to Proc.new the C function rb_block_proc() converts the implicitly passed block into a proc. The rb_funcall() function (which you should be familiar with) then executes the proc by invoking its ‘call’ method. The prototype for rb_block_proc() is as follows:

 VALUE rb_block_proc(); 

Where the return value is the newly created proc.

One more function that exists only in the C API but is nonetheless very useful is rb_need_block(), it throws a LocalJumpError exception if no block is present, use it as follows:

 static VALUE my_method(VALUE self) {     rb_need_block();     rb_yield(Qnil);     return Qnil; } 

Here is its prototype:

 void rb_need_block(); 

Explicit Blocks

To refresh here is the Ruby code for explicit block passing:

 def my_method(&block)     if block then         block.call     else         raise ArgumentError, "a block is required"     end end 

And here is the corresponding C code:

 static VALUE my_method(int argc, VALUE *argv, VALUE self) {     VALUE block = Qnil;     rb_scan_args(argc, argv, "0&", &block); if(RTEST(block))     rb_funcall(block, rb_intern("call"),0); else rb_raise(rb_eArgError, "a block is required");     return Qnil; } 

From above, C doesn’t have any native way of defining a parameter as a ‘block parameter’ so to achieve the equivalent we must use rb_scan_args() and a variable length parameter list.

The “0&” parameter to rb_scan_args() indicates we have no (0) ordinary parameters and one block (&) parameter. The &block  tells rb_scan_args() to save the block  in the variable called ‘block’. And, as in previous examples, rb_funcall() invokes the block (now really a proc).

Caveats

Blocks in C do not behave entirely like their Ruby counterparts. Take the following Ruby code:

 def my_method    yield instance_eval "puts 'heya'" end 

and its C “equivalent”:

 static VALUE my_method(VALUE self) { VALUE cmd = rb_str_new2("puts 'heya'"); rb_yield(Qnil); rb_obj_instance_eval(1, &cmd, self);     return Qnil; } 

When we invoke the Ruby version of my_method on receiver ‘obj’ we get:

 obj.my_method { puts "hello" } output: hello heya 

But for the C version we get:

 obj.my_method { puts "hello" } output: hello ArgumentError: wrong number of arguments (1 for 0) from (irb):3:in `my_method' from (irb):3 

Why the difference in behaviour? The error is due to the fact that in the C API blocks passed to methods are still ‘live’ and are automatically passed-on to other methods that can take blocks.

So, the line that errors in C appears to Ruby as:

 instance_eval("puts 'heya'") { puts "hello" } 

Which is an error: instance_eval can take either a String or a block, but not both.

So how do we get the behaviour we want in C?

 static VALUE my_method(VALUE self) { VALUE cmd = rb_str_new2("puts 'heya'"); rb_yield(Qnil); rb_funcall(self, rb_intern("instance_eval"), 1, cmd); return Qnil; } 

It is important to understand this difference as it can be the source of many headaches and frustrations.

Summary

The intention of this article was to provide some groundwork for the more advanced Metaprogramming articles to come. Although not technically ‘Metaprogramming’ blocks are an integral part of the field and a thorough understanding of them is necessary before moving onto the more advanced material.

In the next article we’ll learn about dynamic method definitions and singletons.

For full prototypes with explanations of some (but not all) of the functions presented here check out the pickaxe.

8 Responses to “Metaprogramming in the Ruby C API: Part One: Blocks”

  1. Garry's avatar

    I was wondering how to yield to a block in a Ruby C extension, and now I know how! Thanks, it’s much easier than I thought it’d be. 🙂

  2. Lya's avatar

    impressive!! 😉

  3. Tamas Foldi's avatar

    Thanks, this help’d a lot

  4. Eric Hodel's avatar

    Why do you use puts instead of rb_raise?

    rb_raise(rb_eArgError, “a block is required”);

    Is correct behavior.

Trackbacks

Leave a comment

Design a site like this with WordPress.com
Get started