以下の各節では、他言語関数インタフェースをGHCで使うに当たってのヒントを与える。
foreign export
とforeign import ccall "wrapper"
を使うforeign export
またはforeign import "wrapper"
を使ったモジュール(M.hs
としよう)をコンパイルするとき、GHCはM_stub.c
とM_stub.h
の二つのファイルを生成する。同時に、GHCは、M_stub.c
を自動的にコンパイルして、M_stub.o
を生成する。
単純なforeign export
の場合、M_stub.h
にはforiegn exportされた関数のプロトタイプが入り、M_stub.c
には定義が入る。例えば、以下のモジュールをコンパイルしたとする。
module Foo where foreign export ccall foo :: Int -> IO Int foo :: Int -> IO Int foo n = return (length (f n)) f :: Int -> [Int] f 0 = [] f n = n:(f (n-1))
すると、Foo_stub.h
の中身は大体次のようなものになる。
#include "HsFFI.h" extern HsInt foo(HsInt a0);
そして、Foo_stub.c
には、コンパイラによって生成されたfoo()
の定義が置かれる。Cからfoo()
を呼ぶには、#include "Foo_stub.h"
としてfoo()
を呼ぶだけで良い。
foo_stub.c
とfoo_stub.h
は-stubdir
を使って別の場所に生成するようにできる。4.7.4. コンパイルの出力先を変えるを見よ。
プログラムをリンクするとき、最終的なリンクのコマンド行にM_stub.o
を含めるのを忘れないように。これをしないと、関数が足りない旨のエラーが発生する。(これは、ghc ––make
でプログラムをビルドする時には不要である。GHCが自動的に必要なものをリンクするので)
main()
を使う通常、GHCのランタイムシステムがmain()
を提供する。このmain()
はHaskellプログラムのMain.main
を起動するように手配する。しかし、main関数を別の言語(例えばC)で書いて、そこにHaskellコードをリンクしなければならないこともあるだろう。これをするためには、明示的にHaskellランタイムシステムを初期化しなければならない。
上の例を使って、これをスタンドアローンのCプログラムから呼び出してみよう。以下がCコードである。
#include <stdio.h> #include "HsFFI.h" #ifdef __GLASGOW_HASKELL__ #include "foo_stub.h" #endif #ifdef __GLASGOW_HASKELL__ extern void __stginit_Foo ( void ); #endif int main(int argc, char *argv[]) { int i; hs_init(&argc, &argv); #ifdef __GLASGOW_HASKELL__ hs_add_root(__stginit_Foo); #endif for (i = 0; i < 5; i++) { printf("%d\n", foo(2500)); } hs_exit(); return 0; }
GHC特有の部分は#ifdef __GLASGOW_HASKELL__
で囲った。それ以外のコードは、FFI標準に対応したHaskell実装の間で可搬であるはずだ。
hs_init()
の呼び出しでGHCのランタイムシステムが初期化される。hs_init()
を呼ぶ前にはいかなるHaskell関数も起動しようと*しない*ように。疑いの余地なく、悪いことが起こるだろう。
hs_init()
にargc
とargv
への参照を渡している。これは、RTSへの引数(つまり、+RTS...-RTS
の間の引数)を抽出できるようにするためである。
次に、hs_add_root
を呼ぶ。これは、プログラム中のHaskellモジュールを初期化するのに必要なGHC固有のインタフェースである。hs_add_root
の引数としては、プログラムの「ルート」モジュールの初期化関数の名前を与えるべきである。ルートモジュールとは、プログラム中の全てのモジュールを直接・間接にインポートするモジュールのことである。スタンドアローンのHaskellプログラムではこれは通常Main
であるが、ライブラリからHaskellコードを使っているならそうとは限らない。(訳注: ライブラリ由来のHaskellコードを使っているならそうとは限らない、と訳すべき?原文は、..., but when you are using Haskell code from a library it may not be.)。プログラムに複数のルートモジュールがあるときは、それぞれについて一回ずつhs_add_root
を呼べば良い。モジュールM
の初期化関数の名前は__stginit_
であり、上記のコードのように外部関数シンボルとして宣言することができる。シンボル名は以下に示すZ符号化に従って変換せねばならないことに注意。M
文字 | 変換先 |
---|---|
.
|
zd
|
_
|
zu
|
`
|
zq
|
Z
|
ZZ
|
z
|
zz
|
Haskellの関数を呼び出し終えた後、hs_exit()
を呼んでRTSを終了させることができる。
hs_init()
を複数回呼んでも良いが、毎回それに対応して一回(だけ)hs_exit()
[11]を呼ぶ必要がある。
参考: 最終的なプログラムをリンクする際、通常最も簡単なのは、GHCを使ってリンクすることである。(絶対に必要な訳ではないが)。GHCを使うなら、フラグ-no-hs-main
を使うことを忘れないように。さもないと、GHCはMain
というHaskellモジュールをリンクしようとする。
8.2.1.1. 自分で用意したmain()
を使うとよく似た状況だが、完全なプログラムをリンクするのが目的ではなく、Haskellコードからライブラリを作って、Cのコードでできたライブラリと同じように使えるようにしたいという場合である。
この場合、まず、Haskellコードが最初に呼ばれる前にランタイムが初期化される必要がある。このため、初期化と脱初期化のためのエントリポイントをCかC++で実装し、ライブラリ内に用意するべきである。例えば以下のように。
HsBool mylib_init(void){ int argc = ... char *argv[] = ... // Haskellのラインタイムを初期化する hs_init(&argc, &argv); // 全てのルートモジュールをHaskellに通知する hs_add_root(__stginit_Foo); // その他の初期化があればここで行ない、 // 問題があればfalseを返す return HS_BOOL_TRUE; } void mylib_end(void){ hs_exit(); }
この初期化ルーチンmylib_init
は、通常どおりhs_init()
とhs_add_root()
を呼んでHaskellランタイムを初期化する。これに対応する脱初期化関数mylib_end()
はhs_exit()
を呼んでランタイムを終了させる。
Cの関数は、通常、Cのヘッダファイルでプロトタイプを使って宣言される。前の版のGHC(6.8.3以前)は、Haskellコードから生成されるCソースファイルにヘッダを#include
していたので、FFIを介して呼ばれるCの関数が正しい型で呼ばれているかどうかを、Cコンパイラが検査することができた。
GHCはもはや、Cを介してコンパイルするときに外部のヘッダファイルをインクルードしないので、この検査は行われない。この変更は、ネイティブコードのバックエンド(-fasm
)との互換性、およびFFI仕様への準拠のために為された。(FFI仕様によると、FFI呼び出しは、Cのヘッダファイルを使ったときに起き得る、マクロ展開その他のCPPによる変換に影響されないことが要求される)。また、この方針によって、他言語呼び出しをモジュール境界やパッケージ境界を越えてインライン化する作業が単純になる。インライン展開された他言語呼び出しをコンパイルするときにそのヘッダファイルが存在する必要がないため、どんな文脈においてもコンパイラが自由に他言語呼び出しをインライン化できるようになるからである。
-#include
オプションはもはや非推奨であり、Cabalのパッケージ仕様の中のinclude-files
フィールドは無視される。
FFIで使うためのメモリ確保については、複数の方法がFFIライブラリで提供されていて、どれを使うのがいいかはっきりしないことがある。これを決めるには、個々の確保方法が特定のコンパイラ・プラットフォームでどの程度効率的かを勘案しなければならないことがあるので、この節では、GHCでのこれらの確保方法の性能がどれほどかをある程度明らかにすることを目標とする。
alloca
とその仲間
特定のIO
計算でのみ利用可能なものとして、短期的な確保をするときに便利である。この種の確保はFFI関数との間でデータをmarshalするときに良く使われる。
GHCでは、alloca
はMutableByteArray#
で実装されているので、確保・開放は速いはずである。Cのmalloc/free
よりはずっと速いが、Cのスタック確保ほどには速くない。可能な限りalloca
を使うと良い。
mallocForeignPtr
GCを必要とする、中長期にわたる確保に便利である。ただし、他言語のデータ構造の中にメモリへのポインタを保持したいなら、mallocForeignPtr
は良い選択ではない。
GHCでは、mallocForeignPtr
もまたMutableByteArray#
で実装されている。メモリを指すのはForeignPtr
だが、実際には終了子が関係せず(addForeignPtrFinalizer
で追加しない限り)、開放はGCが行うので、mallocForeignPtr
は通常とても安価である。
malloc/free
他が全て失敗したなら、Foreign.malloc
とForeign.free
に頼る必要がある。これらは同名のC関数のラッパに過ぎない。よって、効率性は結局プラットフォームのCライブラリにあるこれらの関数の実装に依存する。我々の経験では通常、malloc
とfree
は上に挙げた確保方法に比べてずっと遅い。
Foreign.Marshal.Pool
プールは今のところmalloc/free
を使って実装されているので、メモリを構造化するという点では他の方法より便利かもしれないが、効率的だということはない。ただし、より高性能のプールの実装を計画はしている。
マルチスレッド環境でFFIを使うには、-threaded
オプションを使う必要がある。(4.11.6. リンクに影響するオプションを見よ)
safe
注釈(デフォルトである)付きでforeign import
された関数を呼ぶ場合、プログラムが-threaded
を使ってリンクされているなら、その呼び出しは他のHaskellスレッドと並行に走る。プログラムが-threaded
なしでリンクされている時は、その呼び出しが返るまで他のHaskellスレッドはブロックされる。
つまり、長い時間が掛かったり、いつまでブロックするか分からない関数を他言語呼び出しする必要があるなら、それをsafe
にして、-threaded
を使うべきである。ライブラリ関数の中には、内部的にこの種の呼び出しを行うものもある。その場合は説明文書にその旨記載があるべきである。
複数のHaskellスレッドから他言語呼び出しを行い、しかも-threaded
を使っている場合、呼んでいる外部コードがスレッドセーフでなければならないことに注意すること。特に、ある種のGUIライブラリはスレッドセーフでなく、GUIメソッドを一つのスレッドからのみ呼ぶことを要求する。この場合、GUI操作は一つのHaskellスレッドからのみ行い、さらに結合スレッド(8.2.4.2. HaskellスレッドとOSスレッドの関係を見よ)を使う必要があるかもしれない。
複数のHaskellスレッドでなされた他言語呼び出しは並列に実行され得るのに注意。これは+RTS -N
フラグ(4.14.2. SMP並列性のためのRTSオプション)を使っていない場合でもそうである。+RTS -N
フラグはHaskellスレッドの並列実行を制御するが、ある時点での他言語の呼び出しは、+RTS -N
の値に関係なく、いくらでも多くなり得る。
通常、HaskellスレッドとOSスレッドの間に固定された関係はない。つまり、他言語呼び出しを行った時、それがどのOSスレッドで起こるのかは規定されない。さらに、一つのHaskellスレッドが複数回の呼び出しを行ったとしても、それが同じOSスレッド上で実行される保証はない。
このことは普通問題にならないし、このおかげでGHCのランタイムシステムがOSのスレッド資源を効率的に利用することが可能になっている。しかし、例えばスレッドローカルな状態を使う外部コードを呼ぶ場合など、どのOSスレッドを使うかをもっと制御できた方が便利なことがある。このような場合のために、結合スレッド(bound thread)を用意している。これは、特定のOSスレッドに結び付けられたHaskellスレッドである。結合スレッドについての情報は、Control.Concurrent
モジュールの説明を見よ。
プログラムが-threaded
付きでリンクされているなら、foreign export
された関数を複数のOSスレッドから並行に呼び出してよい。通常通り、ランタイムシステムはhs_init()
とhs_add_root
を呼んで初期化せねばならない。また、これらの呼び出しはforeign export
されたいかなる関数をも呼び出す前に完了していなければならない。
hs_exit()
の使用について通常、hs_exit()
によって、そのシステムで実行中のすべてのHaskellスレッドは終了し、hs_exit()
が返る時点では、もはやHaskellスレッドは一つも走っていない。この後、ランタイムは、必要ならプロファイル出力や統計情報を生成したり、持っているメモリをすべて開放したりして、システムを規則正しく終了させる。
Haskellスレッドを強制的に終了させることができない場合もある。例えば、スレッドが他言語呼び出しを実行中かもしれないが、他言語の呼び出しを終了させる方法はない。さらに、ランタイムは、Haskellコードとランタイムがメモリから削除予定であるという最悪の場合を想定しなければならない。(例えば、WindowsのDLLでは、hs_exit()
が呼ばれるのは、通常そのDLLをアンロードする前である)。そのため、hs_exit()
は、実行中のすべての他言語呼び出しが返るまで待ってからでないと、自分自身返ることができない。
要するに、他言語呼び出しでブロックしているHaskellスレッドがあるなら、その呼び出しが返るまでhs_exit()
はハングする(場合によってはビジーウェイト)ということだ。よって、hs_exit()
を呼び出すときにそのようなスレッドがシステム中にないようにするのが良い。これには、I/Oをしているあらゆるスレッドも含まれる。I/Oはブロックする他言語呼び出しを使って実装されているかもしれない(プラットフォームとI/Oの種類によってはそうでないかもしれない)からである。
GHCのランタイムは、プログラムが終了する場合を特別扱いして、スタンドアローンの実行ファイルが終了するときにブロックしているスレッドを待つ必要がないようにしている。コードがメモリから取り除かれるのと同時にプログラムとそのすべてのスレッドが終了するのだから、スレッドが先に終了するように保証する必要がない。(非公式だが、この、早くていい加減なhs_exit()
が欲しいなら、代わりにshutdownHaskellAndExit()
を呼ぶと良い)
C99標準のfenv.h
ヘッダは、浮動小数点ユニットの状態を確認したり変更したりするための操作を提供している。特に、浮動小数点演算で使われる丸めモードを変更したり、例外フラグを検査したりすることができる。
Haskellでは浮動小数点演算は純粋な型を持ち、その評価順は規定されない。fenv.h
の関数を使うと浮動小数点演算の結果を変えたりその作用を観測することができるので、厳密にはfenv.h
の使用はプログラム全体の浮動小数点演算の振る舞いを未定義にする。
とはいえ、GHCが浮動小数点状態に関して何を行なうかを厳密に記述することが我々にはできる。これは、どうしてもfenv.h
を使わざるを得ないときに、落とし穴についての完全な知識を持っていられるようにするためである。
GHCは浮動小数点環境を完全に無視する。ランタイムはそれを変更しないし、読みもしない。
浮動小数点環境はスレッドの通常のコンテキストスイッチに際して保存されない。なので、あるスレッドで浮動小数点状態を変更すると、その変更が他のスレッドから見えるかもしれない。さらに、例外状態を検査してもその結果は信頼できない。コンテキストスイッチによって変更を受けるかもしれないからである。浮動小数点状態を変更したり検査したりする必要があり、複数スレッドを使う必要もあるなら、bound thread (Control.Concurrent.forkOS
)を使わねばならない。bound threadは一つのOSスレッドを占有し、OSスレッドが浮動小数点状態の保存と復帰を行なうからである。
他言語呼び出しは決してGHCにプリエンプトされないので、他言語呼び出しの間に一時的に浮動小数点状態を変更することは安全である。
実際にシステムを脱初期化するのは最も外側のhs_exit()
である。これが起きた後、GHCのランタイムシステムを信頼性のある方法で再初期化することは今のところできないことに注意。12.1.3. FFI仕様からの逸脱を見よ。