LoginSignup
7
4

RubyでFFIやFiddleとC拡張を混ぜて使いたい話

Last updated at Posted at 2023-10-26

はじめに

RubyからC言語の関数を呼ぶときにはFFIFiddleといったgemを使うと便利です。

FFIは非常に機能が充実しており、大抵のことなら解決できます。Fiddleはやや利便性に劣りますが、Ruby公式のGemなので、ほぼすべての環境で動作することが魅力です。

FFIやFiddleとC拡張を一緒に使いたい

FFIやFiddleで実装したGemを一部だけ、C拡張に書き換えたいというようなケースがありました。libffiを使った関数の呼び出しは、ネイティブC拡張よりも100倍近く遅いという噂もあり、呼び出しが回数が多く、なおかつ速度が求められるケースでは、一度FFIで実装した関数を、C拡張に書き換えたくなる場合もあるでしょう。

方法

基本的な考え方

問題は、FFIやFiddleの構造体のポインタを、C拡張の関数の引数でどのように取ればいいかということだと思います。結論は簡単で、Rubyのオブジェクトである Fiddle::PointerFFI::Pointer からメモリのアドレスを入手すればいいです。

ここでは、rcairo で FFI::Pointer を引数にとるC拡張の関数を見て、どのようにすればいいか学んでいきます。

定数 FFI::Pointer が定義されているか確認する

まずは定数 FFI::Pointer が定義されているかどうか確認します。これは require "fiddle" が行われてFiddle::Pointer が利用可能な状態であるかどうかの確認です。

  if (NIL_P (rb_cairo__cFFIPointer))
    {
      rb_raise (rb_eNotImpError,
                "%s: FFI::Pointer is required",
                rb_id2name (rb_frame_this_func ()));
    }

ここの rb_cairo__cFFIPointerInit_cairo_privateであらかじめ定義されています。

void
Init_cairo_private (void)
{
  // -- 中略 --
  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;
    }
}

Fiddleの場合は

rb_const_get (rb_const_get (rb_cObject, rb_intern ("Fiddle")), rb_intern ("Pointer"));

などとすれば良いでしょう。

引数の型チェック

次に、引数のクラスが整合しているか確認します。FFI::Pointerでは address、Fiddle::Pointerでは to_i というかなり汎用的な名前のメソッドでアドレスを取得しますので、誤作動しないように型をチェックをしておいた方が安全でしょう。

  if (!RTEST (rb_obj_is_kind_of (pointer, rb_cairo__cFFIPointer)))
    {
      rb_raise (rb_eArgError,
                "must be FFI::Pointer: %s",
                rb_cairo__inspect (pointer));
    }

アドレスを取得する(Ruby)

FFIでは、adress メソッドでアドレスを取得できます。

# Ruby-FFI
pt = FFI::MemoryPointer.new(:int)
p pt.address

Fiddleでは、to_i メソッドでアドレスを取得できます。

# Fiddle
pt = Fiddle::Pointer.new(Fiddle::SIZEOF_INT)
p pt.to_i

C拡張では、rb_funcall を使ってこのRubyのメソッドを呼び出します。

rb_funcall (ffi_pointer, rb_intern ("address"), 0)
rb_funcall (fiddle_pointer, rb_intern ("to_i"), 0)

取得したアドレスを引数にしてCの関数を呼び出す

上記のRubyのコードをC拡張のコードの中で動かします。

VALUE rb_cr_address;
rb_cr_address = rb_funcall (pointer, rb_intern ("address"), 0);
cr = NUM2PTR (rb_cr_address);
cr_check_status (cr);

ここで NUM2PTR というマクロは、ruby.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

また cr_check_status という関数は、Cairoのネイティブ関数 cairo_status_to_string を呼び出しています。こういう感じの関数を一つ挟んでおくと安全ですね。

Rubyのオブジェクトの生成

もらったアドレスからRubyのオブジェクトを生成するために、以下のようにしています。

rb_cr = rb_obj_alloc (self);
cairo_reference (cr);
RTYPEDDATA_DATA (rb_cr) = cr;
rb_ivar_set (rb_cr, cr_id_surface, Qnil);

rb_obj_alloc でクラス(ここではself)のインスタンスを作ります。cairo_reference() はcairoの関数で、参照カウントを増やす操作だそうです。おそらく、Ruby-FFIのオブジェクトがGCで回収されても、メモリが解放されないようにするための処置ではないかと思います。RTYPEDDATA_DATA はTypedData Objectsのdataに直接アクセスするやつだと思います。rb_ivar_set はインスタンス変数を設定しています。

最小限の例

次に実際に最小限の例を作ってみます。
ここでは、バインディングを作る対象として、piyo.hpiyo.c を用意しました。

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);
}

これに対して、以下のようなコードが動作するようにC拡張を書きます。

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拡張

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);
}
extconf.rb
require 'mkmf'

find_header('piyo.h', __dir__)
create_makefile('piyo')

コンパイル

ruby extconf.rb
make

実行

ruby test.rb

無事に動作すると、以下のように表示されると思います。

Name: piyoko
Age: 100

これは最小限の例なので、クラス定義の確認、引数の型チェックなどは省略しています。実用的なGemにしていくには、これらのコードを追加していく必要があるでしょう。

この記事は以上です。

7
4
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
7
4