PR

ユーザー定義型を使う

 ここまで,CとHaskellの組み込みの型同士を対応させる場合を見てきました。しかし,実際にライブラリを呼び出す場合にはそれだけでは済みません。ライブラリでは何らかのユーザー定義の型を使用しているのが普通でしょう。そこで,Cのユーザー定義型,すなわち構造体をHaskellでどのように使用するかを見ていきましょう。

 説明を単純にするために,ごく簡単なCのソース・ファイルとヘッダ・ファイルを用意します。これらのファイルでは,座標を表すPoint型(Point構造体)と,Point型を使った処理を行う関数を提供しています。

foreign.h:

struct Point {
    int x;
    int y;
};

struct Point* newPoint (int x, int y);
int getPointX (struct Point *pt);
int getPointY (struct Point *pt);

typedef void (*PointFunc) (struct Point *pt);
void applyPoint (struct Point* pt, PointFunc func);

foreign.c:

#include <stdlib.h>
#include "foreign.h"

struct Point* newPoint (int x, int y) {
  struct Point *pt;
  pt = (struct Point *)malloc( sizeof(struct Point) );
  pt->x = x;
  pt->y = y;
  return pt;
};

int getPointX (struct Point* pt) {
  return pt->x;
};

int getPointY (struct Point* pt) {
  return pt->y;
};

void applyPoint (struct Point* pt, PointFunc func) {
  (*func) (pt);
};

 まず,Point型を作成してPoint型へのポインタを返すnewPoint,Point型へのポインタを取ってxの値を返すgetPointX,Point型へのポインタをとってyの値を返すgetPointYの四つの関数を使用してみましょう。これらを呼び出すHaskell側のソースコードは以下のようになります。

