いものやま。

雑多な知識の寄せ集め

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

残すはFFI::MemoryPointerの話のみ。

OUT引数

Cのインタフェースを設計するときに、関数にポインタを渡し、ポインタを介することで関数の出力を受け取るようにするということがよくある。

例えば、次のようなコードが一例。

/* rbuf.h */
typedef struct _ring_buffer rbuf_t;
typedef unsigned int uint_t;

void
rbuf_create(
  rbuf_t** rbuf,    // [OUT] created ring buffer
  uint_t length);   // [IN ] buffer length

uint_t              // size of wrote
rbuf_write(
  rbuf_t* rbuf,     // [IN ] ring buffer
  const char* data, // [IN ] data to write
  uint_t length);   // [IN ] data length

uint_t              // size of read
rbuf_read(
  rbuf_t* rbuf,     // [IN ] ring buffer
  char* buffer,     // [OUT] buffer to read
  uint_t length);   // [IN ] buffer length

void
rbuf_delete(
  rbuf_t* rbuf);    // [IN ] ring buffer to delete

こういったインタフェースのときに、OUT引数には、アプリケーション側でメモリを用意して、そのポインタを渡す必要が出てくる。

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

#include <stdio.h>
#include <string.h>
#include "rbuf.h"

int main()
{
  /* OUT引数用にメモリを用意 */
  rbuf_t* rbuf = NULL;
  char buffer[256];

  uint_t wrote = 0u;
  uint_t read = 0u;

  /* ring bufferを作成 */
  rbuf_create(&rbuf, 10u);

  /* データを書き込む */
  wrote = rbuf_write(rbuf, "0123456789abcdef", 16);
  printf("wrote: %d\n", wrote);  //=> wrote: 9

  /* データを読み込む */
  memset(buffer, 0, 256);
  read = rbuf_read(rbuf, buffer, 256);
  printf("read: %s (size %d)\n", buffer, read);  //=> read: 012345678 (size 9)

  /* ring bufferを削除 */
  rbuf_delete(rbuf);

  return 0;
}

けど、そのインタフェースをRubyから使おうとすると、ちょっと困ったことになる。
というのも、Rubyでネイティブなメモリを用意し、そのアドレスをライブラリに渡すなんてことは出来ないから。

これを解決する一つの方法としては、標準ライブラリのmalloc()を結びつけて使用する方法がある。
malloc()を使えば、ネイティブなメモリが確保されてそのポインタが返ってくるので、そのポインタを引数として渡せばいい。

けど、そんな面倒なことをしなくても大丈夫。 Ruby-FFIにはFFI::MemoryPointerという便利なクラスがちゃんと用意されている。

FFI::MemoryPointer

FFI::MemoryPointerは、オブジェクトを生成すると、ネイティブなメモリを確保し、そのポインタを(ラップして)返してくれる。

# malloc(sizeof(型));と同等
pointer = FFI::MemoryPointer.new(型)

# malloc(sizeof(型)*サイズ);と同等
pointer = FFI::MemoryPointer.new(型, サイズ)

Rubyの文字列からFFI::MemoryPointerのオブジェクトを生成することも出来る。
この場合、文字列のデータはネイティブなメモリ上にコピーされる。

# pointer = malloc(文字列のサイズ+1);
# memcpy(pointer, 文字列, 文字列のサイズ+1);と同等
pointer = FFI::MemoryPointer.from_string(文字列)

これを使うと、先ほどのCのコードは、Rubyだと次のようになる。

module MyLib
  extend FFI::Library
  ffi_lib 'rbuf'

  class RBuf < FFI::Struct
    # Rubyからアクセスすることがなくても、
    # ダミーでレイアウトを書いておかないとエラーになるっぽい
    layout(:dummy, :int)
  end
  attach_function :rbuf_create, [:pointer, :uint], :void
  attach_function :rbuf_write, [RBuf.ptr, :string, :uint], :uint
  attach_function :rbuf_read, [RBuf.ptr, :pointer, :uint], :uint
  attach_function :rbuf_delete, [RBuf.ptr], :void
end

# rbuf_t* rbuf = NULL;
# char buffer[256];
rbuf_handle = FFI::MemoryPointer.new :pointer
buffer = FFI::MemoryPointer.new :char, 256

# rbuf_create(&rbuf, 10u);
MyLib.rbuf_create(rbuf_handle, 10);
rbuf_pointer = rbuf_handle.read_pointer
rbuf = MyLib::RBuf.new rbuf_pointer

