第8章 外部関数インタフェース(FFI)

目次

8.1. FFI追補に対するGHCの拡張
8.1.1. 非ボックス化型
8.1.2. IOモナドを包むnewtype
8.1.3. プリミティブのインポート
8.1.4. 割り込み可能な外部呼び出し
8.1.5. 呼び出し規約CAPI
8.1.6. hs_thread_done()
8.2. GHCでFFIを使う
8.2.1. GHCでforeign exportforeign import ccall "wrapper"を使う
8.2.1.1. 自分で用意したmain()を使う
8.2.1.2. 外部のコードから呼べるようなHaskellライブラリを作る
8.2.2. ヘッダファイルを使う
8.2.3. メモリ確保
8.2.4. マルチスレッドとFFI
8.2.4.1. foreign importとマルチスレッド
8.2.4.2. HaskellスレッドとOSスレッドの関係
8.2.4.3. foreign exportとマルチスレッド
8.2.4.4. hs_exit()の使用について
8.2.5. 浮動小数点とFFI

GHCは、Haskellの外部関数インタフェースに(大部分)準拠している。これの定義はhttp://www.haskell.org/にあるHaskellレポートの一部である。

FFI対応はデフォルトで有効であるが、-XForeignFunctionInterfaceで明示的に有効にしたり無効にしたりできる。

GHCには、FFI追補に対する固有の拡張がいくつか実装されている。これらの拡張は8.1. FFI追補に対するGHCの拡張に記述されている。これらの拡張を使ったプログラムは可搬でないということにどうか気を付けてほしい。従って、これらの機能は可能な限り避けるべきである。

FFIライブラリの説明は一緒に配布されているライブラリ説明書にある。例えばForeignモジュールを見よ。

8.1. FFI追補に対するGHCの拡張

この節で説明するFFIの機能はGHC特有のものである。これらを使った場合、他のコンパイラに対する可搬性がなくなる。

8.1.1. 非ボックス化型

基本外部型(FFI追補の3.2節を見よ)として、以下の型を使える。Int#Word#Char#Float#Double#Addr#StablePtr# aMutableByteArray#ForeignObj#ByteArray#

8.1.2. IOモナドを包むnewtype

FFIの仕様は、種々の箇所にIOモナドが現れることを要求するが、次のようにIOモナドをnewtypeで包むのが便利なことがある

newtype MyIO a = MIO (IO a)

(このようなことをする理由としては、例えば、プログラムのある場所において、任意のIO手続きを呼ばれることを防ぎたい、というのが考えられる)

Haskell FFIは既に、外部にインポート・エクスポートされるものの引数や結果がnewtypeだった場合、それらは自動的に外される、としている(FFI追補の3.2節)。GHCはこれを拡張して、IOモナド自体を包むnewtypeも自動的に外す。より正確にいうと、FFIの仕様がIO型を要求しているところならどこでも、IO型をnewtypeで包んだ物も認める。例えば、以下の宣言は問題ない。

foreign import foo :: Int -> MyIO Int
foreign import "dynamic" baz :: (Int -> MyIO Int) -> CInt -> MyIO Int

8.1.3. プリミティブのインポート

GHCはFFIを拡張して、primという呼び出し規約を追加している。例をあげる。

foreign import prim "foo" foo :: ByteArray# -> (# Int#, Int# #)

これは、GHCの内部的な呼び出し規約に従う、Cmmコードで書かれた関数群をインポートするのに使う。この機能は、専らGHC付属の中核ライブラリでのみ使うことを意図している。さらなる詳細はGHC開発者wikiを見よ。

8.1.4. 割り込み可能な外部呼び出し

これは、外部呼び出しとControl.Concurrent.throwToの相互作用についてである。通常、throwToの対象が外部呼び出しの実行中であった場合、その呼び出しが完了するまで例外が発生せず、その間呼び出し元はブロックされる。これによってプログラムが無反応になることがあり、特にユーザによる割り込み(Control-Cなど)の場合に問題になる。Control-Cシグナルを受け取ったとき(UnixならSIGINT)のデフォルトの振る舞いは、主スレッドにUserInterrupt例外を発生させることである。その時点で主スレッドが外部関数呼び出しでブロックしていた場合、プログラムはユーザによる割り込みに反応しない。

