FFI のあれこれ
はじめに
少し前に FFI を使って Haskell から C++ の処理を呼び出すというのをやっていてなかなか大変だったのでこれについて書いておきます。
私がわからなくて調べたことなどあまり整理されてない雑多な内容になります。
FFI の情報
参考になりそうなページを貼っておきます。
FFI を使ったところ
OpenGL であれこれ表示したりするコードを書いていて glTF をロードして3Dモデルを表示するというのをやっていました。 glTF で頂点データを圧縮することができる draco という拡張があります。 draco 拡張はどうも圧縮アルゴリズムが仕様としてあるわけではなくて draco 拡張を使う場合には draco ライブラリの関数を呼び出して頂点データをデコードする必要があるようです。
draco ライブラリは C++ で書かれていてこれを Haskell から呼び出すのには FFI が必要となりました。
実際の作業としては下のようなことをしましたが慣れないのもあってなかなか大変でした。
FFI で C++ 呼ぶ場合
Haskell からC言語の関数をインポートするのに foreign import ccall
というのを使います。
(ccall の部分は呼び出し規約にあたるようで他に cplusplus とか jvm とかもあるようですが ghc には多分実装されてません。)
インポートしたC言語の関数は IO として使えて引数を渡して、戻り値を受け取ることができますが使える型には制限があります。
使える型はこのあたりに書かれてました。
https://hackage.haskell.org/package/base-4.14.0.0/docs/Foreign-Ptr.html#g:2
- the argument types are marshallable foreign types, i.e. Char, Int, Double, Float, Bool, Int8, Int16, Int32, Int64, Word8, Word16, Word32, Word64, Ptr a, FunPtr a, StablePtr a or a renaming of any of these using newtype.
- the return type is either a marshallable foreign type or has the form IO t where t is a marshallable foreign type or ().
構造体やクラスは値のままでは扱えないのでポインターにして受け渡す必要があります。
C++ はそのまま使えないので C でラップして使います。以下のものが必要になると思います。
それから C++ を使う場合はプロジェクトの設定で libstdc++ をリンクしたり cxx-options で C++ 用のオプションを指定する必要があります。
リソースの管理
FFI で生成したポインタはそのままでは GC で勝手に破棄されたりはしません。
https://xtech.nikkei.com/it/article/COLUMN/20080902/313965/
こちらに解説されているように newForeignPtr で Ptr から ForeignPtr を作って GC で回収されるときに破棄用の関数を呼ばせることができます。
https://qiita.com/tanakh/items/81fc1a0d9ae0af3865cb#with%E7%B3%BB%E9%96%A2%E6%95%B0
使い終わったらすぐに破棄する場合はこちらの記事のように bracket を使って with 関数を作ってあつかうと例外が投げられた場合にもリークしないので便利です。
Setup.hs でビルドできないか
使ったことないですが Setup.hs に書くことでビルド時に色々な処理を実行させることができます。
これを使って FFI でリンクするライブラリを一緒にビルドできないかと考えつきました。
しかし FFI を使っている色々なライブラリを見てもそういうことをしているプロジェクトはほとんどありませんでした。
この理由はいくつか考えられる気がしますが色々な環境に対応してビルドするのが難しいというのが一つの理由かなと思います。
それからライブラリによっては環境にインストールされている共有ライブラリをリンクするのが望ましいからという場合もあると思います。
draco の場合は Setup.hs で頑張ればなんとかビルドできなくもない気がするんですが Windows など色々な環境対応をしようとするとやっぱり大変そうです。
ほぼ自分しか使わないしビルドが少し複雑になってもまあいいかということで
draco は別にビルドして stack に --extra-include-dirs
と --extra-lib-dirs
のオプションを指定するという感じにしています。
このあたりのオプションは stack.yaml でも指定できます。
ghc から使われるコンパイラなどの設定
ghc --info
でコンパイラやリンカなどの設定が見れます。
ghc --print-libdir
で表示されるディレクトリに settings というファイルがあってこれが設定ファイルのようです。
PIC
static ライブラリをビルドするときは普通は -fPIC
のオプションは付けないらしいんですが
ubuntu (18.04 と 20.04) で draco をビルドしてリンクして使おうとすると下のようなエラーが出てしまいました。
/usr/bin/ld.gold: エラー: /work/./third_party/draco/build/libdraco.a(kd_tree_attributes_decoder.cc.o): requires dynamic R_X86_64_PC32 reloc against 'stderr' which may overflow at runtime; recompile with -fPIC
Windows では -fPIC
なしでも問題なくリンクできていました。
Windows 環境ではどうも gold ではなく ld が使われているようなのでリンカの違いによるかもしれません。
原因よく理解できてないですが -fPIC
つけてビルドするとリンク成功したためビルド時には -fPIC
つけるようにしています。