11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

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

Last updated at Posted at 2019-10-15

CFFIについて

ほとんどのCommon Lisp処理系はCの共有ライブラリにアクセスする何らかの方法を用意しているが、これらは独自拡張なので、各処理系間でAPIが統一されていない。この違いを吸収するためのCFFIというライブラリがあり、Cライブラリを呼ぶときはCFFIを使うのが常套手段となっている。
CFFIの情報源としては以下のようなものがある。

インストール

quicklispからインストールできる。

(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 "/path/to/test.so")

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

(push #P"/path/to/" 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 2d0 ; 引数1
                      :int 10     ; 引数2
                      :double)    ; 返り値の型
;; => 1024.0d0

foreign-funcallで呼び出す度に毎回型を指定するのは面倒だし、エディタによる補完も効かない。そこでpowerのラッパー関数を定義しておく。


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

(power 2d0 10)
;; => 1024.0d0

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関数に渡す。
以下は実際にforeign-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

;; :initial-elementで初期値を指定することもできる
(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 result
  (list (cffi:mem-ref *a* :int) (cffi:mem-ref *b* :int)))

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

result ; => (20 10)

外部オブジェクトのメモリ開放後もresultが値を保持しているので、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型として参照してみる。Cオブジェクトの領域を生成するにはforeign-alloc関数を使う。

(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 1d0 to 10d0 collect x)))

(loop for i from 0 below 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-allocmake-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-arefsetfが使えるので、foreign-alloc:countキーワードを付けて長さで領域を取ってきてから値を代入して初期化するという方法もある。

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

(loop for i from 0 below 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 below 10 do
    (setf (cffi:mem-aref arr :double i) (+ i 1d0)))
  (sum arr 10))

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

Cの文字列の取り扱い

文字列も配列の場合と似ているが、いくつか便利なユーティリティが用意されている。
例として、文字列を大文字に破壊的に変更するCの関数を定義する。


char *str_toupper(char *s)
{
    char *p;
    for (p = s; *p; p++)
        *p = toupper(*p);
    return (s);
}

これに対するラッパー関数toupperを定義する。

(cffi:defcfun ("str_toupper" toupper) (:pointer :char)
  (s (:pointer :char)))

数値の配列と同様にforeign-allocでCの文字列オブジェクトを生成することもできるが、文字列用にforeign-string-allocが用意されている。これはLispの文字列を受け取り、Cの文字列のための領域を確保し、初期化する。逆にCの文字列からLispの文字列に戻すにはforeign-string-to-lispが使える。


(defparameter *str* (cffi:foreign-string-alloc "hello"))

(toupper *str*)

(cffi:foreign-string-to-lisp *str*)
;; => "HELLO"

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

これまでと同様に、領域の確保と開放までを行うwith-foreign-stringsマクロもある。

(cffi:with-foreign-strings ((str "hello"))
  (toupper str)
  (cffi:foreign-string-to-lisp str))

;; => "HELLO"

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
11
7
2

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
7

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?