yunomuのブログ

趣味のこと

HaskellからCライブラリを呼ぶ(FFI)

Haskellのなんでもアリ具合。

ライブラリをビルドしてるとちょいちょい見えるFFIという文字列、気になって調べてみると、ForeignFunctionInterfaceというものがあって、これはHaskellからCの関数を呼び出すためのモジュールというか拡張らしい。
Haskellのものすごいライブラリの充実度の影にはこういうものがあったりするんですね。

ということでちょっと遊んでみました。

だいたいこの記事のトレスです。
本物のプログラマはHaskellを使う - 第22回 FFIを使って他の言語の関数を呼び出す:ITpro

既存のライブラリを呼ぶ

上の記事に習って、libmのsinを呼び出してみます。

まず、何はなくともmanです。sin(3)のmanを見ると、

NAME
    sin, sinf, sinl - sine function

SYNOPSIS
    #include 

    double sin(double x);
    float sinf(float x);
    long double sinl(long double x);

    Link with -lm.

だいたいこんな感じに書かれていると思われる。

これをCで使う時はこんな感じ。

#include <stdio.h>
#include <math.h>

int main()
{
    printf("%f\n", sin(0));
    printf("%f\n", sin(M_PI / 2));
    return 0;
}

コンパイルして実行する。

% gcc -lm sin.c
% ./a.out
0.000000
1.000000

コンパイルの時の"-lm"は、さっきのmanにも書いてありましたが、libm(mathライブラリ)をリンクしろという意味で、実は今のgccはlibmは標準でリンクしてくれるっぽいので省略可能です。

これを割とそのまんまHaskellでも書ける。

