PharoからCで書いたプログラムを呼び出す(その2)

これはSmalltalk Advent Calendar 2018の記事です。

前回の記事でPharoからCのプログラムを呼び出す方法について説明しました。今回もその続きで、UFFIというメカニズムを使った外部インターフェイスについて説明します。

さて、数値のような単純なデータを渡すのは簡単ですが、配列のようなデータを受け渡すには少し注意が必要です。例えば次のようなC関数を考えます。

double fsum(double *list, int n)
{
double result = 0;
for (int i = 0; i < n; i ++) {
result += list[i];
}
return result;
}

double型の要素をn個持つ配列を引数で得て、その和を求めるような場合、Pharo 側では少し工夫しなければなりません。

まず、Cの関数を呼び出すメソッドを定義します。

fsum: anArray size: anInteger
^ self ffiCall: #(double fsum #(FFIExternalArray anArray , int anInteger)) module: UFFITestLibrary

配列のパラメータを指定する部分以外は前回の記事と変わりありません。double型の要素を持つ配列を受け渡す場合には、FFIExternalArray という型を明示的に指定します。また、このメソッドが使われる時には、anArray に FFIExternalArray クラスのインスタンスが指定される必要があります。

fsum: anArray
| size arr |
size := anArray size.
^ [ arr := FFIExternalArray externalNewType: 'double' size: size.
1 to: size do: [ :i | arr at: i put: (anArray at: i) ].
self fsum: arr size: size ]
ensure: [ arr free ]

#externalNewType:size: というメッセージを使って FFIExternalArray のインスタンスを作成します。そのオブジェクトに受け渡したい要素を一つずつ設定してから、先程のメソッドを使います。使い終わったところで、作成した FFIExternalArray のインスタンスを開放します。

Cの関数の中で配列の要素に変更を加えるような場合も似たような形となります。配列の各要素を定数倍する関数を考えてみます。

void fmultiplyBy(double *list, int n, double x)
{
for (int i = 0; i < n; i ++) {
list[i] *= x;
}
}

この関数を呼び出す Pharo のメソッドは以下のようになります。

fmultiply: anArray size: anInteger by: x
^ self ffiCall: #(double fmultiplyBy #(FFIExternalArray anArray , int anInteger , double x)) module: UFFITestLibrary

上のメソッドを使うために FFIExternalArray のインスタンスを作って渡すメソッドを用意します。

fmultiply: anArray by: x
| size arr |
size := anArray size.
[ arr := FFIExternalArray externalNewType: 'double' size: size.
1 to: size do: [ :i | arr at: i put: (anArray at: i) ].
self fmultiply: arr size: size by: x.
1 to: size do: [ :i | anArray at: i put: (arr at: i) ] ]
ensure: [ arr free ]

要するに FFIExternalArray インスタンスから要素を一つずつ取り出して元の配列を変更するわけです。

以上が配列などのデータを受け渡す場合の方法となります。

残念ながら大きな配列のやり取りの場合だと、要素の出し入れに余計なコストがかかるぶん効率は良くありません。特に今回のような例では、以下のようなコードの方がずっと早いです。

a := #(1 2 3 4) asFloatArray.
a *= 2.
a.

こちらは FloatArrayPlugin という VM プラグインを用いており、C言語の関数側で配列の処理を行っています。