昨日の続き。
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. 構造体の中に配列を含む
例えば、次のように、構造体の内部にバッファを確保していたりする場合がある。
#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点。
ただ、ちょっと書くのが煩わしい・・・
そこで、もう一つの方法として、メンバの型に(構造体名).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), (列挙子1のCでの値), ... (列挙子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
今日はここまで!