8.2. GHCでFFIを使う

以下の各節では、他言語関数インタフェースをGHCで使うに当たってのヒントを与える。

8.2.1. GHCでforeign exportforeign import ccall "wrapper"を使う

foreign exportまたはforeign import "wrapper"を使ったモジュール(M.hsとしよう)をコンパイルするとき、GHCはM_stub.cM_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.cfoo_stub.h-stubdirを使って別の場所に生成するようにできる。4.7.4. コンパイルの出力先を変えるを見よ。

プログラムをリンクするとき、最終的なリンクのコマンド行にM_stub.oを含めるのを忘れないように。これをしないと、関数が足りない旨のエラーが発生する。(これは、ghc ––makeでプログラムをビルドする時には不要である。GHCが自動的に必要なものをリンクするので)

8.2.1.1. 自分で用意した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

int main(int argc, char *argv[])
{
  int i;

  hs_init(&argc, &argv);

  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()argcargvへの参照を渡している。これは、RTSへの引数(つまり、+RTS...-RTSの間の引数)を抽出できるようにするためである。

Haskellの関数を呼び出し終えた後、hs_exit()を呼んでRTSを終了させることができる。

hs_init()を複数回呼んでも良いが、毎回それに対応して一回(だけ)hs_exit()[14]を呼ぶ必要がある。

参考: 最終的なプログラムをリンクする際、通常最も簡単なのは、GHCを使ってリンクすることである。(絶対に必要な訳ではないが)。GHCを使うなら、フラグ-no-hs-mainを使うことを忘れないように。さもないと、GHCはMainというHaskellモジュールをリンクしようとする。

hs_init()とともに+RTSフラグを使うには、例に多少の変更を加えなければならない。デフォルトでは、GHCのRTSは「安全な」+RTSフラグ(4.12.6. リンクに影響するオプションを見よ)のみを受け付け、これを変えるにはリンク時フラグ-rtsoptsを使う。しかし-rtsopts-no-hs-mainが使われているときには効果を持たない(-with-rtsoptsも同様)。これらのオプションを設定するには、hs_init()の代わりにGHC固有のAPIを呼ぶ必要がある。

#include <stdio.h>
#include "HsFFI.h"

#ifdef __GLASGOW_HASKELL__
#include "foo_stub.h"
#include "Rts.h"
#endif

int main(int argc, char *argv[])
{
  int i;

#if __GLASGOW_HASKELL__ >= 703
  {
      RtsConfig conf = defaultRtsConfig;
      conf.rts_opts_enabled = RtsOptsAll;
      hs_init_ghc(&argc, &argv, conf);
  }
#else
  hs_init(&argc, &argv);
#endif

  for (i = 0; i < 5; i++) {
    printf("%d\n", foo(2500));
  }

  hs_exit();
  return 0;
}

変更点が二つある。まずRts.hをインクルードした。これはGHC固有の外部RTSインタフェースを定義する。そしてhs_init_ghc()を呼ぶ代わりに、 RtsConfig型の引数を渡してhs_init_ghc()を呼んだ。RtsConfigは、ランタイムシステムの振る舞いに影響を与えるいろいろなフィールドを持つ構造体である。これの定義は以下。

typedef struct {
    RtsOptsEnabledEnum rts_opts_enabled;
    const char *rts_opts;
} RtsConfig;

extern const RtsConfig defaultRtsConfig;

typedef enum {
    RtsOptsNone,         // +RTSはエラーになる
    RtsOptsSafeOnly,     // 安全なRTSオプションが許される。それ以外はエラー
    RtsOptsAll           // 全てのRTSオプションが許される
  } RtsOptsEnabledEnum;

デフォルト値としてdefaultRtsConfigがあり、RtsConfig型の変数の初期化に用いられるべきである。将来、RtsConfigにもっと多くのフィールドが加えられることは間違いないので、コードを前方互換に保つためには、上のコード例のようにdefaultRtsConfigで初期化してから必要なフィールドを変更するのが良い。

8.2.1.2. 他言語のコードから呼べるようなHaskellライブラリを作る

8.2.1.1. 自分で用意したmain()を使うとよく似た状況だが、完全なプログラムをリンクするのが目的ではなく、Haskellコードからライブラリを作って、Cのコードでできたライブラリと同じように使えるようにしたいという場合である。

この場合、まず、Haskellコードが最初に呼ばれる前にランタイムが初期化される必要がある。このため、初期化と脱初期化のためのエントリポイントをCかC++で実装し、ライブラリ内に用意するべきである。例えば以下のように。

 HsBool mylib_init(void){
   int argc = ...
   char *argv[] = ...

   // Haskellのラインタイムを初期化する
   hs_init(&argc, &argv);

   // その他の初期化があればここで行ない、
   // 問題があればfalseを返す
   return HS_BOOL_TRUE;
 }

 void mylib_end(void){
   hs_exit();
 }

この初期化ルーチンmylib_initは、通常どおりhs_init()を呼んでHaskellランタイムを初期化する。これに対応する脱初期化関数mylib_end()hs_exit()を呼んでランタイムを終了させる。

8.2.2. ヘッダファイルを使う

Cの関数は、通常、Cのヘッダファイルでプロトタイプを使って宣言される。前の版のGHC(6.8.3以前)は、Haskellコードから生成されるCソースファイルにヘッダを#includeしていたので、FFIを介して呼ばれるCの関数が正しい型で呼ばれているかどうかを、Cコンパイラが検査することができた。

GHCはもはや、Cを介してコンパイルするときに外部のヘッダファイルをインクルードしないので、この検査は行われない。この変更は、ネイティブコードのバックエンド(-fasm)との互換性、およびFFI仕様への準拠のために為された。(FFI仕様によると、FFI呼び出しは、Cのヘッダファイルを使ったときに起き得る、マクロ展開その他のCPPによる変換に影響されないことが要求される)。また、この方針によって、他言語呼び出しをモジュール境界やパッケージ境界を越えてインライン化する作業が単純になる。インライン展開された他言語呼び出しをコンパイルするときにそのヘッダファイルが存在する必要がないため、どんな文脈においてもコンパイラが自由に他言語呼び出しをインライン化できるようになるからである。

-#includeオプションはもはや非推奨であり、Cabalのパッケージ仕様の中のinclude-filesフィールドは無視される。

8.2.3. メモリ確保

FFIで使うためのメモリ確保については、複数の方法がFFIライブラリで提供されていて、どれを使うのがいいかはっきりしないことがある。これを決めるには、個々の確保方法が特定のコンパイラ・プラットフォームでどの程度効率的かを勘案しなければならないことがあるので、この節では、GHCでのこれらの確保方法の性能がどれほどかをある程度明らかにすることを目標とする。

allocaとその仲間

特定のIO計算でのみ利用可能なものとして、短期的な確保をするときに便利である。この種の確保はFFI関数との間でデータをmarshalするときに良く使われる。

GHCでは、allocaMutableByteArray#で実装されているので、確保・開放は速いはずである。Cのmalloc/freeよりはずっと速いが、Cのスタック確保ほどには速くない。可能な限りallocaを使うと良い。

mallocForeignPtr

GCを必要とする、中長期にわたる確保に便利である。ただし、他言語のデータ構造の中にメモリへのポインタを保持したいなら、mallocForeignPtrは良い選択ではない

GHCでは、mallocForeignPtrもまたMutableByteArray#で実装されている。メモリを指すのはForeignPtrだが、実際には終了子が関係せず(addForeignPtrFinalizerで追加しない限り)、開放はGCが行うので、mallocForeignPtrは通常とても安価である。

malloc/free

他が全て失敗したなら、Foreign.mallocForeign.freeに頼る必要がある。これらは同名のC関数のラッパに過ぎない。よって、効率性は結局プラットフォームのCライブラリにあるこれらの関数の実装に依存する。我々の経験では通常、mallocfreeは上に挙げた確保方法に比べてずっと遅い。

Foreign.Marshal.Pool

プールは今のところmalloc/freeを使って実装されているので、メモリを構造化するという点では他の方法より便利かもしれないが、効率的だということはない。ただし、より高性能のプールの実装を計画はしている。

8.2.4. マルチスレッドとFFI

マルチスレッド環境でFFIを使うには、-threadedオプションを使う必要がある。(4.12.6. リンクに影響するオプションを見よ)

8.2.4.1. foreign importとマルチスレッド

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.15.2. SMP並列性のためのRTSオプション)を使っていない場合でもそうである。+RTS -NフラグはHaskellスレッドの並列実行を制御するが、ある時点での他言語の呼び出しは、+RTS -Nの値に関係なく、いくらでも多くなり得る。

