HaskellからC言語の「値」や「関数形式マクロ」をお手軽に使うための言語拡張CApiFFI
はじめに
HaskellのFFIはよくできている。FFIとhsc2hsを組み合わせれば、Cで書かれた処理をHaskellで組み合わせることができる。FFIとhsc2hsがあれば「たいていのこと」はできる。ただ、C側の関数ではない単純な「値」や「関数形式マクロ」を使うためには、.cファイルを書かざるをえない場合がある。
できるなら、CとHaskellをつなぐ部分を、ひとつのファイルで完結させたい。そこで「言語拡張CApiFFIですよ」という話。
言語拡張CApiFFIを使わない場合
Cの関数をHaskellから使う例
Stackを使う。つぎのようにして、ひながたを作成する。
% stack new try-capi-ffi
% cd try-capi-ffi
package.yamlのlibrary:のところに追加する。
library:
source-dirs: src
c-sources:
- csrc/hello.c
- csrc/foo.c
include-dirs: include
それぞれのファイルを作成する。
% mkdir csrc
% touch csrc/hello.c
% touch csrc/foo.c
% mkdir include
% touch include/hello.h
ビルドしておく。
% stack build
% stack exec try-capi-ffi-exe
someFunc
まちがいがなければ、ビルドが通るはずだ。
Cの関数を定義する。
#include "hello.h"
int
add123(int n)
{
return n + 123;
}
ヘッダファイルも作っておく。
#ifndef _HELLO_H
#define _HELLO_H
int add123(int n);
#endif
関数add123()を使用するモジュールHello.hsを作る。
module Hello where
import Foreign.C.Types
foreign import ccall "add123" c_add123 :: CInt -> CInt
試してみる。
% stack ghci
> c_add123 321
444
これで、C言語の関数をHaskellから使うことができる。
関数は問題ないね、それじゃあ「値」は?
C言語の「値」についてはどうだろうか。たとえば、つぎのように定義する。
int eight = 8;
extern int eight;
Haskellの「関数ではない値」(以下では単に「値」と表記)をC言語の側から見るとどう見えるだろうか。Haskellの「値」をつぎのように考えることができる。
fun2 :: Int -> Int -> Int
fun1 :: Int -> Int
fun0 :: Int
つまりHaskellにおける「値」は引数0の関数と考えることができる。なので、C側の「値」であるeightをHaskell側で「値」として使いたいときは、関数でくるんでやる必要がある。
#include "hello.h"
int
return_eight()
{
return eight;
}
これで、Haskell側から「値」として使うことができる。
foreign import ccall "return_eight" c_eight :: CInt
試してみよう。
> c_eight
8
> c_add123 c_eight
131
関数形式マクロも、おなじ感じ
関数形式マクロも、おなじ感じで関数にくるんでやればいい。
つぎのような関数形式マクロを考える。
#define ADD123(n) ((n)+123)
関数にくるむ。
int
return_ADD123_n(int n)
{
return ADD123(n);
}
Haskell側で使えるようにする。
foreign import ccall "return_ADD123_n" c_ADD123 :: CInt -> CInt
試してみる。
> :reload
> c_ADD123 c_eight
131
言語拡張CApiFFIを使うと
C言語の関数にくるむために、いちいち.cファイルを書くのはめんどくさい。「誰かかわりにやってくれないかな」と思う。言語拡張CApiFFIを使うとGHCがかわりにやってくれる。
モジュールHelloCApiを作る。
{-# LANGUAGE CApiFFI #-}
module HelloCApi where
foreign import capi "hello.h value eight" c_eight :: CInt
foreign import capi "hello.h ADD123" c_ADD123 :: CInt -> CInt
こうすると、foo.cでやっていた「関数でくるむ」作業をGHCがやってくれる。対話環境で試してみよう。ひとつ注意する点がある。
対話環境GHCiにおけるバイトコードとオブジェクトコード
ghciを引数なしで呼び出したとき、ソースコードはバイトコードに変換される。バイトコードへの変換にはオブジェクトコードへのコンパイルによりも、時間がかからない。また、バイトコードはより多くのデバッグに役立つ情報を持っている。
言語拡張CApiFFIを使った場合、おそらくGHCの内部でCのコードを作成する関係かと思われるが、バイトコードへの変換だとGHCiはうまく動かない。オブジェクトコードにコンパイルする必要がある。これにはフラグ-fobject-codeを使用する。
試してみる
% stack ghci --ghci-options -fobject-code
> :module HelloCApi
> c_ADD123 c_eight
131
構造体へのポインタを引数にとる関数形式マクロでは
C言語だと「構造体へのポインタを引数にとる関数形式マクロ」は、わりとよく出てくる。例として、つぎのような関数を考えてみる。
struct point {
int x;
int y; };
struct point* make_point(int x, int y);
#define POINT_X(p) (p->x)
関数make_pointを定義する。
struct point point0;
struct point*
make_point(int x, int y)
{
point0.x = x;
point0.y = y;
return &point0;
}
めんどくさいのでグローバル変数point0のポインタを返すようにしたけれど、mallocでメモリを動的に確保して云々といった処理をする関数を想定している。
関数形式マクロPOINT_XをHaskellから使いたいとする。つぎのように書いてみる。
import Foreign.Ptr
data Point
foreign import ccall "make_point" c_make_point :: CInt -> CInt -> IO (Ptr Point)
foreign import capi "hello.h POINT_X" c_POINT_X :: Ptr Point -> IO CInt
ビルドしてみる。
% stack build
...: error:
warning: dereference 'void *' pointer
#define POINT_X(p) (p->x)
...
エラーになる。関数形式マクロの引数pの型が(void *)であるために、構造体のメンバxにアクセスできないためだ。Ptr PointのC言語側での型を教えてやる必要がある。
data {-# CTYPE "hello.h" "struct point" -#} Point
これでビルドが通るようになる。
試してみる。
> c_POINT_X =<< c_make_point 987 654
987
まとめ
FFIの機能によってHaskellからCの関数を使うことができる。C言語の「値」や「関数形式マクロ」は、関数でつつんでやる必要がある。言語拡張CApiFFIを使うと、わざわざ.cファイルを書かなくてもGHCが「関数でつつむ」作業を代行してくれる。構造体へのポインタを引数にするような「関数形式マクロ」ではC側での型を教えてやる必要がある。