Skip to content
David Grayson edited this page Jan 23, 2015 · 25 revisions

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, [:pointer, :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"
  callback :completion_function, [:pointer, :long, :uint8], :void
  attach_function :do_work, [:pointer, :completion_function], :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. You can do that by saving the return value of attach_function, which is an FFI::VariadicInvoker, and passing it as the function pointer argument:

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

  Worker = attach_function :worker, [:pointer, :long, :uint8], :void
end

LibWrap.do_work(..., LibWrap::Worker)

GIL

There may be situations where the callback function is another C function in the library. In this case it may not make any sense to retain the Ruby GIL when there is no need to protect the Ruby runtime from a thread race. Releasing the GIL will allow the Ruby runtime to (potentially) schedule another thread to run and complete more work. Luckily, FFI::Function allows us to optionally release the GIL by marking the callback as blocking. This setting only affects Ruby-to-native calls; it has no effect for native-to-Ruby calls.

When using a Proc callback setup, the GIL is always retained.

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

  attach_function :do_work, [:pointer, :pointer], :int

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

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

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

FFI::Function, :blocking => true, native method to Ruby

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

(The code path remains the same in this situation for :blocking => false too.)

Callback Proc

Regardless of the direction of the call (native to Ruby or Ruby to native), callback Procs always follow the same 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
    bundle up FFI data
    pass to Ruby callback processing thread
    wait for signal from callback processing thread
  end
-> native code
Clone this wiki locally