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:
- You can save the return value of
attach_function
, which is an FFI::VariadicInvoker or FFI::Function. attach_function
adds FFI::Function implicitly as a class variable.- You can call FFI::Library#ffi_libraries to get an array of FFI::DynamicLibrary objects representing native libraries, choose the correct one, and then call FFI::DynamicLibrary#find_function to get a FFI::Symbol representing the function. This is more complicated but might be appropriate if you are using an FFI wrapper written by someone else and it is not easy to modify it.
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