We often come to the point, when we have to write enormously huge module in Ruby. But we simply don’t want to. Maybe because we’re too lazy/smart, or we do know that such library already exists written in C/C++. Implementing pthread or crypt library in pure Ruby sounds like reinventing the wheel. The other thing is well-written C code most likely will perform much better than any higher-level implementation. It takes advantage of pre compilation, better garbage collection or static typing.
So what should we do? If the functionality is relatively simple, and we have access to original source code (not ony compled .so or .dll binaries), first approach may be to go through code line-by-line and convert it. What about pieces which can’t be processed in such way? Or the algorithm itself is very complicated? Luckily, there is a few ways of calling C functions from Ruby code.
Ruby Inline
It comes as a gem, and requires to have gcc/g++ compiler installed. After that simply gem install RubyInline
. Inline works by embedding pure C/C++ code into Ruby code. And it doesn’t look cool. But will do the job, and will do it fast. Here’s an example of calling pow function from math library, as well as own implementations of fibonacci and factorial functions:
require 'inline'
class InlineTest
inline do |builder|
builder.include '<math.h>'
builder.c '
int inline_pow(int a, int n) {
return pow(a, n);
}'
builder.c '
long inline_factorial(int max) {
int i=max, result=1;
while (i >= 2) { result *= i--; }
return result;
}'
builder.c '
int inline_fibonacci(int n) {
int a = 1, b = 1, c, i;
if (n == 0) {
return 0;
}
for (i = 3; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return b;
}'
end
end
puts InlineTest.new.inline_factorial(5)
puts InlineTest.new.inline_fibonacci(9)
puts InlineTest.new.inline_pow(2, 10)
First thing which comes to my mind, is that you actually NEED to write some C code. At least dummy wrapper of external function. It supports both C and C++, and according to authors, it’s extendable to work with other languages. We don’t need to worry about conversion of passed arguments and returned data. Also no manual compilation is required, it will detect any code changes and adopt them.
Rice
Another great software, which takes complicated C++ classes, types, methods and exposes them into Ruby. This time, separated implementations are required, which gives us better arranged project structure and makes it a little bit more complicated to set-up. First of all, each class method has to be wrapped into C method with calls into Ruby’s API. And here’s a simple example of exposing C function into Ruby:
#include "rice/Class.hpp"
using namespace Rice;
int rice_fibonacci(int n) {
int a = 1, b = 1, c, i;
if (n == 0) {
return 0;
}
for (i = 3; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return b;
}
int rice_factorial(int max) {
int i=max, result=1;
while (i >= 2) { result *= i--; }
return result;
}
int rice_pow(int a, int n) {
return pow(a, n);
}
extern "C"
void Init_rice_test()
{
Class rb_c = define_class("RiceTest")
.define_method("fibonacci", &rice_fibonacci)
.define_method("factorial", &rice_factorial)
.define_method("pow", &rice_pow);
}
Before we jump to the Ruby code, some preparations are required. First step, installing Rice gem gem install rice
. Then we need to create extconf.rb
file containing (or simply execute the code):
require 'mkmf-rice'
create_makefile('rice_test')
Running this piece of code will generate templated Makefile, ready to compile code rice_test.cpp
class code. And finally, running make, which will use pre-generated Makefile to compile our glue-code into shared library. We will end up with rice_test.o
(safe to remove) and rice_test.so
. We can do anything with compiled binaries: add it to our project vendors, or add to system libraries. We just need to include it in Ruby code correctly and call exposed methods:
require './rice_test'
puts RiceTest.new.fibonacci(5)
Using Rice required more preparations than Ruby Inline, but we got cleaner solution, and precompiled libraries ready to drop to application server. It provides a way to expose whole classes, with all their methods, constructors, attributes accessors and so on. So this approach would be better if we want to take OO approach. Also Rice was designed with C++ in mind, and because of that will work best with C++, but can be used also to glue pure C. Another advantage is that we don’t need to have compiler installed on application server, which is nice.
FFI
And last, but not least library I’m going to focus on is FFI (Foreign Function Interface). As usual, it’s available in form of a gem. Although it has simplest and most intuitive dls, it’s not lacking complex functionalities. Great advantage (and maybe disadvantage for somebody at the same time) is that we don’t need to dig into crazy C/C++ interface exposing, glue-code generation and code compilation. As simple as that. So let’s check FFI in action. After installing gem install ffi
and its libraries: libffi-dev
or libffi-devel
depending on your OS, the only thing we need to do is wrapping shared library calls into Ruby:
require 'ffi'
module FfiMathTest
extend FFI::Library
ffi_lib 'c'
ffi_lib 'm'
attach_function :pow, [ :double, :double ], :double
end
puts FfiMathTest.pow(2.2, 10)
Definition syntax is self-explaining, and all the magic happens in Ruby. There’s no intermediate methods calling desired one. We’re just including libraries we want to use: libc and libm (math), and addressing them with their original name, parameters list and types. Another solid point for FFI is that it’s multi-platform and multi-implementation. The same code will run fine on all different Ruby interpreters: MRI, JRuby, Rubinius and all supporting FFI. But on the other hand, it doesn’t support C++ because of the complexity that C++ adds, largely the problem involved the name-mangling. FFI includes many useful components, which includes interaction with C structures, unions, allocating and accessing memory with pointers or data types mapping.
And from custom library which looks like this:
#include <stdlib.h>
#include <math.h>
int ffi_pow(int a, int n) {
return pow(a, n);
}
int ffi_factorial(int max) {
int i=max, result=1;
while (i >= 2) { result *= i--; }
return result;
}
int ffi_fibonacci(int n) {
int a = 1, b = 1, c, i;
if (n == 0) {
return 0;
}
for (i = 3; i <= n; i++) {
c = a + b;
a = b;
b = c;
}
return b;
}
compilation…
$ gcc -c -fPIC ffi_test.c -o ffi_test.o
$ gcc -shared -o ffi_test.so ffi_test.o
Ruby binding and call:
require 'ffi'
module FfiCustomTest
extend FFI::Library
ffi_lib 'c'
ffi_lib './ffi_test.so'
attach_function :ffi_pow, [ :int, :int ], :int
attach_function :ffi_factorial, [ :int ], :int
attach_function :ffi_fibonacci, [ :int ], :int
end
puts FfiCustomTest.ffi_factorial(5)
puts FfiCustomTest.ffi_fibonacci(9)
puts FfiCustomTest.ffi_pow(2, 10)
So which one to use?
RubyInline is extremely easy to start with and allows wrapping C functions calls in glue-layer. It could be useful when working with complex types, structures or classes whereas expecting primitive response. Rice interacts great with C++ and allows OO abstraction. And FFI being able to run on various Ruby implementations. Let’s look at simple benchmark of them, also with Ruby version:
def rb_pow(a, n)
a ** n
end
def rb_factorial(max)
i = max; result = 1
while i >= 2 do
result *= i
i -= 1
end
result
end
def rb_fibonacci(n)
a = 1; b = 1
return 0 if n == 0
for i in 3..n do
c = a + b
a = b
b = c
end
b
end
And finally some numbers. 100000 method calls of: factorial(10)
, fibonacci(20)
, pow(2, 20)
Ruby | RubyInline | Rice | FFI | |
---|---|---|---|---|
factorial | 0.044854815 | 0.026175138 | 0.197720523 | 0.014882004 |
fibonacci | 0.152947962 | 0.026792521 | 0.202714029 | 0.018928646 |
pow | 0.01204131 | 0.03252452 | 0.211258897 | 0.023082315 |
As many of us might expect, FFI performed best (with small exception in power function as there’s not much code and external function calls took most of the time). RubyInline was a nice surprise. Easy to start with combined with good performance makes it great candidate for not-so-demanding problems. But who’s gonna use embedded C code for such trivial tasks. In most of the cases it will be mathematical calculations. Keeping that in mind, we should probably consider other factors: easy of use and ability to adopt in our project.
References
Post by Kamil Dziemianowicz
Kamil, long-term employee of AmberBit (joined 2012), is a full stack developer (Ruby, JavaScript).