A Rubyist's Walk Along the C-side (Part 8): Exceptions & Error Handling

This is an article in a multi-part series called “A Rubyist’s Walk Along the C-side”

In the previous article, we saw TypedData objects, a type unique to the Ruby C API. In this article, we’ll look at how to raise errors and rescue from errors.

Raising errors

We’ve already seen one way to raise errors in part 4. The Check_Type function guards that a variable is of a particular Ruby type and will raise a TypeError otherwise.

The Ruby C API provides builtin errors as global variables. The builtin errors are prefixed with rb_e (e.g. rb_eRuntimeError for the RuntimeError exception). This was covered in more detail in part 4.

rb_raise

The simplest way to raise an error is to use rb_raise. This function accepts the error class and an error message, it then creates the error object internally and raises it. This function is roughly equal to calling raise in Ruby with two arguments, such as:

# Raising error with a custom message
raise RuntimeError, "my error message"

rb_raise accepts two mandatory arguments and has variable arguments and doesn’t return anything:

  1. exc: Class of the error to raise.
  2. fmt: C string for the error message. This string accepts C string formatting (just like the C function printf).
  3. ...: Variable arguments for the fmt string.
// Function prototype for rb_raise
void rb_raise(VALUE exc, const char *fmt, ...);

// Example of raising a RuntimeError
rb_raise(rb_eRuntimeError, "my error message");
// Example of raising a RuntimeError with formatting
int my_number = 42;
rb_raise(rb_eRuntimeError, "this is a number: %d", my_number);

rb_exc_raise

There are two ways to use the rb_exc_raise function:

  1. If we don’t want a custom error message like in rb_raise, the error class can be passed here and it will use the default error message.
  2. We have an error object, and just want to raise it.

This is roughly equal to calling raise in Ruby with one argument, such as:

# Raising an error with the default message
raise RuntimeError
# Raising an error object
raise MyCustomError.new(foo, bar)

rb_exc_raise accepts one argument and doesn’t return anything:

  1. mesg: Error class or error object to raise.
// Function prototype for rb_exc_raise
void rb_exc_raise(VALUE mesg);

// Example of raising a RuntimeError with default error message
rb_exc_raise(rb_eRuntimeError);

Rescuing from errors

Now that we’ve seen how to raise errors, let’s look at how to rescue from errors.

In Ruby, we need two blocks: a begin block that contains code that could raise an error, and a rescue block that handles error cases. In the C API, it works similarly! We need two functions: one that contains code that could raise an error, and one that handles errors.

We can use rb_rescue2 to handle errors. rb_rescue2 accepts four mandatory arguments and has variable arguments. It returns the return value of the b_proc function if there is no error raised. However, if one of the handled errors is raised, then it will return the return value of r_proc. If an error outside of the handled errors is raised, r_proc will not be called and the error will be propagated. The parameters of rb_rescue2 are as follows:

  1. b_proc: Pointer to a C function that could raise an error (equivalent to the begin block in Ruby). This function must have the following signature:
     VALUE try_my_function(VALUE args);
    

    Where args is the value passed into data1. The return value of this function is the value to be returned by rb_rescue2 when no errors are raised.

  2. data1: Data to pass into b_proc. Even though this parameter is of the VALUE type, it does not have to be a Ruby object. VALUE type is guaranteed to be at least the size of a C pointer, so we can pass in a pointer to a region of memory (e.g. stack allocated or malloc‘d memory).
  3. r_proc: Pointer to a C function that is run when one of the handled errors is raised. The list of handled errors is passed into the variable arguments (this is discussed in detail below). This function must have the following signature:
     VALUE rescue_my_function(VALUE args, VALUE error);
    

    Where args is the value passed into data2, and error is the Ruby error that was raised in b_proc. The return value of this function is the value to be returned by rb_rescue2 when there is an error raised in b_proc.

  4. data2: Data to pass into r_proc. Just like data1, even though this parameter is of the VALUE type, it does not have to be a Ruby object.
  5. ...: Variable arguments of all error types to handle. The last element must be NULL (i.e. the value 0).
// Function prototype for rb_rescue2
VALUE rb_rescue2(VALUE (* b_proc) (VALUE), VALUE data1,
                 VALUE (* r_proc) (VALUE, VALUE), VALUE data2, ...);

// Example which rescues from IOError and ThreadError
VALUE try_my_function(VALUE args)
{
    /* Do something dangerous */
}

VALUE rescue_my_function(VALUE args, VALUE error)
{
    /* Rescue from errors */
}

rb_rescue2(try_my_function, (VALUE)try_data_ptr,
           rescue_my_function, (VALUE)rescue_data_ptr,
           rb_eIOError, rb_eThreadError, (VALUE)0);

