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 andFiddle.
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 theFiddle::Pointer
orFFI::Pointer
Ruby objects.
Here you will learn how to write aC Extension function that takes FFI::Pointer as an argument, referring to thercairo gem.
Check for the existence of the constant FFI::Pointer
We begin by ensuring the constantFFI::Pointer
is defined. This step verifies thatrequire "ffi"
has been executed andFiddle::Pointer
is available.
if(NIL_P(rb_cairo__cFFIPointer)){rb_raise(rb_eNotImpError,"%s: FFI::Pointer is required",rb_id2name(rb_frame_this_func()));}
Therb_cairo__cFFIPointer
is pre-set inInit_cairo_private.
voidInit_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. AsFFI::Pointer
andFiddle::Pointer
obtain addresses using relatively common names -address
andto_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 theaddress
method.
# Ruby-FFIpt=FFI::MemoryPointer.new(:int)ppt.address
With Fiddle,to_i
method helps in getting the address.
# Fiddlept=Fiddle::Pointer.new(Fiddle::SIZEOF_INT)ppt.to_i
In C extensions, these Ruby methods are invoked usingrb_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.
VALUErb_cr_address;rb_cr_address=rb_funcall(pointer,rb_intern("address"),0);cr=NUM2PTR(rb_cr_address);cr_check_status(cr);
Here, theNUM2PTR
macro is not provided byruby.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
Thecr_check_status
function calls the native Cairo functioncairo_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);
Userb_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 yourruby-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
andpiyo.c
have been prepared as targets to create bindings.
piyo.h
#ifndef PIYO_H#define PIYO_H#include<stdio.h>typedefstructPiyo{intage;char*name;}Piyo;voiddisplayPiyoInfo(constPiyo*piyo);#endif
piyo.c
#include"piyo.h"voiddisplayPiyoInfo(constPiyo*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'modulePiyoPiyo=Fiddle::Importer.struct(['int age','char* name'])endtori_name='piyoko'Piyo::Piyo.malloc(Fiddle::RUBY_FREE)do|piyo|piyo.age=100piyo.name=tori_namePiyo.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)))#endifVALUErb_cFiddlePointer;VALUErb_display_info(VALUEself,VALUEpiyo){Piyo*ptr;VALUErb_address=rb_funcall(piyo,rb_intern("to_i"),0);ptr=NUM2PTR(rb_address);displayPiyoInfo(ptr);returnQnil;}voidInit_piyo(void){VALUEmPiyo=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.rbmake
Execute with:
ruby test.rb
If everything runs correctly, you should see the output as:
Name: piyokoAge: 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)
For further actions, you may consider blocking this person and/orreporting abuse