Jertype

Member of Information Superhighway

Calling C shared libraries with ruby FFI

Sometimes you need something that only a C library can provide.

This could be software or hardware that only has a C API. You could have a CPU intensive process such as image processing that Ruby is too slow to accomodate.

So how do you communicate with a C Shared Object (.so) file?

One solution is the use of the ffi gem. In this post, we’ll use the ffi gem to call a C function.

Prerequisites

Install the ffi gem by running gem install ffi

Create the C Shared Object

If you didn’t follow the previous post, create the following two files:

concat.h

void concat(const char *s1, const char *s2, char *result);

concat.c

#include <string.h>

void concat(const char *s1, const char *s2, char *result) {
  strcpy(result, s1);
  strcat(result, s2);
}

Then run the following commands:

You should now have a file called concat.so in your folder

Using FFI to call the C function

Let’s create a concat.rb file that we’ll use to call the C function. First, we’ll add the code needed to make the C function available in Ruby.

concat.rb

require 'ffi'

module ConcatInterop
  extend FFI::Library

  ffi_lib './concat.so'
  attach_function :concat, [:string, :string, :pointer], :void
end

The main part here is the attach_function call which will add the C library’s function to the ConcatInterop module.

If you remember earlier, our function signature was:

void concat(const char *s1, const char *s2, char *result)

So how did we choose the variable types? All three represent pointers to strings, so why is only the last parameter a :pointer?

The reason is that the last parameter is going to be modified by the C library. The C library is expecting us to pass a pointer with enough memory allocated to fit the entire concatenated string.

Thanks to the code in the newly created ConcatInterop module we can call the function. But how exactly do we do that? To the bottom of the concat.rb file let’s add another module that will call ConcatInterop’s concat function.

concat.rb

module ConcatLibrary
  def self.concat(first_word, second_word)
    combined_word_size = first_word.length + second_word.length + 1
    concatenated_word = ""
    FFI::MemoryPointer.new(:char, combined_word_size) do |p|
      ConcatInterop.concat(first_word, second_word, p)
      concatenated_word = p.read_string_to_null
    end
    concatenated_word
  end
end

Wow, doesn’t that seem like a lot of code?

The key is the FFI::MemoryPointer. When we send the C function a pointer, we need to allocate enough memory so that the concatenated string can fit.

To do this, we have to create a MemoryPointer that can fit the number of characters that are in the two strings combined. In addition, we place the MemoryPointer call into a block. This allows Ruby to give the memory allocated to the pointer back to the computer after the block is completed.

Within the block you can see we use the read_string_to_null command. This reads the memory located at the pointer until it reaches a NULL character which signifies the end of the string.

Running the Code

Finally, it’s time to run the code. Make sure that your concat.so and concat.rb files are in the same folder. Then open irb by using the following command irb -r ./concat.rb.

Now call ConcatLibrary.concat("It ", "worked!"). Hopefully you’ll see “It worked!” printed out.

In Summary

That was a lot of work just to concatenate some strings wasn’t it? But now that you’ve written the wrapping code, it’s just a one line ruby call to access the C function.

As you’ve seen, using FFI does require some knowledge of how C works, but the heavy lifting can stay inside the C library.

Learning FFI allows you to write ruby while still taking advantage of libraries that are in the C ecosystem.