Search code examples
crubyruby-c-extension

How can a Ruby C extension store a proc for later execution?


Goal: allow c extension to receive block/proc for delayed execution while retaining current execution context.

I have a method in c (exposed to ruby) that accepts a callback (via VALUE hash argument) or a block.

// For brevity, lets assume m_CBYO is setup to make a CBYO module available to ruby
extern VALUE m_CBYO;
VALUE CBYO_add_callback(VALUE callback)
{
    if (rb_block_given_p()) {
        callback = rb_block_proc();
    }

    if (NIL_P(callback)) {
        rb_raise(rb_eArgError, "either a block or callback proc is required");
    }

    // method is called here to add the callback proc to rb_callbacks
}
rb_define_module_function(m_CBYO, "add_callback", CBYO_add_callback, 1);

I have a struct I'm using to store these with some extra data:

struct rb_callback
{
    VALUE rb_cb;
    unsigned long long lastcall;
    struct rb_callback *next;
};
static struct rb_callback *rb_callbacks = NULL;

When it comes time (triggered by an epoll), I iterate over the callbacks and execute each callback:

rb_funcall(cb->rb_cb, rb_intern("call"), 0);

When this happens I am seeing that it successfully executes the ruby code in the callback, however, it is escaping the current execution context.

Example:

# From ruby including the above extension
CBYO.add_callback do
    puts "Hey now."
end

loop do
    puts "Waiting for signal..."
    sleep 1
end

When a signal is received (via epoll) I will see the following:

$> Waiting for signal...
$> Waiting for signal...
$> Hey now.
$> // process hangs
$> // Another signal occurs
$> [BUG] vm_call_cfunc - cfp consistency error

Sometimes, I can get more than one signal to process before the bug surfaces again.


Solution

  • I found the answer while investigating a similar issue.

    As it turns out, I too was trying to use native thread signals (with pthread_create) which are not supported with MRI.

    TLDR; the Ruby VM is not currently (at the time of writing) thread safe. Check out this nice write-up on Ruby Threading for a better overall understanding of how to work within these confines.

    You can use Ruby's native_thread_create(rb_thread_t *th) which will use pthread_create behind the scenes. There are some drawbacks that you can read about in the documentation above the method definition. You can then run the callback with Ruby's rb_thread_call_with_gvl method. Also, I haven't done it here, but it might be a good idea to create a wrapper method so you can use rb_protect to handle exceptions the callback may raise (otherwise they will be swallowed by the VM).

    VALUE execute_callback(VALUE callback)
    {
        return rb_funcall(callback, rb_intern("call"), 0);
    }
    
    // execute the callback when the thread receives signal
    rb_thread_call_with_gvl(execute_callback, data->callback);