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

## はじめに

RubyからC言語の関数を呼ぶときには[FFI](https://github.com/ffi/ffi)や[Fiddle](https://github.com/ruby/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](https://github.com/rcairo/rcairo) で FFI::Pointer を引数にとる[C拡張の関数](https://github.com/rcairo/rcairo/blob/85bb3401ee7f8d979db51107cae529185c07608d/ext/cairo/rb_cairo_context.c#L175-L218)を見て、どのようにすればいいか学んでいきます。

https://github.com/rcairo/rcairo/blob/85bb3401ee7f8d979db51107cae529185c07608d/ext/cairo/rb_cairo_context.c#L175-L218

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

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

```c
  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](https://github.com/rcairo/rcairo/blob/85bb3401ee7f8d979db51107cae529185c07608d/ext/cairo/rb_cairo_private.c#L235C1-L254)であらかじめ定義されています。

```c
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の場合は

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

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

### 引数の型チェック

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

```c
  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
# Ruby-FFI
pt = FFI::MemoryPointer.new(:int)
p pt.address
```

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

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

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

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

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

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

```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` では提供されていないので、下記のように自分で定義する必要があります。

```c
#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のオブジェクトを生成するために、以下のようにしています。

```c
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` を用意しました。

```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拡張を書きます。

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

```ruby:extconf.rb
require 'mkmf'

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

コンパイル

```
ruby extconf.rb
make
```

実行

```
ruby test.rb
```

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

```
Name: piyoko
Age: 100
```



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

この記事は以上です。