{-# LANGUAGE ForeignFunctionInterface #-}
{-# INCLUDE <math.h> #-}

module Main where

foeign import ccall "sin" c_sin :: Double -> Double

main :: IO ()
main = do
    print $ c_sin 0
    print $ c_sin (pi / 2)

今私が使っているversionのghc(7.4.1)だと、言語拡張のForeignFunctionInterface指定は必要ありませんでした。
あと、"INCLUDE "も、

Warning: -#include and INCLUDE pragmas are deprecated: They no longer have any effect

って言われたので、これも要らないみたいです。多分、標準でリンクされてるライブラリの分は要らないってことなんでしょう。
ということで、最初の2行は削っていいみたいです。少なくともINCLUDEの方は警告出るし、削ったほうが無難でしょう。

コンパイル&実行。

% ghc -lm Sample.hs
[1 of 1] Compiling Main ( Sample.hs, Sample.o )
Linking Sample ...
% ./Sample
0.0
1.0

これもgccと同様に"-lm"を付けるんですが、この場合も省略していいみたいです。

CとHaskellの数値型変換

c_sinを定義してるこの行、

foeign import ccall "sin" c_sin :: Double -> Double

「Cのsin関数をc_sinという名前の"Double -> Double"型としてimportしろ」というような感じだと思われます。

ただ、じゃあここでDouble->Double型として書いているけども、そのHaskellのDouble型とCのdouble型って一致してるの? 精度とかバイト数とか符号とか違うんじゃない? という事なんですけど、
そのためにForeign.C.Typesってモジュールがあって、Cの型と対応する型とHaskellの型への変換が定義されているらしい。
Foreign.C.Types
いやここには型定義だけで、変換は正確にはPreludeモジュールにあるんですけど。
fromIntegralとrealToFracです。なんかここ命名規則統一してほしいですよね。

これを使って真面目に書き直すとこうなります。

module Main where

import Foreign.C.Types

foreign import ccall "sin" c_sin :: CDouble -> CDouble

cSin :: Double -> Double
cSin = realToFrac . c_sin . realToFrac

main :: IO ()
main = do
    print $ cSin 0
    print $ cSin (pi / 2)

入力と出力をそれぞれrealToFracで変換する。これでオッケー。

自作のライブラリを呼ぶ

次、自分で作ったライブラリをリンクしてみる。このへんはHaskellというよりC言語っていうかGCCとかダイナミックリンクライブラリの話かもしれませんが。
Cで書いたライブラリをHaskellから呼び出してみます。

自作ライブラリの例としてとりあえず足し算でもしてみましょう。

int add(int a, int b)
{
    return a + b;
}

コンパイルしてダイナミックリンクライブラリを作る。

% gcc -shared add.c -o libadd.so

そしてこれを使う側のHaskellコードを書く。

module Main where

import Foreign.C.Types

foreign import ccall "add" c_add :: CInt -> CInt -> CInt

cAdd :: Int -> Int -> Int
cAdd a b = fromIntegral $ c_add (fromIntegral a) (fromIntegral b)

main :: IO ()
main = do
    print $ cAdd 0 1
    print $ cAdd 3 5

コンパイルして実行する。

% ghc -L. -ladd Add.hs
[1 of 1] Compiling Main ( Add.hs, Add.o )
Linking Add ...
% ./Add
1
8

できた。

今回は自作ライブラリなのでコンパイル時にパスの指定が必要になります。(-LPATH)
あとはまあ、fromIntegralって書くのがめんどくさいとかそのくらいでしょうか。

副作用とか

ここまで見ればわかるとおり、なんの制約もなくHaskellからCの関数を呼び出せてしまいます。
ということはつまり、Cの中でexitしようがIOしようがやりたい放題です。というかむしろmathみたいにIOが無い方が珍しいんですが。

逆に、Haskell側がCの関数をどういう仕様にするかも比較的自由なようで、
たとえばさっきのadd関数が実はIOを伴っているとわかった、もしくはその恐れがある場合は、foreign importの定義をこんなふうにすればいい。

foreign import ccall "add" c_add :: CInt -> CInt -> IO CInt

勝手にいきなりIOって書いても何も問題がない。
というか、これでHaskellの純粋さは保たれます。なんだそれって感じですが多分結構重要です。

まあ普通は副作用の無い関数なんてそうそう呼びだそうと思わないだろうから、FFIやるときはだいたいIOって付ければいいんだと思います。数学系関数でも中でmalloc/freeとかやってるかもしれないしね。
というかたぶんやってます。カーネルコールはすべからくIOだ、油断するな!

ポインタとか

関数呼び出しのパターンにはいくつかあって、これまでみたいに値をやり取りするというのは実は少数派で、だいたいは引数を格納した構造体のポインタを渡したり、バッファのポインタを渡したり、バッファのポインタを受け取ったりする。
readとかwriteとか、connectとかbindとか、fopenやexecみたいな文字列渡しも系もそうですね。

そういうのはじゃあどうするのということで、
さしあたり簡単そうなmalloc/freeを試してみます。

まず、malloc/freeの定義ですが、こんなかんじです。

void *malloc(size_t size);
void free(void *ptr);

freeの戻り値のvoidはいいとして、mallocの戻り値とfreeの引数のvoid*型は何がしか作ってあげる必要がある。

というか説明しきれないので答えを書くとこうなる。

import Foreign.C.Types
import Foreign.Ptr

data CVoid

foreign import ccall "malloc" c_malloc :: CSize -> IO (Ptr CVoid)
foreign import ccall "free" c_free :: Ptr CVoid -> IO ()

malloc :: Int -> IO (Ptr CVoid)
malloc size = c_malloc $ fromIntegral size

free :: Ptr CVoid -> IO ()
free ptr = c_free ptr

CVoidというテキトウな型を定義して、Foreign.Ptrで定義されているPtrで包む、「Ptr CVoid = void *」という感じです。

Ptrで包まれた型は、Ptrで包んでいる限りは何の制約もなしに自由にキャストしたりできます。

cast :: Ptr CVoid -> Ptr Int
cast ptr = castPtr ptr

ただし、それ以外の操作は全くできません。

Ptr型はその名の通りポインタなんですが、「ポインタを辿る」という操作がそもそもできない。そこは許されていない。だからCVoidはデータコンストラクタが要らないわけです。あっても使えないからね。
まあこれだけだと全く意味はないけども、PtrはShowのインスタンスで、メモリのアドレスが返ってくるので、こういうことはできます。

main :: IO ()
main = do
    p <- malloc 0x1000
    print p
    free p

じゃあ何の意味があるんだという感じですけど、Cのライブラリでは最初に初期化関数が構造体のポインタを返して、その後は関数にポインタの値を渡しながら処理をすすめる、構造体の中身はユーザは見ない、というタイプのものが結構あって、これはこれでいい。ファイル読み書きとかがまさにそうですね。

もうちょっとちゃんとメモリを扱いたかったらForeign.Marshal.AllocモジュールとかでStorableのインスタンスになったポインタを確保して、peek/pokeで読み書きするという手もあります。っていうかStorableのインスタンスなら別に問題なく読み書きできるわけです。
そのあたりはマニュアル見りゃわかるような話です。

構造体の扱い(がわからない)

これでだいたいのC関数を呼び出すことはできるようになりました。
もう何も怖くありません。

と言いたいところですが、
結局構造体のポインタ渡しとかはどうやるのっていう話には微妙に答えていない。Cの構造体とHaskellのデータ型のマッピング方法がよくわからない。
ビットフィールド指定とかどうするのかな。いやそこまでいかなくても、普通にどうするんだろう。アラインメントとかどうなるんだろう。
といったあたりはなんかテストプログラム書けば解決しそうな気もしますけど。
そこまではまだ調べていません。

次はData.Bitsの使い方でも調べればいいのかな。
何を書こうとしてるんだ私は。

ソース

今回遊んだソースはこちらです。念のため。
exercises/ffi at master · yunomu/exercises · GitHub