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:
gcc -c -fPIC concat.c -o concat.o
gcc -shared concat.o -o concat.so
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.
- attach_function :concat, [:string, :string, :pointer], :void
- :concat -> The name of the function you want to call
- [:string, :string, :pointer] -> The types of variables you will pass to the function
- :void -> The return type of the function
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.