外部関数インターフェース
はじめに
外部関数インターフェース(FFI)を使うことで,Wolfram言語は外部ライブラリからインポートされた関数を呼び出すことができる.このライブラリは動的共有ライブラリでなければならないが,特別なインターフェース層を加えるための修正は必要ない.ライブラリが別のプログラム(例えばCで書かれたもの)から使えるのであればFFIで呼び出せる.これだとコンパイルされたコードが生成されないためすぐ設定できる.
Wolfram言語FFIの機能として,多様な型(原子型と複合型の両方)との対応,コールバックの使用のサポート,非常に低レベル構造への対応,自動メモリ管理システムとの統合が挙げられる.
主な関数はForeignFunctionLoadである.ここでは,引数と結果のタイプを与えて,例のライブラリ"compilerDemoBase"から関数"addone"をロードするために使う.結果はForeignFunctionオブジェクトになる:
この関数はWolfram言語関数についての通常の方法で呼び出すことができる:
この関数が呼ばれると,式でありWolframインタープリタからの入力はネイティブのC型に変換される.この変換の形式がWolfram Compilerの主要部分である.
FFIを介してライブラリを使うWolfram言語関数はライブラリを相対パスまたは絶対パスのどちらでも取ることができる.またプラットフォーム特有の拡張子は除外できる.
ライブラリ
ライブラリを使うときに便利な関数がFindLibraryであり,パクレットを探す絶対パス名および他のライブラリのパスを返す.これは使用中のプラットフォームに対する適切な拡張子を加えることもできる:
compilerDemoBaseライブラリはWolfram言語ディストリビューションに含まれている.ソースは以下で見ることができる:
このチュートリアルの例で使われる多数の他の関数およびaddone関数を示す.
ソースにはマクロDLLEXPORTの定義を得るためだけのヘッダファイル"WolframLibrary.h"が含まれている.このマクロは関数がライブラリからエキスポートされることを指定する.エキスポートの詳細がプラットフォームによって異なるという理由から,ヘッダファイルが含まれるのである.
既存のライブラリをロードすることもできる.またWolfram言語内で自分のライブラリを作成することもできる.このようなことはFFIの使い方を学ぶことにおいて非常に便利であり,このチュートリアルの他の例でも使われる.
関数の引数と結果
FFI関数の引数とFFI関数の結果は,多数の型をサポートするため,その型を指定することで決まる.ForeignFunctionLoadの関数ページにはそのリストがある.型の中にはプラットフォームの標準定義にマップする"CInt","CDouble"等の原子型がある.これらはWolfram Compilerでもサポートされる.このセクションではさまざまな型の使い方を示す.
ポインタ
ポインタは個々の値を保存する等のために関数に渡すことができるメモリブロックである.ポインタはFFI関数でサポートされている.その例がcompilerDemoBaseライブラリのaddonePointer関数である.Cソースを以下に示す.
int addonePointer (int in, int* out) {
*out = in + 1;
return 0;
}
この関数は以下のようにForeignFunctionLoadを使ってFFIで呼び出すことができる.ポインタ引数には,ポインタが指すもの(この場合は"CInt")をラップする"RawPointer"が与えられる.この形式の型指定はWolfram Compilerの一部である:
もちろんこれで関数が生成されるが,それを呼び出すためにはポインタに作用するWolfram式が作成できなければならない.このためにはRawMemoryAllocateを使う:
RawMemoryAllocateの結果はRawPointerを含むManagedObjectである.何かを管理することで,メモリが使われれなくなったときに収集することができる.これについては後のセクションで説明する.通常ManagedObjectが管理しているものを想定する場所で,ManagedObjectを使うことができる.
ポインタに保存された値を見るためにはRawMemoryReadを使う:
ポインタに値を保存する関数をロードする方法を示した.適切なポインタが作成され,関数が呼び出され,保存された値が取り出された.
RawMemoryAllocateの結果は実は"ManagedObject"にラップされた"RawPointer"である.これは生のメモリブロックにメモリ管理システムを与える.詳細は後ほど説明する.
重要なポインタの型に"OpaqueRawPointer"がある.これは,特に何も指してはいないポインタであるC型のvoid*に相当する.
配列
配列は別の形式のメモリ使用であり,同じ型の要素を一つまたは複数個保存することのできるメモリブロックである.配列もFFI関数でサポートされている.compilerDemoBaseライブラリのpopulateArray関数で使われる例を示す.
int populateArray(long* arr, long len) {
for(int i = 0; i < len; i++)
arr[i] = i*i;
return 0;
}
この関数はForeignFunctionLoadの適切な呼出しでFFIで呼び出すことができる.この配列には,配列が保持するものの型を持つ"RawPointer"型(この場合は"CLong")が与えられる:
もちろんこれで関数が生成されるが,それを呼び出すためにはポインタに作用するWolfram式が作成できなければならない.このためにはRawMemoryAllocateを使う:
配列のすべての内容はRawMemoryImportでリストに読み込むことができる:
配列の最初の値はRawMemoryReadで読むことができる:
RawMemoryReadは配列の最初からのオフセットを取ることもできる(これは部分数ではなくオフセットである):
RawMemoryImportはNumericArrayを返すことができる.データの型を保持したい場合に便利である:
RawMemoryImportはByteArrayを返すことができる.これはメモリから生のバイトを読み込むので,データ要素が1バイトよりも大きければ長さを調整する必要がある.この場合各整数は8バイトである:
実際のバイト数を見ると,大きいデータ要素を作成するためにどのように保存されているかが分かる(エンディアンと言われる).生のメモリを扱うことで,以下のような下位レベルの詳細になる:
RawMemoryImportおよびRawMemoryExportは後のセクションでより詳しく説明する.
文字列
文字列はC言語でゼロバイトが最後にあるバイトの配列として表現される.文字列を作成して返すCプログラムを以下に示す.
この例はC言語でライブラリを作成するが,ライブラリを作成するのにCと互換の他の言語を使うこともできる.
このプログラムはWolfram言語でライブラリにコンパイルすることができる(これにはマシンにCコンパイラがインストールされていなければならない):
これでForeignFunctionLoadを使って関数をロードする.返り値は"UnsignedInteger8"の配列である:
関数が呼ばれる.結果は新しいメモリブロックである.これは管理されていないので,何らかの形で解放されるとメモリは失われる:
生データは文字列にインポートすることができる.文字列はヌル終端なので,長さを与える必要はない:
"String"形式を使ったRawMemoryImportは長さが分かるため便利である.しかし長さは与えることができる:
構造体
構造体は通常いくつかの関連するデータ要素を保存するために導入されるデータ型である.構造体はC言語のような最もコンパイルされる言語でサポートされる.以下に構造体のポインタを返す例のコードを示す:
このプログラムはWolfram言語を使ってライブラリにコンパイルすることができる(このためにはマシンにCコンパイラがインストールされていなければならない):
これでForeignFunctionLoadを使って関数がロードされる.返されるのは文字列と"CInt"を含む構造体へのポインタである.この構造体は{ }の内部にフィールドのタイプを置くことで形成される.フィールドには名前を与える必要はない:
関数が呼ばれる.結果は生のメモリブロックである.これは管理されていないので,解放されるとメモリは失われる(これについては後のセクションで説明する):
データは RawMemoryReadでインポートすることができる.これはリストに構造体のフィールドを返す:
引数としての構造体
構造体は引数として渡すことができる.これは構造体が渡される例である(これは構造体へのポインタではない):
関数をロードする.引数は2つのフィールド両方に"CInt"型を持つ構造体である:
関数を呼び出すと,構造体はその引数をリスト{args…}に置くことによって作成される:
別の構造体にネストされた構造体
関数をロードする.引数は2つのフィールド両方に"CInt"型を持つ構造体である:
RawMemoryReadでポインタの引数をインポートする."CInt"と別の構造体へのポインタの2つのフィールドがある:
コールバック関数
コールバック関数を使うと,外部ライブラリがそれらを呼び出した環境から渡されたコードを実行することができる.以下にcompilerDemoBaseライブラリのcreateArray関数の例を示す.
long* createArray(long (*fun)(long)) {
long* out = (long*)malloc(sizeof(long) * 10);
for(long i = 0; i < 10; i++) {
out[i] = fun(i);
}
return out;
}
この関数は以下に示す通り,ForeignFunctionLoadへの適切な呼出しを介してFFIを使って呼び出すことができる.コールバックが"OpaqueRawPointer"型として渡される.つまり多くのものが渡せるということである.しかし適切な関数ではないものが渡されると困ったことが起こる:
評価子を呼び出すコールバックはCreateForeignCallbackで簡単に作成できる:
生データ
このセクションでは外部関数とWolfram言語の間を行き来する生データがどのように扱われるかについて説明する.主な点として,Wolfram言語でのデータの作成,その書出しと読込み,どのように解放できるか等の管理方法が挙げられる.
外部関数の割当て
compilerDemoBaseライブラリのcreateArray関数は,以下に示すようにCメモリアロケータmallocを使ってデータを作成する.この関数が呼ばれると,割当てが完了しデータがWolfram Evaluatorに返される.
long* createArray(long (*fun)(long)) {
long* out = (long*)malloc(sizeof(long) * 10);
for(long i = 0; i < 10; i++) {
out[i] = fun(i);
}
return out;
}
このメモリはCのメモリ関数freeを呼び出すことで解放することができる.これはFFI関数またはWolfram Compiler関数で行える.これを自動的に行うためには,次のようにManagedObjectでラップする.
Wolfram言語の割当て
データはWolfram言語で作成してFFI関数に渡すことができる.RawMemoryAllocateは初期化されていないデータを作成し,管理された生のポインタを返す.初期化されていないデータはFFI関数に渡してそこで初期化することもできれば,RawMemoryWriteで書き出すこともできる.
ポインタについての情報を見ることができる.ポインタの特徴に関する詳細は後に説明する:
RawMemoryAllocateは配列を割り当てることもできる:
配列の要素を満たすためには,各場所に書き込むオフセットを使うループが必要である.オフセットは0から始まる:
RawMemoryReadは要素を読み込むことができる.上で述べた通り,これはオフセットが0から始まるループで行わなければならない:
並列と配列へのポインタは異なることを理解することが必要である.この例では"UnsignedInteger8"の生のポインタへのポインタが作成される:
これに書き込むためには,RawMemoryExportで作成できる"UnsignedInteger8"の生のポインタが必要である:
これで文字列を保存するポインタを使ってポインタを書き込むことができる:
RawMemoryReadは保存されている値を読むがそれを深くは解釈しない.したがって,ポインタからポインタを読み込むとポインタが戻る:
まとめると,RawMemoryAllocateはポインタと配列を作成することができる.RawMemoryWriteはそれらを充填することができるが一度に一要素だけである.RawMemoryReadはポインタおよび配列から読み取ることができるが,一度に一要素だけである.
エキスポートとインポート
RawMemoryExportとRawMemoryImportは生のポインタや配列の操作について,割当て,読込み,書出しの関数よりも高レベルの関数を提供する.
以下は,値10に初期化された"Integer16"へのポインタを作成する:
リストが使われる場合,適切な値で初期化された配列が作成される:
このデータはRawMemoryImportを使ってリストにインポートすることができる:
これはNumericArrayにインポートすることもできる.これは結果にデータの型を維持する:
ByteArrayにインポートすることもできる.これは結果をバイトとして返す:
インポートされる配列はバイトサイズだったので,結果はもとのデータと同じ長さになる:
ここではインポートされる配列に2バイトを含む要素があり,これは出力のバイト配列に反映されている:
RawMemoryExportとRawMemoryImportの方が配列データの操作には便利である:
文字列
文字列のような特定の式には自動変換機能があるため,文字列では型の引数は必要ない(NumericArrayとByteArrayの場合も同様である):
文字列に保存されている実際のバイト数をインポートすることができる:
文字列には文字コードに関する問題もある.以下の文字列には非ASCIIの文字が含まれている:
しかし実際のバイト数を調べるとデフォルトのUTF8コードが表示される:
文字コードは変更することができる.ここではISOLatin1が使われている.3バイトだけが保存されている:
この文字列をデフォルトの文字コードでインポートしようとするとエラーメッセージが表示される:
デフォルト文字コードのUTF8は広く使われているので,通常はこのような文字コードの問題は考える必要はない.
文字列についての別の問題点として,文字列に実際に0バイトが含まれる場合のヌル終端というものがある.ここに1文字ゼロバイトの文字列がある.これがインポートされると,ゼロバイトの内容はヌル終端と解釈される:
C文字列を使うプログラムに渡される文字列はゼロバイトの内容を含むことができないので,これは実際には周辺的な問題である.
構造体
構造体はRawMemoryExportを使って操作することができる.次では構造体へのポインタを作成する.2レベルのリストを使っている点に注目のこと.一つは引数から構造体を作成するのに必要で,もう一つは配列に1要素あることを指定する:
RawMemoryImportを使ってデータを読み込むことができる:
構造体に文字列等,別の生データを含ませたい場合は明示的にこれを充填する:
RawMemoryImportで読み込む際,データは生の形式で戻り,それ以上深くは解釈されない.このためには別のステップが必要になる:
データには式木が戻ることを妨げる閉路が含まれている可能性があるため,RawMemoryImportがデータを深く解釈することは難しい.
生のポインタとメモリ管理
Wolfram言語のFFI関数の主な機能として生のメモリを扱うというものがある.このセクションではこれの仕組みとどのようにメモリが管理されるのかを説明する.
RawMemoryAllocateは型の要素に対してメモリを割り当てる.これは管理されたポインタまたは配列を返す:
Informationは結果に関して役立つ詳細を返す:
生のポインタを扱う関数の例としてaddonePointerがある:
この関数は生のポインタを第2引数として呼び出すことができる.生のポインタがManagedObjectでラップされていても,呼び出されたときになくなる:
ManagedObjectには実際には管理されていない生のポインタが含まれている.実際の生のポインタを抽出することができる:
ここでもInformationが便利な情報を返す:
割当てを管理する利点の一つとして,ポインタがWolfram言語で使われなくなったときにそのメモリが収集されるということが挙げられる.これはテストユーティリティからロードされるメモリリークのチェック関数を実行して例示することができる:
以下でRawMemoryAllocateを実行し,メモリがリークしていないことを示す:
FFI関数を呼び出すのにManagedObjectを使うと,値が抽出され渡される.この値はまだ管理されているため実際には値を借りているだけである.関数が借りた値をコントロールすることになっていると,それを保持するManagedObjectが解放されると安全ではなくなる.そのため借りた値も解放されるのである.
ManagedObjectより長く値を保存したい場合はUnmanageObjectを使うことができる:
ManagedObjectがもはやアクティブではないことが分かる:
UnmanageObjectがManagedObjectで使われるとメモリが失わる可能性がある.メモリリークチェッカーは,各実行についてどれだけのメモリが失われているかを検出する:
メモリリークを防ぐ方法として,管理されていない生のポインタでRawMemoryFreeを呼び出すというものがある.メモリリークチェッカーはTrueを返す.これはメモリが失われていないことを意味する:
管理されていないオブジェクトを取りそれを再び管理することもできる.最初にUnmanageObjectを呼ばなければよいので少し変ではあるが,これで管理できる:
Wolfram CompilerはManagedObjectもサポートする.コンパイルされたコードでは,適切にメモリが解放されるように確認しながら,低レベルのメモリ割当てを行うのに便利である.
ManagedObject辛い切り離された等の理由で解放を要求するRawMemoryAllocateで割り当てられたメモリは,RawMemoryFreeで解放されなければならない.
FFI割当てからの収集
FFIによって呼び出された関数で割り当てられたメモリがWolfram言語に戻されると,それを収集するために通常別の関数が必要となる.
次のコードにはメモリを割り当てる1つの関数とそれを解放する別の関数が含まれている.通常のアプリケーションでは,1つの関数が構造体データ型のインスタンスを返し,別の関数がそれ(および任意のリソース)を解放する:
次のプログラムはWolfram言語を使ってライブラリにコンパイルすることができる.これにはマシンにCコンパイラがインストールされていなければならない:
メモリを割り当てる関数と解放する関数 ForeignFunctionLoadでロードされる:
解放する関数を忘れずに呼び出さなければならないのは面倒なことがある.その場合解放関数を使ってCreateManagedObjectを使う.
これで管理されるオブジェクトが作成されると,使われなくなったときに解放されるようになる:
C関数のmalloc等の生の割当て関数で割り当てられたメモリは,C関数のfree等,対応する関数への呼出しで解放されなければならない.
生のポインタの変換
同じアドレスを使って"Real64"へのポインタとなる別のポインタを作成する:
"Integer64"ポインタに保存されたデータは予想通りである:
"Real64"ポインタに保存されたデータは整数のビットを浮動小数点数に変換したものである:
整数へのポインタを"OpaqueRawPointer"に変換することもできる:
整数への生のポインタを作成することもできる.以下はアドレス10で"Integer64"への生のポインタを作成する(これを使うと問題になる可能性がある):
同様の操作を"OpaqueRawPointer"について行うことができる.これもほぼ問題になる:
Cおよび他の言語で使われるヌルポインタを構築すると便利である.
同じメモリの場所を指すポインタを作成する重要な側面として,管理されているオブジェクトがその内容を解放するよう設定されるというものがある.1つでも参照が生きているとメモリは解放されない.これはメモリリークチェッカーで見ることができる:
このように生のポインタを扱うと,ポインタをさまざまな型に変換したり整数からポインタを作成したりできるので便利である.多くの言語ではこれをビットキャスティングと言う.もちろんこれは非常に危険であり,どのようなミスもプログラムの終了または未定義の挙動に繋がる.
関数ポインタ
ForeignPointerLookupを使うとライブラリからエキスポートされた関数のアドレスを抽出することができる.
以下はサンプルライブラリ"compilerDemoBase"の関数"addone"のアドレスを返す:
このポインタは別の関数へのコールバックとして使うことができる.
関数の型が分かっていればForeignFunctionLoadで使う:
関数の型が分かっていればForeignFunctionLoadで使う:
InformationはForeignFunctionオブジェクトに関する便利な情報も返す:
ライブラリ
ForeignFunctionLoadやForeignPointerLookup等のライブラリで使えるFFI関数は短縮された形式または完全パス名でライブラリの名前を取ることができる.
プラットフォーム用のライブラリの拡張子を加えることができる.このマシンではdylibである:
ライブラリを扱うFFI関数はFindLibraryを使ってライブラリの名前を見付ける.これは直接呼び出すことができる.この関数は絶対名を返す:
FindLibraryは$LibraryPathで指定された多数の標準的な場所を探す:
またライブラリの拡張子を持つパクレットも探す.効率化のため,多数のFFI関数がロードされている場合は,絶対名を作成するためにFindLibraryを一度使って,それをForeignFunctionLoadで使った方が速いことがある.
依存関係
ライブラリがロードされるとき,そのライブラリが必要とするライブラリもロードされなければならない.システムライブラリのパスが正しく設定されていれば,これは自動的に行われる.そうでない場合はLibraryLoadで最初にロードすることができる.
以下でcompilerDemoBaseライブラリをロードする:
ライブラリに循環依存関係(例えば2つのライブラリが互いに依存し合う関係)がある場合,唯一の解決策はシステムライブラリのパスを設定することである.このような場合は通常まれである.
Wolfram Compiler
Wolfram CompilerはFFIに使える型をサポートする.例えば生のポインタをコンパイルされたコードから返すことができる:
コンパイラ関数ToRawPointerも生のポインタを作成する.しかしこれは呼ばれる関数のスタックフレームに割り当てられる.したがって返すのは安全ではない.
コンパイルされたコードを使って"CArray"等の型を作成してこれを生のポインタにキャストすることもできる:
このような操作はFFIツールを作成したり使用したりするのに便利である.
管理
コンパイラでは管理されたオブジェクトを使うことができる.これは生データを別のプログラムに渡す準備をするのに便利であるがすべてが収集されるように確かめる必要がある.
コンパイラの中の管理されたオブジェクトのシンタックスはFFIシンタックスとはわずかに異なる.将来的にこれらは統合される予定である.
おもしろい例
OpenSSLライブラリの呼出し
OpenSSLライブラリはWolfram言語にバンドルされており,安全な通信をサポートする.このライブラリにはFFIで呼び出せるおもしろい関数がある.
次はライブラリへのパスを返す.これはプラットフォームによって異なるが,次は今使用中のプラットフォームでの値である:
OpenSSLライブラリにはランダムなバイトを生成する関数がある.この関数はバイトの配列と配列の長さを取る.これは次のようにロードすることができる:
Wolfram Compilerを使ってライブラリを呼び出す
Wolfram Compilerを使ってライブラリから関数を呼び出すこともできる.通常これには手間がかかり関数の生成も遅い.しかし簡単な実行パスがある.
Wolfram Compilerで作成されたライブラリでFFIを使う
Wolfram Compilerからライブラリを作成してそれをFFIで呼び出すことができる.生関数をエキスポートするCompiledComponentを作成すると便利である.
次はsquareという関数を1つ含む"demoLibrary"というコンポーネントを宣言する:
ライブラリを使うためには,コンポーネントをロードしなければならない:
Wolfram CompilerやFFIを使う以外にもライブラリを作成したり関数を呼び出したりする方法はたくさんある.これらすべてを組み合せて使うことが,自分に最も適したソリューションを見付けるよい方法となる.
Wolfram Compilerを使ってコールバックを作成する
Wolfram Compilerを使ってコールバックを作成することができる.次はWolframインタープリタを書かれたものよりも速く実行するコールバックを提供する.
compilerDemoBaseライブラリのcreateArray関数をロードする:
評価子を呼び出すコールバックはCreateForeignCallbackで作成することができる:
ここでWolfram Compilerを使ってコールバックを作成する.まず正しい型と機能を持つコンパイル関数を作成する:
実際の生関数はCompiledCodeFunctionに保存される:
ここでCompiledCodeFunctionオブジェクトの生関数ポインタをOpaqueRawPointerに変換する必要がある:
このコールバックを使ってcreateArray関数を起動する: