Skip to content
Jimmy H edited this page Apr 23, 2023 · 10 revisions

This page contains some real-world(ish) examples of Ruby-FFI usage. It will generally present the C interface, along with the Ruby code to call the C code.

There are some other examples in the repository.

Common Usage

Let's imagine a C library interface like the following:

C interface:

/* mylibrary.h */
double               calculate_something(int a, float b);
int                  error_code(void);
struct SomeObject*   create_object(char* name);
double               calculate_something_else(double c, struct SomeObject* obj);
void                 free_object(void* pointer_to_memory);

A possible C application might use this library as follows:

#include "mylibrary.h"
int main()
{
  double c, d ;
  int errcode ;
  struct SomeObject *objptr ;

  c = calculate_something(42, 98.6) ;
  if ((errcode = error_code()) != 0) {
    fprintf(stderr, "error calculating something: %d\n", errcode);
    exit(1);
  }

  objptr = create_object("my object") ;
  d = calculate_something_else(c, objptr) ;
  free_object(objptr) ;  

  fprintf(stdout, "calculated %f\n", d);
  exit(0) ;
}

Note that the code does not access the SomeObject's data members, but rather treats it as an opaque object -- that is, a pointer to a memory block that the application passes into other API calls, but doesn't operate on directly.

Now let's attach the library code to a Ruby FFI wrapper:

# mylibrary.rb
module MyLibrary
  extend FFI::Library
  ffi_lib "path/to/mylibrary.so"
  attach_function :calculate_something, [:int, :float], :double
  attach_function :error_code, [], :int # note empty array for functions taking zero arguments
  attach_function :create_object, [:string], :pointer
  attach_function :calculate_something_else, [:double, :pointer], :double
  attach_function :free_object, [:pointer], :void
end

And then the same application logic, but this time in Ruby:

require 'ffi'
require 'mylibrary'

c = MyLibrary.calculate_something(42, 98.6) # note FFI handles literals just fine
if ( (errcode = MyLibrary.error_code()) != 0)
  puts "error calculating something: #{errcode}"
  exit 1
end

objptr = MyLibrary.create_object("my object") # note FFI handles string literals as well
d = MyLibrary.calculate_something_else(c, objptr)
MyLibrary.free_object(objptr)

puts "calculated #{d}"

Note about NULL Values

Note that you can pass a NULL pointer by passing nil, as follows:

C code:

c = calculate_something_else(14.2, NULL);

Ruby code:

c = MyLibrary.calculate_something_else(14.2, nil)

Output Parameters with MemoryPointer

So far, you've seen a very simple C API. Most C APIs are a little more complicated, though.

Let's imagine the following function is also part of our MyLibrary interface:

/*
 *  find_first_match() looks for an object with name _name_ in the object cache.
 *  returns: the number of total matches,
 *           and sets the found_object pointer to the first SomeObject found
 */
int find_first_match(char* name, struct SomeObject** found_object) ;

This function is different in that it has an output parameter. See the following C code for usage:

  struct SomeObject* objptr ;
  int nfound ;
  double result ;

  nfound = find_first_match("jimbo", &objptr) ;
  result = calculate_something_else(11.2, objptr) ;  

The function is attached via FFI as follows:

  attach_function :find_first_match, [:string, :pointer], :int

And here's the same sample usage translated into Ruby code:

  objptr = FFI::MemoryPointer.new :pointer
  nfound = MyLibrary.find_first_match("jimbo", objptr)
  objptr = objptr.get_pointer(0)
  result = calculate_something_else(11.2, objptr)

Strings as Output Parameters

More on output Parameters: Here are examples of methods which return a single string, and an array of strings. They were based on wrapping the Augeas library.

Single String

Assume the following method:

int aug_get(const augeas* aug, const char* path, const char** value);

Where the value of the get is the third parameter, and the return value tells you success or failure. The function is attached via FFI as follows:

module AugeasLib
  extend FFI::Library
  ffi_lib "libaugeas.so"
  attach_function :aug_get, [:pointer, :string, :pointer], :int
end

And the sample usage of this would be:

   def get(path)
        ptr = FFI::MemoryPointer.new(:pointer, 1)
        AugeasLib.aug_get(@aug, path, ptr)
        strPtr = ptr.read_pointer()
        return strPtr.null? ? nil : strPtr.read_string()
    end

FFI::Pointer.read_string() doesn't care about the string encoding used. To get a ruby string with proper encoding, you have to call force_encoding(encoding). Sample usage of this would be:

   def get(path)
        ptr = FFI::MemoryPointer.new(:pointer, 1)
        AugeasLib.aug_get(@aug, path, ptr)
        strPtr = ptr.read_pointer()
        return strPtr.null? ? nil : strPtr.read_string().force_encoding('UTF-8') # returns UTF-8 encoded string
    end

