園児ニア日記

RubyのFiddleやFFIでOpaque Pointerを扱う

とりあえずRustで簡単なShared Libraryを用意する

use std::ffi::{CStr, CString};
use std::os::raw::c_char;

pub type StringArray = Vec<CString>;

#[no_mangle]
pub extern fn string_array_new() -> *mut StringArray {
    Box::into_raw(Box::new(StringArray::new()))
}

#[no_mangle]
pub extern fn string_array_destroy(string_array: *mut StringArray) {
    unsafe {
        Box::from_raw(string_array);
    }
}

#[no_mangle]
pub extern fn string_array_add(string_array: *mut StringArray, value: *const c_char) {
    unsafe {
        (*string_array).push(CStr::from_ptr(value).to_owned());
    }
}

#[no_mangle]
pub extern fn string_array_for_each(string_array: *const StringArray, callback: extern fn(*const c_char)) {
    unsafe {
        (*string_array).iter().for_each(|value| callback(value.as_ptr()));
    }
}

↑のコードを cargo build --release すると target/releaselibstringarray.so ができるので適当なところにコピーする

FiddleでOpaque Pointerを扱う

とりあえずFiddleの公式ドキュメントに書いてあるstructを使ってみる

require 'fiddle/import'

module StringArrayFiddle
  extend Fiddle::Importer
  dlload './libstringarray.so'

  StringArray = struct([])
end
$ bundle exec ruby main.rb
Traceback (most recent call last):
	6: from main.rb:3:in `<main>'
	5: from main.rb:7:in `<module:StringArrayFiddle>'
	4: from /usr/lib/ruby/2.6.0/fiddle/import.rb:224:in `struct'
	3: from /usr/lib/ruby/2.6.0/fiddle/struct.rb:64:in `create'
	2: from /usr/lib/ruby/2.6.0/fiddle/struct.rb:112:in `size'
	1: from /usr/lib/ruby/2.6.0/fiddle/pack.rb:55:in `align'
/usr/lib/ruby/2.6.0/fiddle/pack.rb:55:in `%': nil can't be coerced into Integer (TypeError)

これは動きません!
Fiddleのstructは最低でも一つは要素がないと使えないのです。
困りましたね。ドキュメントを見てもstruct以外に構造体を宣言できそうなものがありません。

答え 宣言などせずにいきなり使う

宣言無しでいきなり使うと動きます。

require 'fiddle/import'

module StringArrayFiddle
  extend Fiddle::Importer
  dlload './libstringarray.so'

  extern 'StringArray* string_array_new(void)'
  extern 'void string_array_destroy(StringArray *string_array)'
  extern 'void string_array_add(StringArray *string_array, const char* value)'
  extern 'void string_array_for_each(const StringArray *string_array, void (*callback)(const char*))'
end

# 以下動作確認用コード
string_array = StringArrayFiddle.string_array_new
StringArrayFiddle.string_array_add(string_array, 'aaaa')
StringArrayFiddle.string_array_add(string_array, 'bbbb')
callback = Fiddle::Closure::BlockCaller.new(Fiddle::TYPE_VOID, [Fiddle::TYPE_VOIDP]) { |str| puts str }
StringArrayFiddle.string_array_for_each(string_array, callback)
StringArrayFiddle.string_array_destroy(string_array)
$ bundle exec ruby main.rb
aaaa
bbbb

FFIでOpaque Pointerを扱う

次はFFIでOpaque Pointerを扱うやり方
FFIの公式ドキュメントにあるように FFI::Struct を継承してあげればできそうですね

require 'bundler/setup'
require 'ffi'

module StringArrayFFI
  extend FFI::Library
  ffi_lib './libstringarray.so'

  class StringArray < FFI::Struct; end
  attach_function :string_array_new, [], StringArray.by_ref
end

# 以下動作確認用コード
string_array = StringArrayFFI.string_array_new
$ bundle exec ruby main.rb
Traceback (most recent call last):
	3: from main.rb:33:in `<main>'
	2: from main.rb:33:in `string_array_new'
	1: from main.rb:33:in `from_native'
main.rb:33:in `initialize': no Struct layout configured for StringArrayFFI::StringArray (RuntimeError)

これも動きません!
Fiddle同様FFIも最低でも一つは要素がないと使えないようです。

答え :pointerを使う

require 'bundler/setup'
require 'ffi'

module StringArrayFFI
  extend FFI::Library
  ffi_lib './libstringarray.so'

  attach_function :string_array_new, [], :pointer
  attach_function :string_array_destroy, [:pointer], :void
  attach_function :string_array_add, [:pointer, :string], :void
  callback :string_array_for_each_callback, [:string], :void
  attach_function :string_array_for_each, [:pointer, :string_array_for_each_callback], :void
end

# 以下動作確認用コード
string_array = StringArrayFFI.string_array_new
StringArrayFFI.string_array_add(string_array, 'aaaa')
StringArrayFFI.string_array_add(string_array, 'bbbb')
StringArrayFFI.string_array_for_each(string_array, -> str { puts str })
StringArrayFFI.string_array_destroy(string_array)
$ bundle exec ruby main.rb
aaaa
bbbb