Rescuing from all errors

You might be curious as to why the function above is called rb_rescue2. That’s right, it’s because there’s a rb_rescue! rb_rescue is like a Ruby rescue block without specifying the errors to rescue from (i.e. rescue from all descendants of StandardError). It’s used very similarly to rb_rescue2, except it doesn’t have variable arguments at the end. In fact, the implementation is very simple!

VALUE
rb_rescue(VALUE (* b_proc)(VALUE), VALUE data1,
          VALUE (* r_proc)(VALUE, VALUE), VALUE data2)
{
    return rb_rescue2(b_proc, data1, r_proc, data2, rb_eStandardError,
		      (VALUE)0);
}

rb_rescue accepts four arguments, and just like rb_rescue2, it returns the return value of the b_proc function if there is no error raised, and the return value of r_proc if any descendant of StandardError is raised. See the notes above for rb_rescue2 for a more detailed explanation, a shortened version is presented below as a refresher.

  1. b_proc: Pointer to a C function that could raise an error.
  2. data1: Data to pass into b_proc.
  3. r_proc: Pointer to a C function that is run when one of the handled errors is raised.
  4. data2: Data to pass into r_proc.
// Function prototype for rb_rescue
VALUE b_rescue(VALUE (* b_proc)(VALUE), VALUE data1,
               VALUE (* r_proc)(VALUE, VALUE), VALUE data2);

// Example
VALUE try_my_function(VALUE args)
{
    /* Do something dangerous */
}

VALUE rescue_my_function(VALUE args, VALUE error)
{
    /* Rescue from errors */
}

rb_rescue(try_my_function, (VALUE)try_data_ptr,
          rescue_my_function, (VALUE)rescue_data_ptr);

Ensure

To implement Ruby’s ensure through the C API, we can use rb_ensure. This works very similarly to rb_rescue. rb_ensure accepts four arguments, returns the value of b_proc if no error is raised, and will raise the error if one is raised:

  1. b_proc: Pointer to a C function that could raise an error (equivalent to the begin block in Ruby). This function must have the following signature:
     VALUE try_my_function(VALUE args);
    

    Where args is the value passed into data1. The return value of this function is the value to be returned by rb_ensure when no errors are raised.

  2. data1: Data to pass into b_proc. Even though this parameter is of the VALUE type, it does not have to be a Ruby object. VALUE type is guaranteed to be at least the size of a C pointer, so we can pass in a pointer to a region of memory (e.g. stack allocated or malloc‘d memory).
  3. e_proc: Pointer to a C function that is run after b_proc regardless of whether an error is raised or not. This function must have the following signature:
     VALUE ensure_my_function(VALUE args);
    

    Where args is the value passed into data2. The return value is discarded (we can just return some dummy value like Qnil).

  4. data2: Data to pass into e_proc. Just like data1, even though this parameter is of the VALUE type, it does not have to be a Ruby object.
// Function prototype for rb_ensure
VALUE rb_ensure(VALUE (*b_proc)(VALUE), VALUE data1,
                VALUE (*e_proc)(VALUE), VALUE data2);

// Example
VALUE try_my_function(VALUE args)
{
    /* Do something dangerous */
}

VALUE ensure_my_function(VALUE args)
{
    /* Clean up resources */
}

rb_ensure(try_my_function, (VALUE)try_data_ptr,
          ensure_my_function, (VALUE)ensure_data_ptr);

How to have both a rescue and ensure block?

In Ruby, we could do the following:

begin
  # Do something dangerous
rescue
  # Rescue from errors
ensure
  # Clean up resources
end

In the C API, there isn’t a very nice way to do it. We just have to nest a rb_rescue inside a rb_ensure. For example:

VALUE try_my_function(VALUE args)
{
    /* Do something dangerous */
}

VALUE rescue_my_function(VALUE args, VALUE error)
{
    /* Rescue from errors */
}

VALUE try_and_rescue_my_function(VALUE args)
{
    rb_rescue(try_my_function, args, rescue_my_function, args);
}

VALUE ensure_my_function(VALUE args)
{
    /* Clean up resources */
}

VALUE my_function()
{
    void *try_data_ptr = /* ... */;
    void *ensure_data_ptr = /* ... */;
    rb_ensure(try_and_rescue_my_function, (VALUE)try_data_ptr,
              ensure_my_function, (VALUE)ensure_data_ptr);
}

Conclusion

In this article, we looked at how to raise errors, rescue from errors, and wrap functions in ensure blocks in the Ruby C API. In the next article, you’ll work on a small project to practice your C extension skills!