Common LispからCの機能を利用する

先日、Common Lisp RecipesのPDF版を$17.76で安売りしていたので買ってみた。この本の19章がLispから外部機能を利用する話で、これがCFFIの簡潔なチュートリアルになっている。この辺を見ながら色々実験してみたのでまとめてみる。

Common Lispでは外部機能へのインターフェース(FFI)は言語仕様に記載されていないので、各Lisp処理系が独自に実装している。それらのAPIの違いを吸収するCFFIというライブラリがある。

(ql:quickload :cffi)

共有ライブラリ作成

まずCでべき乗を計算する関数を書く。

double power (double base, int exponent) {
  int i;
  double result = 1.0;
  for (i = 1; i <= exponent; i++)
    result *= base;
  return result;
}

これをtest.cに保存してコンパイルし、共有ライブラリを作る。

gcc -fpic -shared test.c -o test.so

ライブラリ読み込み

共有ライブラリはload-foreign-library関数によって読み込む。

直接絶対パスを指定してライブラリを読み込む場合はこう書く。

(cffi:load-foreign-library "/home/wiz/cl/clr/ffi/test.so")

フルパスじゃなくてライブラリのファイル名だけ書く方法もある。その場合デフォルトのサーチパスから探される。例えばLinuxなら/libと/usr/libから、MacOSなら~/lib、/usr/local/lib、/usr/libから探される。サーチパスに追加するにはcffi:*foreign-library-directories*を変更する。