呼び出しがinterruptibleとされており、プログラムがマルチスレッドの場合、そのHaskellスレッドが例外を受けた場合に呼び出しが中断され得る。この中断が発生する機構はプラットフォームによって異なるが、ブロックしているシステムコールが「割り込み発生」のエラーコードで即座に返ることが意図されている。使われているOSスレッドを破壊するものではない。さらなる詳細は8.1.4. 割り込み可能な他言語呼び出しを見よ。

8.2.4.2. HaskellスレッドとOSスレッドの関係

通常、HaskellスレッドとOSスレッドの間に固定された関係はない。つまり、他言語呼び出しを行った時、それがどのOSスレッドで起こるのかは規定されない。さらに、一つのHaskellスレッドが複数回の呼び出しを行ったとしても、それが同じOSスレッド上で実行される保証はない。

このことは普通問題にならないし、このおかげでGHCのランタイムシステムがOSのスレッド資源を効率的に利用することが可能になっている。しかし、例えばスレッドローカルな状態を使う外部コードを呼ぶ場合など、どのOSスレッドを使うかをもっと制御できた方が便利なことがある。このような場合のために、結合スレッド(bound thread)を用意している。これは、特定のOSスレッドに結び付けられたHaskellスレッドである。結合スレッドについての情報は、Control.Concurrentモジュールの説明を見よ。

