Foreign function interface (FFI)

昨日適当なテトリスが出来上がったけど、
まだまだHaskellネタが続くのです。
現時点であと二つほどは書きたいネタがあったりする。
Haskell上でのゲームプログラミングの方法論、
端的に言うとエレガントさの追求はまた今度。


とりあえず昨日までwxHaskellをやって、
それでゲームを作ったわけであるのだが、いくつかの点で限界を感じた。

  • 描画速度・描画機能
  • 音が出せない

普通にGUIアプリを作る分には良いんだろうけど、
(GUIのコントロール色々あるしね)
どうにもこうにもゲームには向いてないような印象だ。


そんなわけでSDLを使おうという話になった。(私の中で…)
Haskellというある意味異端なものを使いながら
結局超標準的なSDLに収まるのもなんともかんとも言えないが、
とりあえずやってみないと分からないのである。
もともと全く関数的でないライブラリをもとに
きちんと宣言的なインターフェースを用意できるのかどうか
ということにもそれなりに興味があるし。


で、まずはGoogleHaskell/SDLの存在を検索してみる。
有ったらそれ使えるので。だがまぁ、とりあえず存在しないらしい。
http://www.libsdl.org/languages.php
ここから調べるところろによると、公式にも認知されていないようである。


というわけで、FFIの勉強を始めることにする。
恥ずかしながらHaskell暦は1年弱あるにもかかわらず
(積極的に使い始めたのはここ数ヶ月だけど)
FFIははじめてである。初めてなのでとんちんかんなことを
書いていても怒らないように。(こっそり教えたってください)
まぁ、wxHaskellも初めてだったんだけど。

まずは、HaskellコードをCから呼び出し。
多分使わないと思うんだけど…GHCのユーザーガイドの
FFIのとこの最初に載ってるから…。


まず、以下のようなfoo.hsを用意。
Haskell側の関数の実装。ドキュメントから引っ張ってきただけだが。

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))

foreign export のところがエクスポートする指定か。
エクスポートできるのはIOモナドだけっぽい。
まぁ、どっちでもあまり変わらないけど。


続いて呼び出し側のC。main.cというファイルにしてある。

#include 
#include "foo_stub.h"

int main(int argc,char *argv[])
{
  hs_init(&argc, &argv);
  printf("%d\n",foo(2004));
  hs_exit();
  return 0;
}


コンパイル方法。

ghc -cpp -fffi -o foo.exe foo.hs main.c


なんかよくわからんのだが、-fffiをつけると
FFIなコードがコンパイル出来るようである。
つけないとコンパイルとおらない。
で、FFIなコードをコンパイルすると、普段作られる
foo.hi,foo.oのほかにfoo_stub.cとfoo_stub.hが作られて、
さらにそれのコンパイルまで行われる。
なんだか至れり尽くせりな感じである。
そのスタブなヘッダをmain.cからincludeする形となっている。
それでとりあえずfoo()が呼び出せるようだが、
最初にhs_init()とhs_exit()を呼び出す必要がある。
(本当に要るのか?と思って省いてみると見事に落ちた)

>foo
2004

で、実行すると普通に動いた。というか、こんだけじゃ
ほんとに呼び出してるかわからんぞ???
もうちょっと複雑なnqueenでも動かさしてみようかの?

nqueen :: MonadPlus m => Int -> m [Int]
nqueen n = inner 0 [] where
  inner cur ls
    | n==cur = return ls
    | otherwise = msum [inner (cur+1) (n:ls) | n <- [1..n], canPut n ls]

  canPut n ls = not $ n `elem` (zip [1..] ls >>= \(d,n) -> [n,n-d,n+d])


MonadPlus版。
これ一つでリストモナドを使えば全解リスト、
Maybeモナドを使えば一つの解を見つけるスグレモノである。
headとりゃいいやん、とか言っちゃ駄目。


というわけで、こんなソースをでっち上げた。

module Queen where
import Control.Monad

nqueen :: MonadPlus m => Int -> m [Int]
nqueen n = inner 0 [] where
  inner cur ls
    | n==cur = return ls
    | otherwise = msum [inner (cur+1) (n:ls) | n <- [1..n], canPut n ls]

  canPut n ls = not $ n `elem` (zip [1..] ls >>= \(d,n) -> [n,n-d,n+d])

foreign export ccall queenCnt :: Int -> IO Int

queenCnt :: Int -> IO Int
queenCnt n = return (length (nqueen n))

queenCntがエクスポートする関数で、解答の数を返す。


C側は

#include 
#include 
#include "queen_stub.h"

int main(int argc,char *argv[])
{
  hs_init(&argc, &argv);
  printf("%d\n",queenCnt(atoi(argv[1])));
  hs_exit();
  return 0;
}

適当にこんな風に変えた。


コンパイル&実行。

>queen 8
92

>queen 10
724

よしよし、これで満足である。(満足なのか…)