DEV Community

kojix2
kojix2

Posted on

Mixing FFI, Fiddle, and C Extension in Ruby

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 ())); } 
Enter fullscreen mode Exit fullscreen mode

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; } } 
Enter fullscreen mode Exit fullscreen mode

In the case of Fiddle, execute:

rb_const_get (rb_const_get (rb_cObject, rb_intern ("Fiddle")), rb_intern ("Pointer")); 
Enter fullscreen mode Exit fullscreen mode

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)); } 
Enter fullscreen mode Exit fullscreen mode

Acquiring the Address

With FFI, you can get the address with the address method.

# Ruby-FFI pt = FFI::MemoryPointer.new(:int) p pt.address 
Enter fullscreen mode Exit fullscreen mode

With Fiddle, to_i method helps in getting the address.

# Fiddle pt = Fiddle::Pointer.new(Fiddle::SIZEOF_INT) p pt.to_i 
Enter fullscreen mode Exit fullscreen mode

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) 
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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); 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

piyo.c

#include "piyo.h"  void displayPiyoInfo(const Piyo *piyo) { printf("Name: %s\n", piyo->name); printf("Age: %d\n", piyo->age); } 
Enter fullscreen mode Exit fullscreen mode

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 
Enter fullscreen mode Exit fullscreen mode

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); } 
Enter fullscreen mode Exit fullscreen mode

Create Makefile with:

extconf.rb

require 'mkmf' find_header('piyo.h', __dir__) create_makefile('piyo') 
Enter fullscreen mode Exit fullscreen mode

Compile with:

ruby extconf.rb make 
Enter fullscreen mode Exit fullscreen mode

Execute with:

ruby test.rb 
Enter fullscreen mode Exit fullscreen mode

If everything runs correctly, you should see the output as:

Name: piyoko Age: 100 
Enter fullscreen mode Exit fullscreen mode

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)