8.2.4.3. foreign exportとマルチスレッド

プログラムが-threaded付きでリンクされているなら、foreign exportされた関数を複数のOSスレッドから並行に呼び出してよい。通常通り、ランタイムシステムはhs_init()を呼んで初期化せねばならない。また、これらの呼び出しはforeign exportされたいかなる関数をも呼び出す前に完了していなければならない。

8.2.4.4. 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()を呼ぶと良い)

8.2.5. 浮動小数点とFFI

C99標準のfenv.hヘッダは、浮動小数点ユニットの状態を確認したり変更したりするための操作を提供している。特に、浮動小数点演算で使われる丸めモードを変更したり、例外フラグを検査したりすることができる。

Haskellでは浮動小数点演算は純粋な型を持ち、その評価順は規定されない。fenv.hの関数を使うと浮動小数点演算の結果を変えたりその作用を観測することができるので、厳密にはfenv.hの使用はプログラム全体の浮動小数点演算の振る舞いを未定義にする。

とはいえ、GHCが浮動小数点状態に関して何を行なうかを厳密に記述することが我々にはできる。これは、どうしてもfenv.hを使わざるを得ないときに、落とし穴についての完全な知識を持っていられるようにするためである。

  • GHCは浮動小数点環境を完全に無視する。ランタイムはそれを変更しないし、読みもしない。

  • 浮動小数点環境はスレッドの通常のコンテキストスイッチに際して保存されない。なので、あるスレッドで浮動小数点状態を変更すると、その変更が他のスレッドから見えるかもしれない。さらに、例外状態を検査してもその結果は信頼できない。コンテキストスイッチによって変更を受けるかもしれないからである。浮動小数点状態を変更したり検査したりする必要があり、複数スレッドを使う必要もあるなら、bound thread (Control.Concurrent.forkOS)を使わねばならない。bound threadは一つのOSスレッドを占有し、OSスレッドが浮動小数点状態の保存と復帰を行なうからである。

  • 他言語呼び出しは決してGHCにプリエンプトされないので、他言語呼び出しの間に一時的に浮動小数点状態を変更することは安全である。

[14]

実際にシステムを脱初期化するのは最も外側のhs_exit()である。これが起きた後、GHCのランタイムシステムを信頼性のある方法で再初期化することは今のところできないことに注意。13.1.1.8. 多言語関数インタフェースを見よ。