# wrote = rbuf_write(rbuf, "0123456789abcdef", 16);
# printf("wrote: %d\n", wrote);  //=> wrote: 9
wrote = MyLib.rbuf_write(rbuf, "0123456789abcdef", 16)
puts "wrote: #{wrote}"

# memset(buffer, 0, 256);
# read = rbuf_read(rbuf, buffer, 256);
# printf("read: %s (size %d)\n", buffer, read);  //=> read: 012345678 (size 9)
read = MyLib.rbuf_read(rbuf, buffer, 256)
str = buffer.read_string(read)
puts "read: #{str} (size #{read})"

# rbuf_delete(rbuf);
MyLib.rbuf_delete(rbuf);

rbuf_t**の扱いがちょっと分かりにくいかもしれないけど、逐一説明すると、

  1. rbuf_handle = FFI::MemoryPointer.new :pointer
    ネイティブメモリ上に、「ポインタを納めるメモリ」を確保し、そのメモリのポインタ(つまり、ポインタのポインターーハンドル)を得る。
  2. MyLib.rbuf_create(rbuf_handle, 10)
    ハンドルをrbuf_create()に渡すことで、そのハンドルの指し示す先(ポインタを納めるメモリ)に「作られたRBufオブジェクトのポインタ」を納めさせる。
  3. rbuf_pointer = rbuf_handle.read_pointer
    ハンドルの指し示す先(ポインタを納めるメモリ)のデータを(ポインタとして)読むことで、そこに納められた「作られたRBufオブジェクトのポインタ」を得る。
  4. rbuf = MyLib::RBuf.new rbuf_pointer
    「作られたRBufオブジェクトのポインタ」をキャストして、RBufオブジェクトを得る。

という感じ。

FFI::MemoryPointerのメソッド

オブジェクトを作成することでネイティブなメモリを確保できるけれど、当然そのメモリに読み書きが出来ないと使い物にはならないので、そのためのメソッドが用意されている。
(なお、これらはFFI::MemoryPointerの親であるFFI::Pointer(や、さらにその親であるFFI::AbstractMemory)で定義されているので、FFI::Pointerでも使用可能)

よく使いそうなものとしては、以下。

メソッド 説明
clear メモリをゼロクリアする
read_int ポインタの位置の整数値を読み込む
write_int(value) ポインタの位置に整数値valueを書き込む
read_float ポインタの位置の浮動小数点数を読み込む
write_float(value) ポインタの位置に浮動小数点数valueを書き込む
read_double ポインタの位置の倍精度浮動小数点数を読み込む
write_double(value) ポインタの位置に倍精度浮動小数点数valueを書き込む
read_pointer ポインタの位置のポインタの値を読み込む
write_pointer(value) ポインタの位置にポインタの値valueを書き込む
read_bytes(length) ポインタの位置から長さlengthバイトを読み込み、バイト列として返す
write_bytes(str, index=0, length=nil) ポインタの位置にバイト列strを書き込む(indexやlengthを指定すると、部分バイト列を書き込む)
read_string(length=nil) ポインタの位置から文字列を読み込む(lengthを指定すると、その長さ分だけ読み込む)
write_string(str, length=nil) ポインタの位置に文字列strを書き込む(lengthを指定すると、その長さ分だけ書き込む)

なお、intのところでは、shortやuint、int64といった型も利用できる。
また、オフセットを指定できる put_type、get_typetypeはintやbytesなど)もある。

他のメソッドや詳細は、MemoryPointerのrdocを参照。

オートリリース

FFI::MemoryPointerでネイティブな領域に確保されたメモリは、FFI::MemoryPointerのオブジェクトがGCで破壊されるタイミングで、自動的で解放される。

これと同様のことを構造体でやりたい場合、FFI::StructのかわりにFFI::ManagedStructを継承するようにする。
そうすると、オブジェクトが破壊されるタイミングでreleaseクラスメソッドが呼び出されるので、そこでメモリの解放を行うことが出来るようになる。

例えば、先ほどの例だと、次のようにしておく。

class RBuf < FFI::ManagedStruct
  # レイアウトはダミー
  layout(:dummy, :int)
  
  # オブジェクトが破壊されるタイミングでメモリの解放をする
  def self.release(ptr)
    MyLib.rbuf_delete(ptr)
  end
end

今日はここまで!