With *W family of winapi functions you need to use #read_bytes(len * 2) (because #read_string stops on first \x00).

Array of Strings

Assume the following method:

int aug_match(const augeas* aug, const char* path, char*** matches);

Where the value of the match is the third parameter, and the return value tells you success or failure and the size of the array. The function is attached via FFI as follows:

module AugeasLib
  extend FFI::Library
  ffi_lib "libaugeas.so"
  attach_function :aug_match, [:pointer, :string, :pointer], :int 
end

And the sample usage of this would be:

    def match(path)
        ptr = FFI::MemoryPointer.new(:pointer, 1)
        len = AugeasLib.aug_match(@aug, path, ptr)
        if (len < 0)
            raise SystemCallError.new("Matching path expression '#{path}' failed")
        else
            strPtr = ptr.read_pointer()
            strPtr.null? ? [] : strPtr.get_array_of_string(0, len).compact
        end
    end 

Structs

Now let's get more advanced -- let's access SomeObject's data members through a Ruby object.

/* my_library.h */

struct SomeObject {
  struct SomeObject *next ;
  char              *name ;
  double             value ;
};
  struct SomeObject *objptr1 ;
  struct SomeObject *objptr2 ;

  objptr1 = create_object("foobar");
  printf("%s\n", objptr1->name); // => 'foobar'

  int nfound = find_first_match("foobar", &objptr2) ;
  printf("%s\n", objptr2->name); // => 'foobar'

  free_object(objptr1);

Here's how to create a Ruby object to mirror this struct.

(Note: see Automatic Struct Layout for a way to automate layout offset calculations.)

module MyLibrary
  class SomeObject < FFI::Struct
    layout :next,  :pointer,
           :name,  :string,
           :value, :double
  end
end

And here's the application code:

  obj_ptr2 = FFI::MemoryPointer.new :pointer

  obj_ptr1 = MyLibrary.create_object "foobar"
  obj1 = MyLibrary::SomeObject.new(obj_ptr1) # wrap ruby object around C pointer
  puts obj1[:name] # => 'foobar'

  nfound = MyLibrary.find_first_match("foobar", obj_ptr2)
  obj2 = MyLibrary::SomeObject.new(obj_ptr2.read_pointer()) # wrap ruby object around C pointer
  puts obj2[:name] # => 'foobar'

  MyLibrary.free_object(obj1.pointer); # equivalent to free_object(obj_ptr1)

Custom Memory Management

Now that we have a full-blown Ruby object to access C struct data members, wouldn't it be nice if we didn't have to take care of calling free_object each time we created a SomeObject?

You can do it! Inherit from FFI::ManagedStruct instead of FFI::Struct, and define a release() class method:

module MyLibrary
  class SomeObject < FFI::ManagedStruct
    layout :next,  :pointer,
           :name,  :string,
           :value, :double,

    def self.release(ptr)
      MyLibrary.free_object(ptr)
    end
  end
end

Yes, it's that easy. Now the code from the previous section could look like this:

begin
  obj_ptr1 = MyLibrary.create_object "foobar"
  obj1 = MyLibrary::SomeObject.new(obj_ptr1) # wrap ruby object around C pointer
  puts obj1[:name] # => 'foobar'
end
# obj1 is now out-of-scope, and when it is GC'd,
# the SomeObject#release() method will be invoked with obj1 as its argument, allowing it to free the C struct's memory.

