A photo of Kim Burgestrand

Kim Burgestrand

Heartfelt software development. Sometimes other things.

Asynchronous callbacks in Ruby C extensions

~ 6 mins

WARNING: this post will be very technical and mention topics related to concurrent programming and thread-synchronization. Knowledge about mutexes and condition variables will be assumed.

Okay. Down to the nitty-gritty. You have this awesome (and imaginary) C library, “Library of Massive Fun And Overjoy”, and you can call upon it to do some work. Thing is, when LMFAO does some work it spawns a new thread, and that thread will call a callback-function that you supply.

Today, we are going to write a C extension for Ruby that allows our fellow Ruby programmers to use LMFAO without knowing an ounce of C; and to do that you’ll need the library’s source, so here it is:

Writing the Ruby bindings

This is not a basic guide in writing Ruby C extensions, so if you’ve never written one yourself this particular part might confuse you slightly. Don’t fret! Go forth slowly, Google the things you don’t understand and you’ll understand in no time.

Now, where were we? Oh, yes, Ruby bindings for LMFAO! We will be supporting an API similar to this:

require 'lmfao'

result = LMFAO::call("some ruby object") do |data|
  puts "LMFAO callback called"
  data.upcase # handle the data from the callback somehow
end

puts "Result: #{result}"

I’ve taken the liberty of writing most of it for you. It is available in a GitHub repository: Burgestrand/Library-of-Massive-Fun-And-Overjoy. Once you run LMFAO (rake default) you’ll notice the tests don’t pass: the callback returns false.

You might not realize it yet, but we have a major problem here. Inside lmfao_callback we do not hold the GIL, so we cannot call the Ruby C API safely. rb_thread_call_with_gvl looks promising at first, but we cannot use it as the the current thread was not created by Ruby. So, what do we do?

Solving the communication problem

As we are not allowed to call Ruby from within our lmfao_callback, we need a workaround. Now, this is the tricky part, so stay close with me.

Once the callback fires, we need to tell Ruby that it has fired, and we also need to communicate which parameters it was given. As we cannot call Ruby directly, we need to put the parameters in a location that Ruby can access. Additionally, the callback must wait for a return value from Ruby before it can return said value to its’ caller.

Now, since we want to listen for notifications from our callbacks we must wait for them to arrive. Waiting in the main thread is a bad idea, as it would mean we did a lot of waiting and very little work. What we can do, is use a separate Ruby thread that’ll wait for us.

So. Quick recap:

Whew! A lot of things to keep track of, but this is a high-level view of what we need to do. Lets get to work!

The Ruby Event thread

First off, we’ll need a designated event thread. As previously mentioned, this thread will do nothing but wait for callbacks to happen; and when they do, it will dispatch off to a callback handler.

As I’ve already explained the high-level view of how this should work, I’m just going to give you the code. I’ve done my best to explain what is being done and why for each function and struct member in their comments. Do read it now, it’ll help understanding what’s coming next!

Calling out to Ruby

You’ve probably noticed both LMFAO_handle_callback and lmfao_callback are empty functions. We’ll fill them in in this chapter, but they require more intimate discussion in comparison to the ruby event thread.

We’ll talk about lmfao_callback first, the simpler one of the two functions. This function should dump its’ data in the global queue, notify the event thread, and wait for the return value. Only two things in this code should ever change between different callbacks: the parameter dumping and type casting the return value.

As the parameters become more complex, so does parameter dumping. I’ve thought about making the data field in the callback_t struct a linked list instead. Each node would contain the data type, pointer to the value and finally a pointer to the next node. I think I’ll leave this an exercise for you!

Handling the callback

Now to look at LMFAO_handle_callback. In LMFAO, the callback data is just a Ruby array containing a proc and the parameters to give it. We call it, and simply return the result to the callback (lines #146 to #153).

In practice, it is never this simple. You need to convert the callback data to Ruby data, figure out which Ruby handler to invoke, and finally convert the result back to pure C data that the callback function can return.

If you have a small amount of callbacks, you could handle these conversions for a few simple data types (or in LMFAO’s case, no conversion whatsoever). If you want to handle the general case however, it quickly gets complicated. Ruby FFI has an implementation of this in its callback_with_gvl and invoke_callback functions.

Summary

We’ve written a C threaded (albeit small) library, a C extension binding it and a whole lot of concurrency-related code. If you are still confused about all this, you are not alone; concurrency is hard!

If you have suggestions, ideas or any other feedback you’re welcome to contact me. You’ll find my contact details on the About Me page.

Final note: do keep in mind that this entire article is a proof-of-concept. My hope is that if you ever find yourself needing to do this (shudder), you now have a better idea of how.

rss facebook twitter github youtube mail spotify instagram linkedin google pinterest medium rubygems