Introduction
If you're working with Ruby and need to invoke a function written in C language, there are some convenient gems available: Ruby-FFI and Fiddle.
Ruby-FFI has many features, handling most challenges you might encounter. Fiddle might seem a bit less convenient, but being an official Ruby gem, it is available from the start in most environments.
When You Want to Use Both FFI or Fiddle and C Extension
There can be situations where you want to rewrite certain parts of your Gem implemented with FFI or Fiddle into C extensions. Function calls using libffi are known to be nearly 100 times slower than that of native C extensions. If large numbers of calls need to be made with a demand for speed, you might want to consider rewriting the FFI-implemented function using C extensions.
The Method
Basic Principles
The main challenge here is determining how to handle the pointer of FFI or Fiddle's structure as an argument in a C extension function. The solution is straightforward: get the memory address from the Fiddle::Pointer
or FFI::Pointer
Ruby objects.
Here you will learn how to write a C Extension function that takes FFI::Pointer as an argument, referring to the rcairo gem.
Check for the existence of the constant FFI::Pointer
We begin by ensuring the constant FFI::Pointer
is defined. This step verifies that require "ffi"
has been executed and Fiddle::Pointer
is available.
if (NIL_P (rb_cairo__cFFIPointer))
{
rb_raise (rb_eNotImpError,
"%s: FFI::Pointer is required",
rb_id2name (rb_frame_this_func ()));
}
The rb_cairo__cFFIPointer
is pre-set in Init_cairo_private.
void
Init_cairo_private (void)
{
// -- code omission --
if (rb_const_defined (rb_cObject, rb_intern ("FFI")))
{
rb_cairo__cFFIPointer =
rb_const_get (rb_const_get (rb_cObject, rb_intern ("FFI")),
rb_intern ("Pointer"));
}
else
{
rb_cairo__cFFIPointer = Qnil;
}
}
In the case of Fiddle, execute:
rb_const_get (rb_const_get (rb_cObject, rb_intern ("Fiddle")), rb_intern ("Pointer"));
Ensure Argument Type Consistency
After confirming the constant, we ensure the argument classes are consistent. As FFI::Pointer
and Fiddle::Pointer
obtain addresses using relatively common names - address
and to_i
respectively, performing a type check helps prevent errors.
if (!RTEST (rb_obj_is_kind_of (pointer, rb_cairo__cFFIPointer)))
{
rb_raise (rb_eArgError,
"must be FFI::Pointer: %s",
rb_cairo__inspect (pointer));
}
Acquiring the Address
With FFI, you can get the address with the address
method.
# Ruby-FFI
pt = FFI::MemoryPointer.new(:int)
p pt.address
With Fiddle, to_i
method helps in getting the address.
# Fiddle
pt = Fiddle::Pointer.new(Fiddle::SIZEOF_INT)
p pt.to_i
In C extensions, these Ruby methods are invoked using rb_funcall
.
rb_funcall (ffi_pointer, rb_intern ("address"), 0)
rb_funcall (fiddle_pointer, rb_intern ("to_i"), 0)
Call a C Function Using the Acquired Address as an Argument
The above Ruby code is executed within the C extension code.
VALUE rb_cr_address;
rb_cr_address = rb_funcall (pointer, rb_intern ("address"), 0);
cr = NUM2PTR (rb_cr_address);
cr_check_status (cr);
Here, the NUM2PTR
macro is not provided by ruby.h
so you'll need to define it yourself:
#if SIZEOF_LONG == SIZEOF_VOIDP
# define PTR2NUM(x) (ULONG2NUM((unsigned long)(x)))
# define NUM2PTR(x) ((void *)(NUM2ULONG(x)))
#else
# define PTR2NUM(x) (ULL2NUM((unsigned long long)(x)))
# define NUM2PTR(x) ((void *)(NUM2ULL(x)))
#endif
The cr_check_status
function calls the native Cairo function cairo_status_to_string
. It's safe to insert a function like this in the middle.
Creating a Ruby Object
To create a Ruby object from the obtained address, do as follows:
rb_cr = rb_obj_alloc (self);
cairo_reference (cr);
RTYPEDDATA_DATA (rb_cr) = cr;
rb_ivar_set (rb_cr, cr_id_surface, Qnil);
Use rb_obj_alloc
to create an instance of the class (self, in this case). cairo_reference()
is a Cairo function that increases the reference count, which ensures Garbage Collection won't remove your ruby-FFI
object. RTYPEDDATA_DATA
is used to access the data of TypedData Objects directly. Lastly, rb_ivar_set
sets an instance variable.
Basic Example
Let's walk through a basic example. For this, piyo.h
and piyo.c
have been prepared as targets to create bindings.
piyo.h
#ifndef PIYO_H
#define PIYO_H
#include <stdio.h>
typedef struct Piyo
{
int age;
char *name;
} Piyo;
void displayPiyoInfo(const Piyo *piyo);
#endif
piyo.c
#include "piyo.h"
void displayPiyoInfo(const Piyo *piyo)
{
printf("Name: %s\n", piyo->name);
printf("Age: %d\n", piyo->age);
}
Write the C extension so that the following code functions correctly:
require 'fiddle/import'
require_relative './piyo.so'
module Piyo
Piyo = Fiddle::Importer.struct(['int age', 'char* name'])
end
tori_name = 'piyoko'
Piyo::Piyo.malloc(Fiddle::RUBY_FREE) do |piyo|
piyo.age = 100
piyo.name = tori_name
Piyo.display_info(piyo)
end
Ruby C Extension
piyo_rb.c
#include "ruby.h"
#include "piyo.h"
#if SIZEOF_LONG == SIZEOF_VOIDP
#define PTR2NUM(x) (ULONG2NUM((unsigned long)(x)))
#define NUM2PTR(x) ((void *)(NUM2ULONG(x)))
#else
#define PTR2NUM(x) (ULL2NUM((unsigned long long)(x)))
#define NUM2PTR(x) ((void *)(NUM2ULL(x)))
#endif
VALUE rb_cFiddlePointer;
VALUE rb_display_info(VALUE self, VALUE piyo)
{
Piyo *ptr;
VALUE rb_address = rb_funcall(piyo, rb_intern("to_i"), 0);
ptr = NUM2PTR(rb_address);
displayPiyoInfo(ptr);
return Qnil;
}
void Init_piyo(void)
{
VALUE mPiyo = rb_define_module("Piyo");
rb_define_singleton_method(mPiyo, "display_info", rb_display_info, 1);
}
Create Makefile with:
extconf.rb
require 'mkmf'
find_header('piyo.h', __dir__)
create_makefile('piyo')
Compile with:
ruby extconf.rb
make
Execute with:
ruby test.rb
If everything runs correctly, you should see the output as:
Name: piyoko
Age: 100
While this is a simple example and doesn't include every aspect, such as class definition verification and argument type checks, you will need to add these elements to transition it into a practical gem.
That's all for this post.
This article was translated from Japanese to English by a collaboration of ChatGPT, DeepL, and the author. The author, despite having the weakest command of English among the three, played a crucial role in providing instructions to ChatGPT and DeepL. In Japanese, 'Piyo' represents the chirping sound of a chick and is often used as a meta-syntax variable, following 'hoge' and 'fuga'.
Top comments (0)