あるライブラリのバインディングを作成するにあたって、
FFI、hsc2hs、inline-cの基本的な使い方をまとめ、その選定基準を調べてみました。
FFI、hsc2hsの使い方については、RWHにもっと網羅的に書かれていましたので、学習用途ではそちらを参照されたほうがよろしいかと思います。この記事は、使う時にさっと確認するという意図で書かれています。
ちなみに、筆者の環境ですが、OSはWindowsです。MinGWを使いました。
間違いの指摘などあれば、コメント頂けると嬉しいです。
1 FFIでCの関数を利用
まず、C言語で単純な関数を用意します。
int plus(int a)
{
return a + 1;
}
int minus(int a)
{
return a - 1;
}
これを利用するコードをHaskellで書きます。
module Main where
import Foreign.C.Types
foreign import ccall "plus" c_plus :: CInt -> IO CInt
foreign import ccall "minus" c_minus :: CInt -> IO CInt
plus :: Int -> IO Int
plus = fmap fromIntegral . c_plus . fromIntegral
minus :: Int -> IO Int
minus = fmap fromIntegral . c_minus . fromIntegral
main :: IO ()
main = do
print =<< plus 5
print =<< minus 5
実行ファイル作成
まずは愚直にやってみます。
.cファイルをコンパイルして、.oファイルを作成。
それをHaskellのファイルとまとめてコンパイルします。
gcc -c plus.c // plus.oが生成される
gcc -c minus.c // minus.oが生成される
ghc main.hs plus.o minus.o // main.exeが生成される
実行する。
$ main
6
4
想定どおりの動作です。
ライブラリを作成
Cのソースは、まとめてライブラリにしておいた方が都合がよいことがあります。
ということで、Cのライブラリを作成してからリンクする方法を示します。
最初に流れを図示しておきます。
1.1 静的ライブラリ
まずは静的なライブラリを作成します。
先ほど作成しておいた.oファイルから、libpm_static.aという名前のライブラリを作成します。
ar rcs libpm_static.a plus.o minus.o
コンパイルします。
-L.で現在のディレクトリからライブラリを探すようにして、
-lpm_staticでライブラリを指定する。
-l名前で、"lib名前.a"というライブラリが読み込まれます。
ghc main.hs -L. -lpm_static
以下のように、直接指定しても大丈夫です。
ghc main.hs libpm_static.a
1.2 動的ライブラリ
次に動的ライブラリDLLを作成します。
.oファイルから-shared
オプションでDLLを作成します。
gcc -shared -o pm.dll plus.o minus.o // DLL(pm.dll)が作成される
そしてコンパイル。
ghc main.hs -L. -lpm
この方法で作成された実行ファイルは、pm.dllを動的にリンクするので、pm.dllが無いと動作しません。
2.hsc2hsで定数や構造体とやり取りする
関数とやり取りする方法は分かりました。
今度は、hsc2hsというツールの力を借りて、
Cの定数や構造体とやり取りします。
2.1.定数
2.1.1.#const(1つ)
単純な例として、定数1をHaskell側に取り込むということをやってみます。
#define ONE 1
module Main where
import Foreign.C.Types
#include "foo.h"
myone :: CInt
myone = #const ONE
main :: IO ()
main = print myone
hsc2hsを使って、#const ONE
の部分を置き換えます。
これで、main.hsが生成されます。
$ hsc2hs main.hsc
main.hsの中身を見ると、#const ONE
の部分が、Cで定義されていた1に変換されています。
myone :: CInt
myone = 1
あとは、このソースをコンパイルするだけです。
$ ghc main.hs
$ main
1
2.1.2.#enum(複数)
enumを使えば、複数の定数を取り込みを自動化できます。
#define ONE 1
#define TWO 2
#define THREE 3
enumは以下のように指定します。
#enum 型, 構築子, 値, 値, ...
module Main where
#include "foo.h"
data Number = N Int
#{enum Number, N
, mnOne = ONE
, mnTwo = TWO
, mnThree = THREE
}
main = mapM_ print [mnOne, mnTwo, mnThree]
すると、このように変換されます。
mnOne :: Number
mnOne = N 1
mnTwo :: Number
mnTwo = N 2
mnThree :: Number
mnThree = N 3
$ hsc2hs main.hsc
$ ghc main.hs
$ main
N 1
N 2
N 3
ということは、そのままIntで欲しいならば、
型をIntにして、構築子を空にしておけばよいのですね。
module Main where
#include "foo.h"
#{enum Int,
, mnOne = ONE
, mnTwo = TWO
, mnThree = THREE
}
main = mapM_ print [mnOne, mnTwo, mnThree]
変換します。
$ hsc2hs main.hsc
そのままIntで得られました。
mnOne :: Int
mnOne = 1
mnTwo :: Int
mnTwo = 2
mnThree :: Int
mnThree = 3
2.2.構造体
構造体とのやり取りには、ポインターを使います。
構造体の作成
まず、C言語側で二次元ベクトルの構造体CVectを作ります。
参考のため、無駄に文字列を持たせています。
typedef struct {
int x;
int y;
char *label;
} CVect;
操作する関数の実装
#include "CVect.h"
#include <stdio.h>
// CVectを表示する
void printCVect(CVect *v) {
printf("CVect: %d %d '%s'\n", v->x, v->y, v->label);
}
// CVectのメンバをインクリメントするだけ
void add(CVect *a) {
a->x++;
a->y++;
}
Haskell側で利用できるようにする
この構造体CVectをHaskell側で扱えるようにします。
Cから直接構造体を読み取ったり、Cへ直接渡したりすることはできませんが、
メモリを通してやり取りするような仕組みが提供されています。
データ型Vectを作り、これをStorable型にすると、ポインタを通して読み書きできるになります。
Cの構造体CVectと、HaskellのデータVectをマーシャリングできるようにします。
peekで読み、pokeで書き込みます。その際、型を合わせる必要があります。これが間違っていても、コンパイラは何も教えてくれませんので、注意してコーディングします。
pokeで書き込む時には、IntからCIntを、StringからCStringを作ります。注意すべきことは、CStringの実体は、Ptr Charなので、メモリ管理に気をつけることです。ですので、Vect型に直接CStringを持たせた場合、メモリリークのほのかな香りが感じられて怖いです。
ちなみに、私はまだalignmentについてあまり理解しておりませんので、そこらへんが怪しいです。
色々とネット上で調べてみると、alignment _ = undefined
的な感じになっているソースが多いので、それでもよいのでしょうか。。
module Vect where
import Foreign.C.Types
import Foreign.C.String (withCString, peekCString)
import Foreign.Ptr (Ptr)
import Foreign.Marshal.Utils (with)
import Foreign.Storable
import Control.Applicative ((<$>), (<*>))
#include "CVect.h"
-- alignmentのためのマクロ
-- ただし#letはクロスコンパイル未対応なので気をつける
#let alignment t = "%lu", (unsigned long)offsetof(struct {char x__; t (y__); }, y__)
data Vect = Vect
{ x :: Int
, y :: Int
, label :: String
} deriving Show
instance Storable Vect where
sizeOf _ = #{size CVect}
alignment _ = #{alignment CVect}
peek ptr = Vect <$> (toInt <$> #{peek CVect, x} ptr)
<*> (toInt <$> #{peek CVect, y} ptr)
<*> (peekCString =<< #{peek CVect, label} ptr)
where
toInt :: CInt -> Int
toInt = fromIntegral
poke ptr (Vect x y label) = do
#{poke CVect, x} ptr x'
#{poke CVect, y} ptr y'
withCString label $ \cstr ->
#{poke CVect, label} ptr cstr
where
x' = fromIntegral x :: CInt
y' = fromIntegral y :: CInt
foreign import ccall "printCVect" c_printCVect :: Ptr Vect -> IO ()
foreign import ccall "add" c_add :: Ptr Vect -> IO ()
printVect :: Vect -> IO ()
printVect vect = with vect c_printCVect
add :: Vect -> IO Vect
add v = with v $ \p -> c_add p >> peek p
試してみる
実際に使ってみます。
import Vect
main :: IO ()
main = do
let a = Vect 1 10 "label"
print a
printVect a -- C言語側で表示
putStrLn "-- add --"
b <- add a
print b
printVect b -- C言語側で表示
コンパイルします。
$ gcc -c CVect.c // CVect.oを生成
$ hsc2hs Vect.hsc // Vect.hsc --> Vect.hs
$ ghc main.hs CVect.o
そして実行。
$ main
Vect {x = 1, y = 10, label = "label"}
CVect: 1 10 'label'
-- add --
Vect {x = 2, y = 11, label = "label"}
CVect: 2 11 'label'
3.inline-cを使ってインラインでCのコードを書く
inline-cでは、Haskellのコード中にCのコードを書くことができます。
詳しい使い方の例は、inline-c-nagを参考にして、とのことでしたが、ボリュームがそこそこあってつらかったので、構造体を利用する例を作っておきました。
3.1.exp・pure・block
関数を使うくらいだったらこんなに簡単に書けます。
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
import qualified Language.C.Inline as C
C.include "<stdio.h>"
C.include "<math.h>"
main :: IO ()
main = do
let x = 0.5 :: C.CDouble
print =<< [C.exp| double{ sin( $(double x) ) } |] -- CDouble -> IO CDouble
print [C.pure| double{ sin( $(double x) ) } |] -- CDouble -> CDouble
[C.block| void{
double ans = sin( $(double x) );
printf("cos(%.2f) = %f\n", $(double x), ans);
} |]
コンパイルはちょっと面倒ですが。
$ ghc -c Main.hs
$ gcc -c Main.c -o Main_c.o
$ ghc Main.o Main_c.o
実行します。
$ main
0.479425538604203
0.479425538604203
cos(0.50) = 0.479426
3.2.構造体とのやり取り
hsc2hsでやった時のように、データ型をStorableクラスのインスタンスにして、マーシャリングできるようにします。
さらに、それ用にContextを定義しておけば、Cのコードに埋め込めるようになります。
ただし、やはりポインタを通してですが。
構造体の定義
まずはメンバを1つだけ持つ単純な構造体Fooを定義し、
適当な関数を用意しました。
typedef struct {
int field;
} Foo;
void printFoo(Foo*);
void addFoo(Foo*);
#include "foo.h"
#include <stdio.h>
void printFoo(Foo *foo) {
printf("Foo: %d\n", foo->field);
}
void addFoo(Foo *foo) {
foo->field++;
}
対応するデータ型を準備
対応するデータ型Fooを作成し、Storableクラスのインスタンスにします。
例によって、.hscで書いておいて、hsc2hsで.hsに変換することを前提としています。
さらに、ここで一緒にContextも作成しています。
#let alignment t = "%lu", (unsigned long)offsetof(struct {char x__; t (y__); }, y__)
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
{-# LANGUAGE OverloadedStrings #-}
module Foo where
import Language.C.Inline
import Language.C.Inline.Context
import qualified Language.C.Types as C
import Control.Applicative ((<$>), (<*>))
import Foreign.Storable (Storable(..))
import Data.Monoid (mempty, (<>))
import qualified Data.Map as M
import qualified Language.Haskell.TH as TH
#include "foo.h"
include "foo.h"
data Foo = Foo {field :: CInt} deriving Show
instance Storable Foo where
sizeOf _ = #{size Foo}
alignment _ = #{alignment Foo}
peek ptr = Foo <$> (#{peek Foo, field} ptr)
poke ptr (Foo a) = #{poke Foo, field} ptr a
fooCtx :: Context
fooCtx = baseCtx <> ctx
where
ctx = mempty {ctxTypesTable = table}
table :: M.Map C.TypeSpecifier TH.TypeQ
table = M.singleton (C.TypeName "Foo") [t| Foo |]
実際にCコード内で利用するコードを書いてみます。
$(Foo *ptr)
で取り込んでいます。
{-# LANGUAGE QuasiQuotes #-}
{-# LANGUAGE TemplateHaskell #-}
module Main where
import qualified Language.C.Inline as C
import Foreign.Marshal.Utils (with)
import Foreign.Storable (peek)
import Foo
C.context fooCtx
C.include "foo.h"
printFoo :: Foo -> IO ()
printFoo foo =
with foo $ \ptr -> [C.exp| void{ printFoo( $(Foo *ptr) ) } |]
addFoo :: Foo -> IO Foo
addFoo foo =
with foo $ \ptr -> do
[C.exp| void{ addFoo( $(Foo *ptr) ) }|]
peek ptr
main :: IO ()
main = do
let foo = Foo 100
print foo
printFoo foo
foo' <- addFoo foo
print foo'
printFoo foo'
コンパイルが複雑になるので、Cabalに任せることにします。
全てのソースをsrc
フォルダに保存してから、以下のような設定を書きました。
executable foo
main-is: Main.hs
other-modules: Foo
hs-source-dirs: src
cc-options: -Wall -O2
c-sources: src/Main.c, src/foo.c
includes: src/foo.h
include-dirs: src
build-tools: hsc2hs
C言語側で、表示と加算ができたことが確認できました。
$ cabal run
Foo {field = 100}
Foo: 100
Foo {field = 101}
Foo: 101
4.まとめ(FFIとinline-cの使い分け)
FFIやinline-cを使えば、C言語のコードを利用できるのですが、どのように使い分ければよいのでしょうか。
motivation(これはlanguage-c-inlineのもので、inline-cとは違うようですが。源流は同じ?)などを読んで、私的に解釈した限りでは、大規模なライブラリのバインディングではinline-cを使うほうがベターだということでした。
確かに、大規模なライブラリのバインディングについては、作成が大変ですし、元のAPIがちょっと変更されただけで、コンパイルできなくなります。たとえ、変更箇所とは異なるAPIが使いたいだけなのに!という場合だとしても。従って、使いたいAPIだけを、inline-cを通じて使うというのは合理的だと思います。
しかし、FFIで完全なバインディングが用意されているという状況は、ライブラリのユーザーにとっては魅力的です。なぜならば、元のライブラリのAPIを理解するのが、バインディングの作成者だけで済むのですから。ユーザーは、Haskellのコードだけを扱えばよいことになります。
まとめると、ライブラリの安定性・規模、現在・将来的なユーザー数、などを考慮して決めるということでしょうか。
小規模・安定ならばFFIを。
大規模・不安定ならば、inline-cを。
ユーザー数については難しくて、多いならば一部の人がバインディングを作成した方が、プログラマ全体の効率は上がりそうですが、変更が多い場合はその都度作り直しになるので、絵に描いた餅になってしまいますね。