いものやま。

雑多な知識の寄せ集め

Ruby-FFIについて調べてみた。(その2)

昨日の続き。

Ruby-FFIのコア・コンポーネント

Ruby-FFIを理解する上で、重要なクラス、モジュールがいくつかある。

  • FFI::Library
    ライブラリのロードや、グローバル変数、関数を結びつける機能などを提供する。
  • FFI::Pointer
    ライブラリで確保されたネイティブなメモリを参照する機能などを提供する。
  • FFI::MemoryPointer
    Ruby側でネイティブなメモリを確保し、それを参照する機能などを提供する。
  • FFI::Struct, FFI::Union
    構造体や共用体の構造を記述する機能などを提供する。

重要なのは、メモリに「Rubyが使用するメモリ(ライブラリからは参照できない)」と「ライブラリが使用するネイティブなメモリ(Rubyからは参照できない)」の2つがあるということ。
そこで、ライブラリで確保されたデータにアクセスするためのFFI::Pointerが必要になってくるし、Ruby側でネイティブなメモリを確保するためのFFI::MemoryPointerが必要になってくる。

データ構造

何事もまずはデータ構造から。

Cの標準的な型を表すシンボル

Ruby-FFIでは、Cの標準的な型を意味するシンボルが用意されている。
これらは、構造体の構造を記述したり、関数のインタフェースを結びつけるときに使用する。

シンボル 意味
:char char
:uchar unsigned char
:short short
:ushort unsigned short
:int int
:uint unsigned int
:long long
:ulong unsigned long
:long_long long long
:ulong_long unsigned long long
:int8 8-bitの整数型
:uint8 8-bitの符号なし整数型
:int16 16-bitの整数型
:uint16 16-bitの符号なし整数型
:int32 32-bitの整数型
:uint32 32-bitの符号なし整数型
:int64 64-bitの整数型
:uint64 64-bitの符号なし整数型
:float float (32-bitの浮動小数点数)
:double double (64-bitの浮動小数点数)
:pointer ポインタ
:string C文字列

なお、:stringは使える場所がかなり限られている感じ。(C++const char * constと宣言できる場所で使えるイメージ)
:stringが使えない場合、:pointerで代用することになる。

構造体や共用体の構造の記述

構造体や共用体の構造を記述するには、FFI::StructやFFI::Unionを使用する。

class (定義する構造体) < FFI::Struct
  layout(
    (構造体のメンバ1), (メンバ1の型), 
      ...
    (構造体のメンバN), (メンバNの型))
end

例えば、次のような感じ。

class Point2D < FFI::Struct
  layout(
    :x, :double,
    :y, :double)
end

構造体のメンバには、ハッシュと同様にアクセスすることが出来る。

# 構造体のオブジェクトを作成
# ※構造体のデータ自体はネイティブなメモリに確保される
point = Point2D.new

# メンバに代入
point[:x] = 1.0
point[:y] = 2.0

# メンバを参照
puts point[:x]  #=> 1.0
puts point[:y]  #=> 2.0

複雑な構造体の記述

具体的には、以下のケース。

  1. 構造体の中に配列を含む
  2. 構造体の中に別の構造体/共用体を含む
  3. 構造体の中に別の構造体/共用体へのポインタを含む
1. 構造体の中に配列を含む

例えば、次のように、構造体の内部にバッファを確保していたりする場合がある。

#define BUF_SIZE  (1024)
typedef struct {
  unsigned int current_read;
  unsigned int current_write;
  char buffer[BUF_SIZE];
} RingBuffer;

この場合、メンバの型を書くときに[配列の型, 配列のサイズ]とする。

class RingBuffer < FFI::Struct
  BUF_SIZE = 1024
  layout(
    :current_read, :uint,
    :current_write, :uint,
    :buffer, [:char, BUF_SIZE])
end
2. 構造体の中に別の構造体/共用体を含む

例えば、次のように、複数の構造体を内部に持つような構造体が定義されている場合がある。

typedef struct {
  double x;
  double y;
} Vector;

typedef struct {
  Vector position;
  Vector speed;
} Node;

この場合、メンバの型として構造体名を書く。