(push #P"/home/wiz/cl/clr/ffi/" cffi:*foreign-library-directories*)
(cffi:load-foreign-library "test.so")

:defaultキーワードを付けると拡張子も環境から自動で決まる。:orキーワードを付けると存在しているいずれかのライブラリを使う。

(cffi:load-foreign-library '(:default "test"))
(cffi:load-foreign-library '(:or "test.so.3" "test.so"))

より柔軟に環境に合わせた設定をするために、複数の環境ごとの設定をまとめて名前を付けることができる。

(cffi:define-foreign-library test-lib
  (:darwin (:or "test.3.dylib" "test.dylib"))
  (:windows (:or "test.dll" "test.dll"))
  (t (:default "test")))

(cffi:load-foreign-library 'test-lib)

LispからCの関数を呼び出す

(cffi:foreign-funcall "power"            ; 関数名
                      :double (sqrt 2d0) ; 引数1
                      :int 2             ; 引数2
                      :double)           ; 返り値の型
;; => 2.0000000000000004d0

foreign-funcallで毎回型を指定するのは面倒なのでpowerのラッパーを定義しておく。

(cffi:defcfun "power" :double
  (base :double)
  (exponent :int))

(power (sqrt 2d0) 2)
;; => 2.0000000000000004d0

Cの関数名は自動的にLisp風に変換される。例えば"c_power_num"だったらラッパー関数の名前はc-power-numになる。明示的にラッパー関数に名前をつける場合はこのように書く。

(cffi:defcfun ("c_power_num" power) :double
  (base :double)
  (exponent :int))

Cのポインタの取り扱い

ポインタの例としてK&Rとかにも出てくる、2つの値を交換する関数swapを定義する。

void swap (int *a, int *b) {
  int t = *b;
  *b = *a;
  *a = t;
}

次に、LispからCのswap関数を呼ぶためのラッパーを定義する。この際、引数の型に:pointerを指定する。

(cffi:defcfun swap :void
  (a :pointer)
  (b :pointer))

この関数を呼び出すには、まずLisp側からCのメモリ領域を生成し、そのメモリ領域へのポインタを表すオブジェクトを作ってswap関数に渡す。
以下は実際にforeighn-alloc関数でint型のCオブジェクトのメモリ領域を2つ生成し、swap関数を適用する例。Cのメモリ領域はGCによって回収されないので、最後にforeign-free関数で開放してやる必要がある。

(defparameter *a* (cffi:foreign-alloc :int))
;; => #.(SB-SYS:INT-SAP #X7FFFDC000F80) ; system-area-pointer

;; ポインタが差すデータを参照するにはmem-refを使う。setfで代入もできる
(cffi:mem-ref *a* :int)           ; =>  0
(setf (cffi:mem-ref *a* :int) 10) ; => 10
(cffi:mem-ref *a* :int)           ; => 10

(defparameter *b* (cffi:foreign-alloc :int :initial-element 20))

;; 外部関数のswapを呼び出す
(swap *a* *b*)
(cffi:mem-ref *a* :int) ; => 20
(cffi:mem-ref *b* :int) ; => 10

(defparameter lst
  (list (cffi:mem-ref *a* :int) (cffi:mem-ref *b* :int)))

;; メモリ開放
(cffi:foreign-free *a*)
(cffi:foreign-free *b*)

lst ; => (20 10)

メモリ開放後もlstが値を保持しているので、mem-refする度にLispオブジェクトを生成していることが分かる。

with-foreign-objectsマクロを使うと上の一連の流れをまとめてやることができる。

(cffi:with-foreign-objects ((a :int) (b :int))
  (setf (cffi:mem-ref a :int) 10
        (cffi:mem-ref b :int) 20)
  (print (list (cffi:mem-ref a :int) (cffi:mem-ref b :int)))
  (swap a b)
  (list (cffi:mem-ref a :int) (cffi:mem-ref b :int)))
;; (10 20)
;; => (20 10)

特に何も指定しなくてもメモリ開放が保証されるのでこちらの方が安全といえる。

Cのポインタは型付きだが、Lispのポインタは何でも指せる。その代わりにmem-refに (cffi:mem-ref :int) のように型情報を付ける必要がある。

ラッパー関数の引数のポインタに型を付けることもできるが、型チェックとかはしてくれない。試しにラッパー関数ではintへのポインタと宣言しているところにdoubleで確保した領域へのポインタを渡してみるとどうなるか。

(cffi:defcfun swap :void
  (a (:pointer :int))
  (b (:pointer :int)))

;; bをdoubleにしてswapを呼んでみる
(cffi:with-foreign-objects ((a :int) (b :double))
  (setf (cffi:mem-ref a :int) 42
        (cffi:mem-ref b :double) 23d0)
  (print (list (cffi:mem-ref a :int)
               (cffi:mem-ref b :double)))
  (swap a b)
  (list (cffi:mem-ref a :double) (cffi:mem-ref b :int)))
;; (42 23.0d0) 
;; => (0.0d0 42)

エラーは起こらないがswapした結果がおかしなことになる。

ここで何が起こっているかを理解するためには、mem-refで指定する型によって参照する領域が決まることを理解しておく必要がある。
例として、short型の領域を2つ生成し、それらをまとめて4バイトのfloat型として参照してみる。

(defparameter *test* (cffi:foreign-alloc :short :initial-contents (list 42 23)))

;; 型のサイズ
(cffi:foreign-type-size :short) ; => 2
(cffi:foreign-type-size :float) ; => 4

(list (cffi:mem-ref *test* :short)
      (cffi:mem-ref *test* :short 2) ; 先頭から2バイト先をshort型と解釈して参照する
      (cffi:mem-ref *test* :float))  ; short型2個をfloat型1個と解釈して参照する
;; => (42 23 2.1122753e-39)

このように問題なく参照できてしまう。

Cの配列の取り扱い

上の例でforeign-allocでshort型が2つ並んだ領域を作ったが、配列も基本的にこれと同じになる。
例えば、C側で次のように配列の総和を取る関数を定義したとする。

double sum (double *arr, int size) {
  int i;
  double result = 0.0;
  for (i = 0; i < size; i++) {
    result += arr[i];
  }
  return result;
}

これをLisp側から呼び出してみる。

(cffi:defcfun sum :double
  (arr :pointer)
  (size :int))

(defparameter *arr*
  (cffi:foreign-alloc :double
                      :initial-contents
                      (loop for x from 1 to 10
                            collect (float x 1d0))))

(loop for i from 0 to (1- 10) collect
  (cffi:mem-aref *arr* :double i)) ; mem-refと違うことに注意
;; => (1.0d0 2.0d0 3.0d0 4.0d0 5.0d0 6.0d0 7.0d0 8.0d0 9.0d0 10.0d0)

(sum *arr* 10) ; => 55.0d0

foreign-allocはmake-arrayと似たような感じで、:initial-contentsでリストによって初期値を指定することができる。また:countキーワードで個数を指定して領域を取ってくることもできる。その際に:initial-elementを指定しておくとその値で埋め尽くされる。

配列を参照するにはmem-arefを使う。mem-refでは第3引数に先頭からのバイト数を指定していたが、mem-arefはインデックスになっているところが違う。

;; mem-refはバイト数
(cffi:mem-ref *arr* :double 0) ; => 1.0d0
(cffi:mem-ref *arr* :double 8) ; => 2.0d0
;; mem-arefはインデックス
(cffi:mem-aref *arr* :double 0) ; => 1.0d0
(cffi:mem-aref *arr* :double 1) ; => 2.0d0

mem-arefもsetfが使えるので、foreign-allocに:countキーワードを付けて長さで領域を取ってきてから値を代入して初期化するという方法もある。

(defparameter *arr2* (cffi:foreign-alloc :double :count 10))

(loop for i from 0 to (1- 10) do
  (setf (cffi:mem-aref *arr2* :double i) (+ i 1d0)))

(sum *arr2* 10) ; => 55.0d0

;; 後始末
(cffi:foreign-free *arr*)
(cffi:foreign-free *arr2*)

with-foreign-objectsのオプショナル引数に配列の長さを指定しても同じことができる。

(cffi:with-foreign-objects ((arr :double 10))
  (loop for i from 0 to (1- 10) do
    (setf (cffi:mem-aref arr :double i) (+ i 1d0)))
  (sum arr 10))

C側からLispの配列にアクセスするにはLisp処理系が対応していなければならないらしく、Lispworksとかにはそのインターフェースがあるらしいが、CFFIではサポートされてない。

Cの構造体の取り扱い

構造体の場合も関数と同じで、Cの定義に対応するものをLisp側でも定義する。

struct complex {
  double real;
  double imag;
};
double magnitude_squared (struct complex *c) {
  return c->real * c->real + c->imag * c->imag;
}
(cffi:defcstruct c-complex
  (real :double)
  (imag :double))

;; 構造体へのポインタを引数に取る関数
(cffi:defcfun "magnitude_squared" :double
  (c :pointer))

ここで定義した構造体のオブジェクトを生成するのにもこれまでと同様にforeign-alloc/with-foreign-object/with-foreign-objectsが使える。型としては'(:struct c-complex)を指定する。構造体のスロットにアクセスするにはforeign-slot-valueを使う。

(cffi:with-foreign-object (c '(:struct c-complex))
  (setf (cffi:foreign-slot-value c '(:struct c-complex) 'real) 3d0
        (cffi:foreign-slot-value c '(:struct c-complex) 'imag) 4d0)
  ;; 初期化した構造体へのポインタをCの関数へ渡す
  (sqrt (magnitude-squared c)))
;; => 5.0d0

まとめ

ここまでのことで大体のCライブラリの機能は使えると思う。
共通する流れとしては、使いたいCの関数のラッパー関数をLisp側で定義し、Cのデータ構造をforeign-allocなどで生成してやって、そのポインタをラッパー関数に渡すというものだ。
受け渡しするデータが数値の場合はある程度自動で変換されるが、例えばLispの整数は多倍長だがCではそうでないので、LispからCに変換する過程で情報が落ちる可能性があることなどに注意しなければならない。
次は実際にCのライブラリのラッパーを作ってみる。