{-# INCLUDE "foreign.h" #-}
{-# LANGUAGE ForeignFunctionInterface #-}
module FFIExample where
import Foreign.Ptr

data Point = Point Int Int
data CPoint = CPoint

newPoint (Point x y) = c_newPoint x y

main = do
    pt <- newPoint (Point 16 34)
    x <- c_getPointX pt
    print x

foreign import ccall "newPoint" c_newPoint :: Int -> Int -> IO (Ptr CPoint)
foreign import ccall "getPointX" c_getPointX :: Ptr CPoint -> IO Int
foreign import ccall "getPointY" c_getPointY :: Ptr CPoint -> IO Int

 foreign宣言からわかるように,Ptr CPointがCのPoint型へのポインタを表しています。このようにHaskellのFFIでは無引数のデータ型を定義し,その型をForeign.Ptrモジュールで提供されているPtr型にくるむことで,Cの任意の型へのポインタを表現できます。

 実際には,CPointは型検査のためのラベルとして使うだけで,他の役割はありません。CPointによって具体化されるPtr aの型変数aのように,型を作成・区別するためだけに用いる型を「幽霊型(phantom type)」と呼びます(参考リンク)。

 幽霊型ラベルであるCPointは式で使用しないため,データ構成子を書くのは無駄です。このため,HugsやGHCを含む多くの処理系は,データ構成子を持たない型を定義するための拡張機能を提供しています。Hugsでは標準でこの機能を利用できます。GHCでは,-X*オプションやLANGUAGE指示文を使ってEmptyDataDeclsを指定することで,この拡張機能が有効になります(参考リンク1参考リンク2)。また,この機能は次期標準のHaskell'で追加される予定です(参考リンク)。

{-# INCLUDE "foreign.h" #-}
{-# LANGUAGE ForeignFunctionInterface #-}
{-# LANGUAGE EmptyDataDecls #-}
module FFIExample where
import Foreign.Ptr

data Point = Point Int Int
data CPoint

newPoint (Point x y) = c_newPoint x y

main = do
    pt <- newPoint (Point 16 34)
    x <- c_getPointX pt
    print x
    y <- c_getPointY pt
    print y

foreign import ccall "newPoint" c_newPoint :: Int -> Int -> IO (Ptr CPoint)
foreign import ccall "getPointX" c_getPointX :: Ptr CPoint -> IO Int
foreign import ccall "getPointY" c_getPointY :: Ptr CPoint -> IO Int

 foreign宣言を見ると,関数の呼び出しによって返ってくる型が「IO (Ptr CPoint)」のようにIOにくるまれていることがわかります。先のc_sin関数の例からわかるように,一部の例外を除き,FFIでは返ってくる型をIOにくるむことを特に強制するわけではありません。

 しかし,c_newPointは「メモリーの確保」というHaskellが関与しない副作用を発生させる関数です。また,c_getPointXとc_getPointYは,ポインタを直接対象にしている以上,何らかの原因によってポインタが示すメモリーの内容が書き換えられ,期待したのとは違う結果が返ってくる可能性があります。このような副作用を伴う関数を呼び出す場合には,Haskellで通常行うのと同じように,IOにくるんだ型を返すようにするのが良い習慣です。このため今回は意識的に,これらの関数の結果となる型をIOにくるんでいます。

 では,Point型と定義した関数を実際に使ってみましょう。GHCではHaskellのソースコードとともにCのソースコードのファイルを渡すことで,Cのソースコードを含むHaskellプログラムをコンパイルできます。

$ ghc FFIExample.hs foreign.c -main-is FFIExample

$ ./main
16
34

 GHCiを使う場合には,あらかじめGHCまたはCコンパイラなどを使ってコンパイルしたオブジェクト・ファイルを渡す必要があります。

$ gcc -c foreign.c

$ ghci FFIExample.hs foreign.o
GHCi, version 6.8.3: http://www.haskell.org/ghc/  :? for help
Loading package base ... linking ... done.
Loading object (static) foreign.o ... done
final link ... done
Ok, modules loaded: FFIExample.
*FFIExample> pt <- newPoint (Point 12 22)
0x02164690
*FFIExample> c_getPointX pt
12
*FFIExample> c_getPointY pt
22

 Hugsでは,ffihugsコマンドにCのソース・ファイルを渡すことで,Cのソースコードから生成されたオブジェクトを含むDLLを生成してくれます。

$ ffihugs FFIExample.hs foreign.c
FFIExample.c: In function 'hugsprim_newPoint_0':
FFIExample.c:49: warning: assignment makes pointer from integer without a cast
$ hugs FFIExample.hs
~ 略 ~
FFIExample> newPoint (Point 12 22)

FFIExample> newPoint (Point 12 22) >>= c_getPointX >>= print
12

FFIExample> newPoint (Point 12 22) >>= c_getPointY >>= print
22

 どれも正常に呼び出しができているのがわかりますね。ところで,ghciの「pt <- newPoint (Point 12 22)」の結果として表示されている0x02164690という数字は何でしょうか?

 実は,Ptr型にはShowクラスのインスタンスが提供されており,そのポインタのアドレスを表示するように定義されています。つまり,この数字はポインタのアドレスを示しています。

Prelude Foreign.Ptr> :i Ptr
data Ptr a = GHC.Ptr.Ptr GHC.Prim.Addr#         -- Defined in GHC.Ptr
instance Eq (Ptr a) -- Defined in GHC.Ptr
instance Ord (Ptr a) -- Defined in GHC.Ptr
instance Show (Ptr a) -- Defined in GHC.Ptr

 Hugsでも明示的にprintを使うことで,そのポインタのアドレスを表示できます。

FFIExample> newPoint (Point 12 22) >>= print
0x005025c0

 もちろん,空のポインタ(NULL)を示すnullPtrでは,アドレスの代わりに無意味な値(たいていは0)を表示します。

Prelude Foreign.Ptr> :t nullPtr
nullPtr :: Ptr a
Prelude Foreign.Ptr> nullPtr
0x00000000

 ここまではCのPoint型を使用するために,Cのソースコードを用意していました。同等の動作をHaskellだけで実現することもできます。Foreign.Marshal.Allocモジュールには,メモリーの確保と解放を行うCと同等の関数が提供されています。Storableクラスのインスタンスを定義すれば,Haskellのユーザー定義型をあたかもCの構造体のように扱えます。

Prelude Foreign.Marshal.Alloc> :browse
alloca ::
  (Foreign.Storable.Storable a) => (GHC.Ptr.Ptr a -> IO b) -> IO b
allocaBytes :: Int -> (GHC.Ptr.Ptr a -> IO b) -> IO b
finalizerFree :: GHC.ForeignPtr.FinalizerPtr a
free :: GHC.Ptr.Ptr a -> IO ()
malloc :: (Foreign.Storable.Storable a) => IO (GHC.Ptr.Ptr a)
mallocBytes :: Int -> IO (GHC.Ptr.Ptr a)
realloc ::
  (Foreign.Storable.Storable b) =>
  GHC.Ptr.Ptr a -> IO (GHC.Ptr.Ptr b)
reallocBytes :: GHC.Ptr.Ptr a -> Int -> IO (GHC.Ptr.Ptr a)

Prelude Foreign.Storable> :i Storable
class Storable a where
  sizeOf :: a -> Int
  alignment :: a -> Int
  peekElemOff :: GHC.Ptr.Ptr a -> Int -> IO a
  pokeElemOff :: GHC.Ptr.Ptr a -> Int -> a -> IO ()
  peekByteOff :: GHC.Ptr.Ptr b -> Int -> IO a
  pokeByteOff :: GHC.Ptr.Ptr b -> Int -> a -> IO ()
  peek :: GHC.Ptr.Ptr a -> IO a
  poke :: GHC.Ptr.Ptr a -> a -> IO ()
        -- Defined in Foreign.Storable
instance Storable Double -- Defined in Foreign.Storable
instance Storable Bool -- Defined in Foreign.Storable
instance Storable Char -- Defined in Foreign.Storable
instance Storable Int -- Defined in Foreign.Storable
instance Storable Float -- Defined in Foreign.Storable

 ただこれだと,Storableクラスのインスタンスを定義するには,構造体の大きさ(sizeOf)や構造体フィールド上のバイト・オフセット(offset),アライメント(alignment)など,通常Cでプログラミングするときには気にしなくていい情報を逐一与えなければならず面倒です。

 Cの世界では,これらの情報は環境やヘッダ・ファイルから自動的に決定されます。Haskellでも,同様に情報を自動生成してくれるプリプロセサがあれば便利です。こうした目的のために,GHCではhsc2hsというツールが提供されています(参考リンク)。

 実際には,以下のようなコードを拡張子hscのファイルとして保存します。

#include "foreign.h"
{-# LANGUAGE ForeignFunctionInterface #-}
module HscExample where
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Foreign.Storable

data Point = Point {pointX ::Int, pointY ::Int}

instance Storable Point where
    sizeOf = const #size struct Point
    alignment = sizeOf
    poke pt (Point x y) = do
        (#poke struct Point, x) pt x
        (#poke struct Point, y) pt y
    peek pt = do
        x <- (#peek struct Point, x) pt
        y <- (#peek struct Point, y) pt
        return $ Point x y


foreign import ccall "getPointX" c_getPointX :: Ptr Point -> IO Int
foreign import ccall "getPointY" c_getPointY :: Ptr Point -> IO Int

 Storableクラスのインスタンスを定義するために,Storableクラスが定義されているForeign.Storableモジュールをインポートしています。pokeとpokeElemOff,pokeByteOffはそれぞれを使って循環的にデフォルトの定義を用意しているため,どれか一つを定義すれば十分です。peekとpeekElemOff,peekByteOffも同様です。

 このコードに対してhsc2hsを使用します。

hsc2hs HscExample.hsc

 これにより,#演算子を使って記述した定義が展開され,以下のようなHaskellのソースコードが生成されます。

{-# INCLUDE "foreign.h" #-}
{-# LINE 1 "HscExample.hsc" #-}

{-# LINE 2 "HscExample.hsc" #-}
{-# LANGUAGE ForeignFunctionInterface #-}
module HscExample where
import Foreign.Marshal.Alloc
import Foreign.Ptr
import Foreign.Storable

data Point = Point {pointX ::Int, pointY ::Int}

instance Storable Point where
    sizeOf = const (8)
{-# LINE 12 "HscExample.hsc" #-}
    alignment = sizeOf
    poke pt (Point x y) = do
        ((\hsc_ptr -> pokeByteOff hsc_ptr 0)) pt x
{-# LINE 15 "HscExample.hsc" #-}
        ((\hsc_ptr -> pokeByteOff hsc_ptr 4)) pt y
{-# LINE 16 "HscExample.hsc" #-}
    peek pt = do
        x <- ((\hsc_ptr -> peekByteOff hsc_ptr 0)) pt
{-# LINE 18 "HscExample.hsc" #-}
        y <- ((\hsc_ptr -> peekByteOff hsc_ptr 4)) pt
{-# LINE 19 "HscExample.hsc" #-}
        return $ Point x y

foreign import ccall "getPointX" c_getPointX :: Ptr Point -> IO Int
foreign import ccall "getPointY" c_getPointY :: Ptr Point -> IO Int

 #includeがINCLUDE指示文,#size struct Pointが実際の構造体の大きさ,#poke *と#peek *がそれぞれ,構造体のフィールドのメンバーが格納されているバイト・データの書き込みと読み込みに展開されているのがわかりますね。

 ところどころに埋め込まれているLINE指示文は,生成されたソースコードが元ファイルの何行目に当たるかを示すものです。GHCでは,エラーが発生したファイル名と行数を正しく示すために使います(参考リンク)。例えば,sizeOfの定義を「sizeOf = const #size struct Point」から「sizeOf = #size struct Point」に変えると,以下のようなエラー・メッセージが表示されます。

$ ghci HscExample.hs foreign.o
~ 略 ~
[1 of 1] Compiling HscExample       ( HscExample.hs, interpreted )

HscExample.hsc:11:14:
    No instance for (Num (Point -> Int))
      arising from the literal `8' at HscExample.hsc:11:14
    Possible fix: add an instance declaration for (Num (Point -> Int))
    In the expression: (8)
    In the definition of `sizeOf': sizeOf = (8)
    In the definition for method `sizeOf'
Failed, modules loaded: none.

 作成したStorableクラスのインスタンスを実際に使用してみましょう。

$ ghci HscExample.hs foreign.o
~ 略 ~
Ok, modules loaded: HscExample.
*HscExample> pt <- malloc:: IO (Ptr Point)
0x003ffff0
*HscExample> poke pt (Point 12 34)
*HscExample> c_getPointX pt
12
*HscExample> pt' <- peek pt
*HscExample> pointX pt'
12
*HscExample> pointY pt'
34

 mallocで確保したptを,c_getPointXの対象として扱えていることを確認できます。また,peekで読み込んだ値をPoint型として利用できています。

 なお,メモリーの初期化忘れによるバグを防ぐため,Foreign.Marshal.Utilsモジュールではmallocとpokeを同時に行うnew関数を用意しています。

*HscExample> :m + Foreign.Marshal.Utils
*HscExample Foreign.Marshal.Utils> :t new
new :: (Storable a) => a -> IO (Ptr a)
*HscExample Foreign.Marshal.Utils> pt <- new (Point 11 88)
0x02163508
*HscExample Foreign.Marshal.Utils> c_getPointX pt
11
*HscExample Foreign.Marshal.Utils> c_getPointY pt
88