class Vector < FFI::Struct
  layout(
    :x, :double,
    :y, :double)
end

class Node < FFI::Struct
  layout(
    :position, Vector,
    :speed, Vector)
end

アクセスするときには、入れ子のハッシュのようにすればいい。

node = Node.new
node[:position][:x] = 1.0
3. 構造体の中に別の構造体/共用体へのポイントを含む

例えば、次のようにリスト構造を作ることを考える。

typedef struct _list {
  int value;
  struct _list *next;
} List;

この場合、一つの方法として、メンバの型に:pointerを使う方法がある。

class List < FFI::Struct
  layout(
    :value, :int,
    :next, :pointer)
end

こうした場合、次のような感じで使うことになる。

first = nil
before = nil

# 10個作成し、連結
10.times do |i|
  list = List.new
  list[:value] = i
  if before.nil?
    first = list
  else
    # 直前のリストの次の要素として自分を設定する。
    # :pointer型なので、FFI::Struct#pointerでポインタを得る。
    before[:next] = list.pointer
  end
  before = list
end

# 順番に参照していく
list = first
loop do
  puts list[:value]
  next_list_pointer = list[:next]
  if next_list_pointer == nil
    break
  else
    # ポインタを構造体へキャストする。
    list = List.new next_list_pointer
  end
end

ポイントは次の2点。

  • 構造体のポインタを得るには、FFI::Struct#pointerを使う。
  • ポインタからそのポインタの指す先の構造体を得るには、FFI::Struct.newの引数としてポインタを渡す。

ただ、ちょっと書くのが煩わしい・・・

そこで、もう一つの方法として、メンバの型に(構造体名).ptrを使う方法もある。
これを使うと、Ruby-FFIはそのメンバの型が構造体のポインタであると理解して、うまいこと処理してくれる。

# 構造体名が使えるように前方宣言しておく
class List < FFI::Struct; end

class List
  layout(
    :value, :int,
    :next, List.ptr)
end

この定義だと、先ほどの例は次のように書ける。

first = nil
before = nil

# 10個作成し、連結
10.times do |i|
  list = List.new
  list[:value] = i
  if before.nil?
    first = list
  else
    # 直前のリストの次の要素として自分を設定する。
    # before[:next]に構造体を代入すると、Ruby-FFIはそのポインタを代入してくれる。
    before[:next] = list
  end
  before = list
end

# 順番に参照していく
list = first
loop do
  puts list[:value]
  # list[:next]を参照すると、Ruby-FFIは構造体を返してくれる
  list = list[:next]
  break if list.pointer == nil
end

ポインタと構造体との変換をRuby-FFI側でやってくれるので、お手軽。

列挙型

列挙型を定義するにはいくつかの方法があるっぽい。
けど、基本的にはFFI::Library#enumを使って、次のようにしておくのがよさそう。

(定義する列挙型) = enum(
  (列挙子1), (列挙子1Cでの値),
    ...
  (列挙子N), (列挙子NのCでの値))
# Cでの値は省略可能

例えば、次のような感じ。

Day = enum(
  :sunday, 1,
  :monday,
  :tuesday,
  :wednesday,
  :thursday,
  :friday,
  :saturday)

こうしておくと、以下のようなメリットがある。

  • 構造体の定義や関数の結びつけを行うときに、型として列挙型の型名を書けば、RubyのシンボルとCでの値の変換をRuby-FFI側でうまいことやってくれる。
  • Rubyのコードで列挙子のCでの値を知りたい場合、列挙型[列挙子]とすることで参照できる。

例えば、次のような感じになる。

module MyLib
  extend FFI::Library
  
  Type = enum(
    :triangle, 3,
    :rectangle,
    :pentagon)
  
  class Figure < FFI::Struct
    layout(
      :type, Type,
      :height, :uint,
      :width, :uint)
  end
end

figure = MyLib::Figure.new

# Ruby内では、Ruby-FFIがうまいことやってくれるので、シンボルで処理できる
figure[:type] = :triangle
p figure[:type]  #=> :triangle

# ネイティブなメモリ上のデータを読むと、Cの値である3が書き込まれている
pointer = figure.pointer
p pointer.get_int(0)  #=> 3

今日はここまで!