ローファイ日記

出てくるコード片、ぼくが書いたものは断りがない場合 MIT License としています http://udzura.mit-license.org/

参照先を更新する関数をPython/Rubyのffiで扱うには

諸事情Python の ctypes のコードを Ruby の fiddle に移植しているんですが、どちらもだいたい同じことができる、一方APIなどの設計思想が違う、みたいな感じで興味深いです(興味深い)。

今回ctypesで普通にやるような、「参照先を更新する関数」の呼び出しがfiddleの場合ちょっと癖があったのでメモ。

C側

#define INT_VALUE 1234567

int update_int(int* ptr) {
  *ptr = INT_VALUE;
  return 0;
}

これを shared ライブラリにする。

gcc -fPIC -c sample.c -o sample.o
gcc -shared sample.o -o libsample.so

ctypesでは

from ctypes import *
lib = CDLL("./libsample.so")

i = c_int()
print(lib.update_int(byref(i)))
print(i.value)
python3 ct.py
0
1234567

byref が癖ですが結構普通に扱える感じです。

docs.python.org

fiddleでは

コード全体です。

require 'fiddle/import'
module CLib
  extend Fiddle::Importer
  dlload "./libsample.so"
  extern "int update_int(*int)"
end

i = Fiddle::Pointer.malloc(Fiddle::SIZEOF_INT)
p CLib.update_int(i)
p i[0, i.size].unpack("i!")
ruby fiddle.rb
0
[1234567]

詳細

まず、 fiddle/import というライブラリを使い、特定のRuby側のモジュールとCのライブラリを紐付けます。 extend Fiddle::Importer; dlload "./libsample.so" の箇所ですね。

extern というメソッドで関数を取り出して使えるようになります。シンボルと同名のメソッドを定義します。この辺はるりまの通り。

docs.ruby-lang.org

で、 int を格納するための領域を作らないといけないんですけど、 Ruby の fiddle の場合各型ごとの専用のコンストラクタはなくて、 Fiddle::Pointer.malloc で汎用的に確保します。型ごとの(ビルドした環境での)サイズは定数があります。

docs.ruby-lang.org

あとはその Fiddle::Pointer インスタンスを引数に渡せば、参照先が更新されます。

されるんですが、 fiddle の場合はその値を取り出すのにも癖があって、メモリ上の表現を String#unpack で変換してあげる必要があります。

以下のように size 分のcharを取り出し、型が int なら例えば i! を指定します。

i[0, i.size].unpack("i!")

long, double などの場合も同様にできて、詳細は以下のテンプレート文字列の説明をご覧ください。本来エンディアンを考慮すべきですが、そこ含めて String#unpack がやってくれます。

docs.ruby-lang.org

最後に

基本的な型について上記の操作を確認する実装を以下のgistに置いておくので、ご参考まで。

Makefile · GitHub

なお

ffi.gem も存在します(上にfiddleより人気みたい...)が、今回は標準添付のライブラリfiddleを使いました。


各種バージョン情報です。

vagrant@ubuntu-bionic:~$ ruby --version
ruby 2.5.1p57 (2018-03-29 revision 63029) [x86_64-linux-gnu]
vagrant@ubuntu-bionic:~$ python3 --version
Python 3.6.9
vagrant@ubuntu-bionic:~$ uname -a
Linux ubuntu-bionic 5.0.0-29-generic #31~18.04.1-Ubuntu SMP Thu Sep 12 18:29:21 UTC 2019 x86_64 x86_64 x86_64 GNU/Linux
vagrant@ubuntu-bionic:~$ gcc -v
Using built-in specs.
COLLECT_GCC=gcc
COLLECT_LTO_WRAPPER=/usr/lib/gcc/x86_64-linux-gnu/7/lto-wrapper
OFFLOAD_TARGET_NAMES=nvptx-none
OFFLOAD_TARGET_DEFAULT=1
Target: x86_64-linux-gnu
Configured with: ../src/configure -v --with-pkgversion='Ubuntu 7.4.0-1ubuntu1~18.04.1' --with-bugurl=file:///usr/share/doc/gcc-7/README.Bugs --enable-languages=c,ada,c++,go,brig,d,fortran,objc,obj-c++ --prefix=/usr --with-gcc-major-version-only --program-suffix=-7 --program-prefix=x86_64-linux-gnu- --enable-shared --enable-linker-build-id --libexecdir=/usr/lib --without-included-gettext --enable-threads=posix --libdir=/usr/lib --enable-nls --with-sysroot=/ --enable-clocale=gnu --enable-libstdcxx-debug --enable-libstdcxx-time=yes --with-default-libstdcxx-abi=new --enable-gnu-unique-object --disable-vtable-verify --enable-libmpx --enable-plugin --enable-default-pie --with-system-zlib --with-target-system-zlib --enable-objc-gc=auto --enable-multiarch --disable-werror --with-arch-32=i686 --with-abi=m64 --with-multilib-list=m32,m64,mx32 --enable-multilib --with-tune=generic --enable-offload-targets=nvptx-none --without-cuda-driver --enable-checking=release --build=x86_64-linux-gnu --host=x86_64-linux-gnu --target=x86_64-linux-gnu
Thread model: posix
gcc version 7.4.0 (Ubuntu 7.4.0-1ubuntu1~18.04.1)