FFI のあれこれ

はじめに

少し前に FFI を使って Haskell から C++ の処理を呼び出すというのをやっていてなかなか大変だったのでこれについて書いておきます。

私がわからなくて調べたことなどあまり整理されてない雑多な内容になります。

FFI の情報

参考になりそうなページを貼っておきます。

wiki.haskell.org

xtech.nikkei.com

book.realworldhaskell.org

www.haskell.org

downloads.haskell.org

FFI を使ったところ

OpenGL であれこれ表示したりするコードを書いていて glTF をロードして3Dモデルを表示するというのをやっていました。 glTF で頂点データを圧縮することができる draco という拡張があります。 draco 拡張はどうも圧縮アルゴリズムが仕様としてあるわけではなくて draco 拡張を使う場合には draco ライブラリの関数を呼び出して頂点データをデコードする必要があるようです。

draco ライブラリは C++ で書かれていてこれを Haskell から呼び出すのには FFI が必要となりました。

実際の作業としては下のようなことをしましたが慣れないのもあってなかなか大変でした。

  • draco ライブラリから呼び出す必要がある処理を見つける
  • C言語でラッパーを作成
  • Haskell から呼び出す部分の実装
  • package.yaml の修正
  • CI でビルドできるようにする

FFIC++ 呼ぶ場合

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 つけるようにしています。