FFI
is a fantastic tool for easily interfacing your Ruby code with native libraries. To help you quickly ramp up and become a happier and more productive FFI
master, the following are a few of the fundamental concepts you'll want to understand in order to get the most out of FFI
.
Overall Architecture
Using FFI, you can use native libraries from Ruby without writing a single line of native code. Your good friend FFI
takes care of all of the cross Ruby implementation (MRI, JRuby, Rubinius, MacRuby, etc) and platform specific issues so that you can focus on writing and testing your Ruby code.
As great as this sounds, you just know there's something missing. Your spidey sense is right again.
As FFI is effectively a bridge between the multiple worlds of Ruby implementations and multiple platform types, you might suffer a bit of cognitive dissonance trying to pull all the pieces together. When we develop in Ruby we tend to think in higher level terms, and don't so much concern ourselves with the lower level issues. While FFI allows us to stay firmly rooted in Ruby, we also have to start thinking in lower level terms.
Core Components
FFI
has a number of useful components. Investing the time to understand FFI's components and capabilities will pay off as you begin using FFI
. That said, it's nice to have an idea which components you should look at first. Understanding the following core modules and classes is a great way to start getting FFI's capabilities:
::FFI::Library
- along withrequire 'ffi'
, this module brings FFI's powerful native library interfacing capabilities into your Ruby code as a DSL. Typically you extend your custom module with this one, specify the native libraries and their calling conventions, prototype the native library's functions and structs in Ruby, and then start using the native library's API from Ruby.::FFI::Pointer
- wraps native memory allocated by a third party library. It provides a number of methods for transferring data from unmanaged native memory to Ruby-managed native memory (::FFI::MemoryPointer
). The native memory wrapped by this class is not freed during garbage collection runs.::FFI::MemoryPointer
- allows for Ruby code to allocate native memory and pass it to non-Ruby libraries. Lifecycle management (allocation and deallocation) are handled by this class, so when it gets garbage collected the native memory is also freed, unless#autorelease=
was set to false.::FFI::Struct
and::FFI::Union
Memory Management
When you're writing Ruby code you usually don't think about memory management. It's just taken care of for most of your use cases. However, when you're leveraging FFI
, even though you're still developing in Ruby, you have to begin thinking more about these low-level issues.
Ruby has the concept of object references and garbage collection. In contrast C libraries have pointers and manual allocation and deallocation of memory. So you have to take care, that objects which provide memory referenced by a pointer, stays valid until the pointer is no longer used by the library. Therefore you have to keep references to these objects, even when they are no longer used by your ruby code.
So any use of :pointer
, :string
, :buffer_in
, :buffer_out
or :buffer_inout
must be backed by keeping a reference in ruby.
This is true for function arguments, pointer references in FFI::Struct as well as function pointers (callbacks).
In other words: A pointer on C level to a Ruby object is not considered an object reference by the Ruby runtime and is therefore not sufficient to protect the object from being garbage collected.
String Memory Allocation
One such memory management consideration occurs when you try to integrate with C functions that keep a reference to a Ruby allocated string rather than making their own copy of the string's contents. Take the following rogueware snippet which occurs more often than we'd like to admit:
static char* my_name;
void bad_set_my_name(char* name) {
my_name = name;
}
Mapped to ruby like so:
module Bar
attach_function :bad_set_my_name, [ :string ], :void
end
This code assumes that its my_name
reference to the Ruby string will remain valid during the time it's needed. So you have to store the ruby String object, that is passed to the C function, in a variable, that stays valid until my_name
is no longer in use. Usually this is either a constant or an instance variable of a Ruby object, that has the same lifetime as the loaded library. If the Ruby object is not stored in a long enough living variable, the memory address held by my_name
is no longer pointing to the string.
This is a safe call to the C function:
module Foo
NAME = "Barney"
Bar.bad_set_my_name(NAME)
end
If the string to be passed contains null bytes or the string data is modified on either the C or the Ruby side, you should copy the Ruby string to native memory. Then pass the pointer to that buffer to the C function instead of passing a raw string, and finally, update your original attach_function
signature to use the :pointer
rather than the original :string
. For example:
# proper use of the bad C function from Ruby
module Foo
NAME = FFI::MemoryPointer.from_string("Barney")
Bar.bad_set_my_name(NAME)
end
# new Ruby mapping
module Bar
attach_function :bad_set_my_name, [ :pointer ], :void
end
Using malloc/free
The FFI library provides ::FFI::MemoryPointer
to allocate heap memory.
To use the memory independently of the Ruby object lifetime, FFI::Pointer#autorelease= can be set to false.
Then it can be used like malloc()
in C.
So the memory is not freed by the Ruby runtime, even when the Pointer object is garbage collected.
This is sometimes necessary for libraries that take a memory buffer as an argument and then expect to manage that buffer's lifecycle.
buffer = FFI::MemoryPointer.from_string("content") # Allocate 8 byte of memory to put string into plus zero termination
buffer = FFI::MemoryPointer.new(8) # Alternatively allocate 8 byte of memory and fill with zeros
buffer.autorelease = false
SomeLib.function_that_will_manage_buffer(buffer)
It's still possible to free the buffer manually by calling FFI::Pointer#free, when it's no longer in use by the library.