Wednesday, January 12, 2011

Writing Ruby Extensions in C - Part 6, Catch/Throw

This is the sixth in my series of posts about writing ruby extensions in C. The first post talked about the basic structure of a project, including how to set up building. The second post talked about generating documentation. The third post talked about initializing the module and setting up classes. The fourth post talked about types and return values. The fifth post focused on creating and handling exceptions. This post will talk about ruby catch and throw blocks.

Catch/Throw

In ruby, raising exceptions is used to transfer control out of a block of code when something goes wrong. Ruby has a second mechanism for transferring control to blocks called catch/throw. Any ruby block can be labelled via catch(), and then any line of code within that block can throw() to terminate the rest of the block. This also works with nested catch/throw blocks so an inner nested throw could throw all the way back out to the outer block. Essentially, they are a fancy goto mechanim; see [1] for some examples. How can we catch and throw from within our C extension module? Like exceptions, we accomplish this through callbacks.

To set up a catch in a C extension, the rb_catch() function is used. rb_catch() takes 3 parameters: the first parameter is the name of the catch block, the second parameter is the name of the callback to invoke in block context, and the third parameter is data to be passed to the callback. As may be expected, the callback function must take a single VALUE parameter in and return a VALUE.

To return to a catch point in a C extension, the rb_throw() function is used. rb_throw() takes two parameters: the name of the catch block to return to, and the return value (which can be any valid ruby object, including Qnil). If rb_throw() is executed, control is returned from the point of the rb_throw() to the end of the rb_catch() block, and execution continues from there.

An example can demonstrate much of this. First let's look at the C code to implement an example catch/throw:

 1) static VALUE m_example;
 2)
 3) static VALUE catch_cb(VALUE val, VALUE args, VALUE self) {
 4)     rb_yield(args);
 5)     return Qnil;
 6) }
 7)
 8) static VALUE example_method(VALUE klass) {
 9)     VALUE res;
10)
11)     if (!rb_block_given_p())
12)         rb_raise(rb_eStandardError, "Expected a block");
13)
14)     res = rb_catch("catchpoint", catch_cb, rb_str_new2("val"));
15)     if (TYPE(res) != T_FIXNUM)
16)         rb_throw("catchpoint", Qnil);
17)
18)     return res;
19) }
20)
21) void Init_example() {
22)     m_example = rb_define_module("Example");
23)
24)     rb_define_module_function(m_example, "method",
25)                               example_method, 0);
26) }
Lines 21 through 26 set up the extension module, as described elsewhere.

Lines 8 through 19 implement the module function "method". Line 11 checks if a block is given; if not, an exception is raised on line 12. Line 14 sets up an rb_catch() named "catchpoint". The callback catch_cb() will be executed, and a new string of "val" will be passed into the callback. Lines 3 through 6 implement the callback; the value is yielded to the block initially passed into "method", and a nil is returned (which is ignored). Line 15 checks the return value from the block; if it is not a number, then line 16 does an rb_throw() to abort the entire block (with control passing to the line of ruby code after the Example::method call). If the value from the block is a number, then it is returned at line 18. Note that this particular sequence of calls is contrived, since the value returned from the block is just returned to the caller. Still, I think it is a good example of what can be done with rb_catch() and rb_throw().

Now let's look at some example ruby code that might utilize the above code:

require 'example'

# if the method were to be called like this, an exception would be
# raised since no block is given
# retval = Example::method

# if the method were to be called like this, an exception would be
# raised since the return value from the block is not a number
# retval = Example::method {|input|
#     "hello"
# }

# this works properly, since the return value is a number
retval = Example::method {|input|
    puts "Input is #{input}"
    6
}

[1] http://ruby-doc.org/docs/ProgrammingRuby/html/tut_exceptions.html

No comments:

Post a Comment