問題は、一般に外部呼び出しに安全に割り込むことが不可能であることだ。しかしなお、ブロックするシステムコールに割り込む方法をGHCは提供しており、UnixとWindowsの両方で、大部分のシステムコールに対して動作する。InterruptibleFFI拡張が有効である場合、外部呼び出しにsafeunsafeの代わりにinterruptibleという注釈を付けることができる。

foreign import ccall interruptible
   "sleep" :: CUint -> IO CUint

interruptiblesafeと同様に振る舞うが、違いは、interruptibleな外部呼び出しを実行中のスレッドがthrowToの対象になった時、OS依存の機構を使って外部呼び出しが返るように仕向けようとする点である。

Unixシステム

外部呼び出しを行っているスレッドに対してSIGPIPEシグナルが送られる。通常、これだけでブロック中のシステムコールがEINTRで返ることになる(GHCはデフォルトで空のシグナルハンドラをSIGPIPEに設定し、デフォルトの挙動(プロセスの終了)を上書きしている)。

Windowsシステム

[Vista以降のみ] RTSがCancelSynchronousIOというWin32関数を呼び、これが、ブロックしているI/O操作をエラーERROR_OPERATION_ABORTEDで終了させる。

システムコールが成功裏に割り込まれると、それはHaskellに返り、そこで例外が発生する。interruptibleを使う場合には、この外部関数の呼び出し元が、呼び出しが割り込まれた結果を正しく扱えるようになっているかどうか特に注意すること。UnixではEINTRを常に検査するのは良い習慣であるが、Windowsでは通常はERROR_OPERATION_ABORTEDに対処することが必要でない。

8.1.5. 呼び出し規約CAPI

拡張CApiFFIは、foreign宣言中でcapiという呼び出し規約を使うことを可能にする。例えば、

foreign import capi "header.h f" f :: CInt -> IO CInt

プラットフォームのABIに従ってfへの呼び出しを生成する代わりに、ここではheader.hで定義されたCのAPIを使ってfを呼ぶ。これにより、fが正規の関数でなくCPPマクロとして定義されていたとしてもこれを呼ぶことができる。

capiを使うとき、(関数でなく)値をインポートすることができる。例えば、

foreign import capi "pi.h value pi" c_pi :: CDouble

は、pi

const double pi = 3.14;

と定義されていても、

#define pi 3.14

と定義されていても動作する。

Haskellの型に対応するCの型をGHCに教えるために、型定義にCTYPEプラグマを付けることができる。その型が定義されているヘッダを指定することもできる。構文は次のようになる。

data    {-# CTYPE "unistd.h" "useconds_t" #-} T = ...
newtype {-# CTYPE            "useconds_t" #-} T = ...

8.1.6. hs_thread_done()

void hs_thread_done(void);

スレッドがforeign exportを介してHaskellの関数を呼ぶ際、GHCは少量のスレッドローカル・メモリを確保する。通常このメモリはhs_exit()まで解放されない。以降のHaskellへの呼び出しを高速で行なうために、このメモリはキャッシュされるのである。しかし、アプリケーションが長時間走り続けるものであり、Haskellを呼ぶスレッドを繰り返し作るのなら、おそらく、Haskell関数を呼び終わったスレッドにはこのメモリを解放させる方が良いだろう。そのためには、メモリを解放したいスレッドからhs_thread_done()を呼べば良い。

hs_thread_done()を呼ぶか呼ばないかは完全に自由である。好きなだけの回数呼んで良い。Haskellの関数を呼んだことがない、あるいはこれから呼ぶ予定がないスレッドからこれを呼んでも安全である。これを呼ぶのを忘れた場合、起こり得る最悪の事態は、多少のメモリがhs_exit()が呼ばれるまで確保されたままになることである。これを必要以上に多く呼んだ場合、起こり得る最悪の事態は、Haskell関数を次に呼んだ時に多少のオーバーヘッドが発生することである。