123456789_123456789_123456789_123456789_123456789_

Let’s assume we have a C API that allows for callbacks by taking a function pointer argument:

typedef void completion_function(char *buffer, long count, unsigned char code);
int do_work(char *buffer, completion_function *);

FFI supports the mapping of Ruby closures (Proc, lambda) to C function pointers, and it also supports passing in a function pointer that points directly to another C function.

Proc callback

The simplest way to define a callback is by using a Proc to create an anonymous function. In the example given below, the callback is assigned to a constant Callback so that we never need worry about the garbage collector removing it.

module LibWrap
  extend FFI::Library
  ffi_lib "some_lib.so"
  callback :completion_function, [:pointer, :long, :uint8], :void
  attach_function :do_work, [:completion_function], :int

  Callback = Proc.new do |buf_ptr, count, code|
    # finish up
  end
end

LibWrap.do_work(LibWrap::Callback)

Upon execution of the callback by the C API, the FFI library unwraps the Proc to see if an FFI::Function has been allocated for it. If it finds one, it invokes the FFI::Function immediately. If no FFI::Function is found, it allocates an FFI::Function and invokes it.

FFI::Function

You can save a little work and gain a little flexibility by defining your callback as an FFI::Function directly. Check the rdoc page for a complete description.

module LibWrap
  extend FFI::Library
  ffi_lib "some_lib.so"
  attach_function :do_work, [:pointer], :int

  Callback = FFI::Function.new(:void, [:pointer, :long, :uint8]) do |buf_ptr, count, code|
    # finish up
  end
end

LibWrap.do_work(LibWrap::Callback)

C callback

There may be situations where the callback function is another C function in the library. There are two ways to get a Ruby object representing the C function so that it can be passed as a function pointer argument:

module LibWrap
  extend FFI::Library
  ffi_lib "some_lib.so"
  attach_function :do_work, [:pointer], :int

  Completer = attach_function :completer, [:pointer, :long, :uint8], :void
end

LibWrap.do_work(LibWrap::Completer)

LibWrap.do_work(LibWrap.class_variable_get(:@@completer))

LibWrap.do_work(LibWrap.ffi_libraries[0].find_function('completer'))

GIL

MRI/CRuby implementation uses a global lock called GIL or GVL. It is locked whenever Ruby code is executed. It needs to be released to allow true parallel processing. But if the GIL is released, no Ruby code can be executed.

By default all calls to C functions keep the GIL locked and therefore block other Ruby threads to be executed. This is no problem, if the function is fast and doesn’t wait for external resources. However if the function waits for IO or does some extensive computation, it’s desirable that other Ruby threads continue to run.

Releasing the GIL will allow the Ruby runtime to (potentially) schedule another thread to run while the C function is still running. Luckily, FFI::Function allows us to optionally release the GIL by marking the callback as blocking: true. This setting only affects Ruby-to-native calls; it has no effect for native-to-Ruby calls.

module LibWrap
  extend FFI::Library
  ffi_lib "some_lib.so"

  attach_function :long_running_function, [], :int, blocking: true
end

Looking at the code path for both kinds of setup can shed some light on what’s happening under the covers.

FFI::Function, blocking: false, Ruby to native method call

Ruby ->
  FFI stub for parameter conversion ->  
  call native function ->
  FFI stub for result conversion ->
Ruby

FFI::Function, blocking: true, Ruby to native method call

Ruby -> 
  FFI stub for parameter conversion -> 
  release GIL ->
  call native function -> 
  reacquire GIL ->
  FFI stub for result conversion ->
Ruby

Callback Proc

Callback Procs always follow this code path.

Ruby -> FFI callback stub ->
  if thread.has_gil?
    convert parameters to Ruby
    call Ruby
    convert results to native

  elsif thread.is_ruby_thread?
    acquire GIL
    convert parameters to Ruby
    call Ruby
    convert results to native
    release GIL

  else # not a Ruby-owned thread
    start a new ruby thread
    bundle up FFI data and pass it to the new thread
    convert parameters to Ruby
    call Ruby on this dedicated thread
    convert results to native
    bundle up Ruby result and pass it to the origin thread
    terminate the ruby thread

  end
#=> native code