はじめに
RubyからC言語の関数を呼ぶときにはFFIやFiddleといったgemを使うと便利です。
FFIは非常に機能が充実しており、大抵のことなら解決できます。Fiddleはやや利便性に劣りますが、Ruby公式のGemなので、ほぼすべての環境で動作することが魅力です。
FFIやFiddleとC拡張を一緒に使いたい
FFIやFiddleで実装したGemを一部だけ、C拡張に書き換えたいというようなケースがありました。libffiを使った関数の呼び出しは、ネイティブC拡張よりも100倍近く遅いという噂もあり、呼び出しが回数が多く、なおかつ速度が求められるケースでは、一度FFIで実装した関数を、C拡張に書き換えたくなる場合もあるでしょう。
方法
基本的な考え方
問題は、FFIやFiddleの構造体のポインタを、C拡張の関数の引数でどのように取ればいいかということだと思います。結論は簡単で、Rubyのオブジェクトである Fiddle::Pointer
や FFI::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__cFFIPointer
はInit_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.h
と piyo.c
を用意しました。
#ifndef PIYO_H
#define PIYO_H
#include <stdio.h>
typedef struct Piyo
{
int age;
char *name;
} Piyo;
void displayPiyoInfo(const Piyo *piyo);
#endif
#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拡張
#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);
}
require 'mkmf'
find_header('piyo.h', __dir__)
create_makefile('piyo')
コンパイル
ruby extconf.rb
make
実行
ruby test.rb
無事に動作すると、以下のように表示されると思います。
Name: piyoko
Age: 100
これは最小限の例なので、クラス定義の確認、引数の型チェックなどは省略しています。実用的なGemにしていくには、これらのコードを追加していく必要があるでしょう。
この記事は以上です。