Note that for structs that are created with only native elements (like int's, etc.) may not need any cleanup method, as the structs themselves are cleaned up automatically at GC time. In the instance above we need a cleanup method since the struct refers to a pointer that needs to be cleaned up, to avoid a memory leak.

For both normal structs and "memory managed" you can explicitly call #free on them, if desired.

WARNING: Don't Use ManagedStruct When You Want Type-Casting

There's a potential pitfall when using FFI::ManagedStruct, which is that you may not always actually want to free underlying memory. For instance, in this case we don't want to free the underlying memory:

  obj_ptr2 = FFI::MemoryPointer.new :pointer
  nfound = MyLibrary.find_first_match("foobar", obj_ptr2)
  obj2 = MyLibrary::SomeObject.new(obj_ptr2) # wrap ruby object around C pointer
  puts obj2[:name] # => 'foobar'

However, as soon as obj2 goes out of scope, that underlying memory ''will'' be freed, despite the fact that the find_first_match() cache still has a pointer to it, leading to a probable segfault and FAIL. That's bad.

I've found it useful to define two Ruby classes, one inherited from ManagedStruct, for managing object lifetimes, and one inherited from Struct, used for typecasting pointers.

Here's an example of this:

module MyLibrary
  module SomeObjectLayout
    def self.included(base)
      base.class_eval do
        layout :next,  :pointer,
               :name,  :string,
               :value, :double,
      end
    end
  end

  class SomeObject < FFI::ManagedStruct
    include SomeObjectLayout
    def self.release(ptr)
      MyLibrary.free_object(ptr)
    end
  end

  class SomeObjectCast < FFI::Struct
    include SomeObjectLayout
  end
end

And here's some sample usage:

begin
  obj_ptr1 = MyLibrary.create_object "foobar"
  obj1 = MyLibrary::SomeObject.new(obj_ptr1) # NOTE use of managed struct class
  puts obj1[:name] # => 'foobar'

  obj_ptr2 = FFI::MemoryPointer.new :pointer
  nfound = MyLibrary.find_first_match("foobar", obj_ptr2)
  obj2 = MyLibrary::SomeObjectCast.new(obj_ptr2) # NOTE: use of typecast class
  puts obj2[:name] # => 'foobar'
end
# obj1 and obj2 are now both out-of-scope. however, free_object() will
# only be invoked once, for obj1.

Callbacks

Many APIs use function pointers to manage "callbacks", which are functions-to-be-called-later. Ruby libraries commonly uses Procs or lambdas (let's call them '''closures''' here) for this, and FFI allows you to map closures to C function callbacks. Nice!

Again, here's our C API:

/*
 *  notifyWhenObjectWaggles callback:
 *    waggled: the object that just waggled
 *    waggle_size: how much the object waggled
 *  should return:
 *    non-zero integer to continue waggling, zero to cease waggling
 */
typedef int (*notifyWhenObjectWaggles)(struct SomeObject *waggled, int waggle_size) ;
void set_object_waggle_callback(struct SomeObject *waggler, notifywhenObjectWaggles callback) ;

Here's how it's used in C:

int waggle_callback(struct SomeObject *waggled, int waggle_size)
{
  printf("object %s just waggled %d\n", waggled->name, waggle_size);
  return(waggle_size > 10 ? 1 : 0);
}

int main()
{
  ...

  struct SomeObject *p ;
  p = create_object("foobar");
  set_object_waggle_callback(p, waggle_callback);

  ...
}

Here's the Ruby declaration equivalent:

module MyLibrary
  ... # same as before

  #  declare the callback type (the callback's function signature)
  callback :object_waggled_callback, [:pointer, :int], :int

  #  declare the function signature that takes the callback as a param.
  #  note we use the callback type just like any builtin type here.
  attach_function :set_object_waggle_callback, [:pointer, :object_waggled_callback], :void

And finally, the callback in action:

#
#  it's a good policy to assign closures to a constant.
#  more on this below.
#
MyLibrary::WaggleCallback = Proc.new do |waggled_ptr, waggle_size|
  waggled = MyLibrary::SomeObjectCast.new waggled_ptr # note we use the typecast flavor
  puts "object #{waggled[:name]} just waggled #{waggle_size}"
  waggle_size > 10 ? 1 : 0
end

obj_ptr = FFI::MemoryPointer.new :pointer
obj_ptr = create_object("foobar")
obj = MyLibrary::SomeObject.new obj_ptr
set_object_waggle_callback(obj.pointer, MyLibrary::WaggleCallback)

WARNING: Be Careful Your Proc Doesn't Get GC'd

Let's imagine the above example was written as follows:

begin
  proc = Proc.new do |waggled_ptr, waggle_size|
    waggled = MyLibrary::SomeObjectCast.new waggled_ptr # note we use the typecast flavor
    puts "object #{waggled[:name]} just waggled #{waggle_size}"
    waggle_size > 10 ? 1 : 0
  end
  set_object_waggle_callback(obj.pointer, proc)
end

At the end of the block, proc is out of scope and may be garbage-collected. This is really bad for your application, because as soon as the object waggles, the C library will be dereferencing a pointer that no longer points to your proc object.

You must be careful to ensure that your closures do NOT go out of scope while they are active!

The easiest way to guarantee this is to assign the proc to a constant, which will never go out of scope. This is what was done in the example above.

Using :varargs

Here's an example

require 'ffi'

module Hello
  extend FFI::Library
  attach_function 'printf', [:string, :varargs], :int
end

3.times {  Hello.printf("cputs %s %d %x", :string, "yoyo", :int, 33, :int, 34)} # each one needs its own specifier of which type it is

Using typedef

You can define "aliases" for types using typedef current, new. Once an alias has been defined, you can use it in a struct or function definition. This can make those definitions more descriptive and meaningful, or make it easier to deal with platform differences. Here's a contrived example:

module MyLibrary
  extend FFI::Library

  typedef :pointer, :id

  # Define :status as an alias for :long on Mac, or :int on other platforms.
  if FFI::Platform.mac?
    typedef :long, :status
  else
    typedef :int, :status
  end

  class MyStruct < FFI::Struct
    layout :a, :id, :b, :status
  end

  attach_function :doSomething, [:id, :int], :status
end
Clone this wiki locally