CFFI


Tags: ライブラリ, FFI, CFFI

CFFIは、動的にリンクするタイプのライブラリをCommon Lispから利用できるようにする、Foreign function interface(以下FFI)のためのライブラリです。WindowsでのDLL、UNIXでの共有ライブラリを利用できます。型や関数、変数などについての簡単な定義をCommon Lispで書くだけで、ライブラリの関数や変数を参照できるようになります。

本来、Common Lispの処理系の多くにはFFIのためのAPIがありますが、それぞれの処理系で違いが大きく、特定の処理系に合わせたコードを書くと他の処理系で使えません。この問題を解決するため、処理系ごとの違いを吸収するのがCFFIの役割です。それぞれの処理系に共通する機能だけをサポートしているので、機能の面では処理系独自のAPIに及ばないこともありますが、複数の処理系で動作させたいコードではCFFIを使うと良いでしょう。

ここでは、明記していない限り、CFFIから利用するライブラリがCで書かれていることを想定して解説していますが、決められた形式に沿っている限り、他の言語で書かれたライブラリでも利用できます。その場合、ライブラリに使われている言語の事情に合わせて用語などを読み替えてください。



関連ページ


実例

4 An Introduction to Foreign Interfaces and CFFI

CFFI User Manualの、libcurlでファイルをダウンロードするという非常に実用的な例です。英文が苦手な方でも、このページの説明と合わせて読めば分かりやすいでしょう。

CFFI 入門 (1)

LISPUSERにある簡単な例です。


ライブラリの設定

メモリ上に読み込んでいないライブラリの関数を呼ぶことはできないので、CFFIを使うときは普通、利用するライブラリを動的に読み込むための設定を最初にします。Common Lispの処理系に元々リンクされているCのランタイムライブラリなどを使うときは、この節の手順は省略できるので飛ばしてください。

ライブラリの定義

define-foreign-libraryを使って、CFFI経由で利用するライブラリを定義します。

;; ライブラリの情報を定義し、libkyotocabinetというシンボルに結びつける
(define-foreign-library libkyotocabinet
  ;; UNIXでは最初にlibkyotocabinet.so.6を探し、
  ;; 見つからなかったらlibkyotocabinet.soを探す
  (:unix (:or "libkyotocabinet.so.6" "libkyotocabinet.so"))
  ;; それ以外の環境では、環境に応じた拡張子を付けたファイルを探す
  (t (:default "libkyotocabinet")))

:unixや:windowsなどの*features*に使われるシンボルを指定することで、環境ごとに読み込むライブラリのファイルを変えることもできます。

ライブラリの読み込み

利用するライブラリを定義したら、use-foreign-libraryを使い、必要になったときにライブラリを読み込むように設定します。

;; 必要になったとき、定義した情報を元にライブラリを読み込むようにする
;; 定義した情報を使わず、直接ライブラリのファイル名を指定することもできる
(use-foreign-library libkyotocabinet)

あるいは、load-foreign-libraryを呼んで、その場でライブラリを読み込むこともできます。

;; その場でライブラリを読み込む
(load-foreign-library 'libkyotocabinet)

型の定義

intなどの整数型、doubleなどの実数型、ポインタ型、C99で導入されたint32_tなどのサイズ固定の整数型など、組み込み型は標準でひと通りサポートされていますが、利用するライブラリでユーザ定義型を使っている場合、CFFIでも型を定義する必要があります。

別名

defctypeを利用することで、CFFIの型の別名を定義できます。Cのtypedefと同じです。

;; ポインタ型として定義する
;; 2011年9月現在のCFFIの仕様では、どの型のポインタなのかは単に無視され、
;; 読む人間に対してのヒントの役割しか持っていない
(defctype kcdb* (:pointer kcdb))

構造体

defcstructで構造体を定義します。オフセットも指定できます。

例えば、Cの

struct in_addr {
  u_int32_t s_addr;
};

という構造体と同じ構造の構造体を定義する場合、

(defcstruct in_addr
  (s_addr :uint32))

このように定義します。

共用体

defcunionで定義します。

(defcunion kcdb
  (db :pointer))

列挙型

defcenumで定義します。

(defcenum error-code
  :success
  :not-implemented
  :invalid-operation
  :no-repository
  :no-permission
  :broken-file
  :record-duplication
  :no-record
  :logical-inconsistency
  :system-error
  (:misc 15))

普通は0からひとつずつ増えていきますが、特定の値を割り当てることもできます。

(defcenum numbers
  (:one 1)
  :two
  (:four 4))

Cの関数を呼び出すときに、キーワードを自動的に数値に変換してくれます。戻り値も、自動的に数値からキーワードに変換されます。

ビットフィールド

defbitfieldで定義します。Cで良く使われるビットフィールドをCommon Lispから扱いやすくするための仮想的な型です。Cの構造体で定義できるビットフィールドに対応するための型ではありませんので注意してください。

(defbitfield open-mode
  (:reader 1)
  :writer
  :create
  :truncate
  :auto-transaction
  :auto-sync
  :without-locking
  :without-blocking
  :without-auto-repair)

普通は1から始まり、左に1ビットシフトした値がそれぞれ割り当てられますが、特定の値を割り当てることもできます。

(defbitfield open-flags
  (:rdonly #x0000)
  :wronly               ; #x0001
  :rdwr                 ; ...
  :nonblock
  :append
  (:creat  #x0200))

Cの関数を呼び出すときに、シンボルのリストを自動的に数値に変換してくれます。戻り値も、自動的に数値からシンボルのリストに変換されます。

環境によって定義が変わるユーザ定義型

環境によって定義が変わるユーザ定義型を扱うときは注意しなければなりません。典型的な例はsize_tです。32ビットの環境でunsigned intとして定義されているからといって:uintとして定義してしまうと、64ビットの環境では正しく動かない可能性があります。(少なくとも2011年現在の主流のLP64、LLP64データモデルでは正しく機能しません)

この問題に対処するには、大きく分けてふたつのアプローチがあります。環境に応じたそれぞれの定義を用意する方法と、ヘッダファイルを解釈する方法です。両方にメリットとデメリットがあるので、どちらを使うべきかは場合によって判断してください。

環境に応じた定義を用意する

環境に応じた定義を用意する方法の利点はお手軽さです。単に環境ごとのCFFIでの定義を並べるだけです。Cコンパイラやヘッダファイルも必要としません。Common Lispだけで完結します。

ただし、Cでの定義が変更されると、CFFIでの定義もそれに合わせてすべて直す必要があり、新しい環境に対応するときにも定義を毎回追加しなければならない欠点があります。また、それぞれの環境に完璧に対応する場合、細かく条件分けをしなければならないため、コードの動作を把握するのが難しくなるかもしれません。

具体的には、*features*を使って定義を切り替えることが多いでしょう。

;; size_tの定義の例
;; 環境によっては必ずしも動作するとは限らない
#+32-bit-target
(defctype size_t :uint32)
#+64-bit-target
(defctype size_t :uint64)

;; あるいは
(defctype size_t
    #+32-bit-target :uint32
    #+64-bit-target :uint64)

*features*の内容が処理系によって互換性がないことには注意してください。例えば、ある処理系がx86-64アーキテクチャで動いているとき、*features*に追加されるシンボルは:x86-64かもしれませんし、:x64かもしれません。多くの処理系に対応するには、*features*にどんなシンボルが追加されるか知らないといけませんので、調べる手間がかかります。

*features*に頼らず、ヒューリスティックな検査用のコードを使って定義を切り替える方法もあります。

;; size_tの定義の別の例
;; こちらも必ずしも動作するとは限らない
(defctype size_t
    #.(ecase (foreign-type-size :pointer)
        (8 :uint64)
        (4 :uint32)))

場合によっては*features*で判断するより簡潔になりますが、予想外の環境に対応できないのは変わりません。

ヘッダファイルの定義を利用する

ヘッダファイルの定義を利用する方法の利点は、正確さと、定義の変更や新しい環境への対応の強さです。ヘッダファイルの定義を参照すれば、CFFIで実態と合わない定義をしてしまうことはありませんし、ヘッダファイルが変更されてもCFFIでの定義を変更する必要はありません。欠点は、ヘッダファイルを用意しなければいけなかったり、Cコンパイラを必要としたりして、前提となる条件が増えることです。

CFFIではCFFI-Grovelという仕組みでこの方法をサポートしています。詳しくはCFFI User Manual13 The Grovellerを見てください。


変数の定義

defcvarで定義します。元々の変数名をアスタリスク(*)で囲んだ名前で参照できるようになります。また、アンダースコア(_)は、Lispの単語区切りの慣習に従ってハイフン(-)に変換されます。

;; int型のerrnoを参照するための定義
(defcvar "errno" :int)

*errno* ;=>  4

CFFIによって自動的に付けられる名前とは別の名前で参照できるようにしたり、定数として扱い、書き込めないようにすることもできます。

;; char*型の定数、KCVERSIONを参照するための定義
;; 定数を命名するときの慣習に従って+で囲む
(defcvar ("KCVERSION" +kcversion+ :read-only t) :string)

+kcversion+     ;=>  "1.2.29"

関数の定義

defcfunで定義します。引数や戻り値には、ユーザ定義の型を指定できます。また、変数と同じで、名前に含まれるアンダースコアはハイフンに変換されます。

;; CFFIでのユーザ定義型error-codeを引数として受け取り、
;; 文字列を返すkcecodenameという関数を参照するための定義
(defcfun "kcecodename" :string
  (code error-code))

(kcecodename :no-repository)    ;=>  "no repository"

自動的に付けられる名前とは別の名前で関数を参照することもできます。

;; 前の例と同じ定義だが、kc-error-code-nameという名前で参照できるようにする
(defcfun ("kcecodename" kc-error-code-name) :string
  (code error-code))

(kc-error-code-name :no-repository)     ;=>  "no repository"

オブジェクトの受け渡しと取り扱い

Common Lispの型とCの型は一対一で対応していないので、Common LispからCの関数を呼んだり、Common Lispの関数をCから呼ばせたりする(コールバック関数として渡す)ときには、引数や戻り値のデータの変換(マーシャリング)が必要です。CFFIを利用すると、数値型や文字列型などの単純な型は、定義にもとづいて、ある程度自動的に変換されます。

ただし、オブジェクトのポインタを渡して値を書き換えさせたり、構造体をCommon Lisp側で作って参照渡しをしたりするときのように、受け渡すデータを自分で作ったり、自分で参照しなければならない場合もあります。

数値

定義に合わせて自動的に変換されます。

(defcfun "pow" :double
  (x :double)
  (y :double))

(pow 2d0 -3d0)  ;=> 0.125d0

真偽値

:booleanを指定すると、Common Lispの真偽値(tとnil)とCの真偽値(1と0)とが自動的に変換されます。

(defcfun "isalpha" :boolean
  (c :int))

(isalpha (char-code #\a))       ;=> T
(isalpha (char-code #\0))       ;=> NIL

文字列

:stringを指定すると、Common Lispの文字列型とCのchar*とが自動的に変換されます。

(defcfun "strerror" :string
  (errnum :int))

(strerror 10)   ;=> "No child processes"

strdupのように、新しいメモリを割り当て、そのポインタを返す関数の戻り値に:stringを指定しないように気をつけてください。ポインタを参照できなくなってしまうため、割り当てられたメモリを解放できません。そういった関数に対しては、:pointerか:string+ptrを指定して、ポインタを返させるようにしてください。

;; 間違った定義。メモリリークする
(defcfun ("_strdup" strdup) :string
  (s :string))

;; strdupが本来返す、割り当てた領域を指すポインタの代わりに、
;; CFFIによって自動的に変換された文字列が返される
;; ポインタを参照できなくなるため、メモリを解放できない
(strdup "abc")  ;=> "abc"

;; 正しい定義
(defcfun ("_strdup" %strdup) :pointer
  (s :string))

;; 割り当てた領域を指すポインタを返すようにすれば、
;; freeで解放でき、リークしない
(defun strdup (s)
  (let ((p (%strdup s)))
    (unless (null-pointer-p p)
      (unwind-protect (foreign-string-to-lisp p)
        (foreign-funcall "free" :pointer p :void)))))

(strdup "abc")  ;=> "abc", 3

;; もうひとつの正しい定義
(defcfun ("_strdup" %strdup) :string+ptr
  (s :string))

;; 変換された文字列と、割り当てた領域を指すポインタのリストが返されるので、
;; メモリを解放できる
(defun strdup (src)
  (destructuring-bind (s p) (%strdup src)
    (unless (null-pointer-p p)
      (foreign-funcall "free" :pointer p :void)
      s)))

(strdup "abc")  ;=> "abc"

ポインタ

CFFI経由で呼ぶ関数から利用できる形でオブジェクトを作り、オブジェクトのアドレスをポインタとして渡します。戻り値として返されたポインタが指すオブジェクトは、mem-refmem-arefで参照します。

;; ad hocにsize_tを定義
(defctype size_t #-x86-64 :uint32 #+x86-64 :uint64)

;; Cの標準ライブラリのmemsetを定義
(defcfun "memset" :pointer
  (s :pointer)
  (c :int)
  (n size_t))

(defvar *length* 10)

;; メモリを確保して0で初期化
(defvar *foreign-string*
  (foreign-alloc :char :count (1+ *length*) :initial-element 0))

;; 文字列全体を97(a)に書き換え
(memset *foreign-string* 97 *length*)

(loop for n upto *length* collect (mem-aref *foreign-string* :char n))
;=> (97 97 97 97 97 97 97 97 97 97 0)

(foreign-string-to-lisp *foreign-string*)
;=> "aaaaaaaaaa", 10

foreign-allocで割り当てたメモリは自動的にガベージコレクトされないので、メモリリークを防ぐために、不要になったときに解放しなければなりません。割り当てたメモリを解放するにはforeign-freeを使います。

;; メモリを解放
(foreign-free *foreign-string*) ;=> NIL

また、CFFI経由で呼ぶ関数が、新しく割り当てたメモリのポインタを戻り値として返すような場合は、それも解放しなければなりません。このとき、foreign-freeは使えません。(foreign-freeは動作しているCommon Lisp処理系のFFI用APIを利用しますが、Allegro Common Lispのように独自のメモリ管理をしている場合があり、mallocで割り当てたメモリを解放できる保証はありません)freeや、CFFI経由で利用するライブラリが提供している方法でメモリを解放してください。

;; Cの標準ライブラリのmallocを定義
(defcfun "malloc" :pointer
  (size size_t))

;; Cの標準ライブラリのfreeを定義
(defcfun "free" :void
  (ptr :pointer))

(let ((p (malloc *length*)))
  (unless (null-pointer-p p)
    (unwind-protect
         (progn
           ;; ゼロクリアする
           (memset p 0 *length*)
           (loop for n below *length* collect (mem-aref p :char n)))
      (free p))))
;=> (0 0 0 0 0 0 0 0 0 0)

なお、メモリの割り当てから解放までのお決まりの流れを簡単に書けるよう、with-foreign-objectwith-foreign-pointerのようなマクロが定義されています。


C++

C++のように、オブジェクトコードの中のシンボルが名前修飾される言語で、それが標準化されていない場合、CFFIから関数を参照することが難しくなります。C++の関数を実用的にCFFIから呼び出すには、ふたつの方法があります。

自分でCのラッパーを書く

クラスやメンバ関数を直接扱わなくても良いように、クラスやメンバ関数を覆い隠すような、Cのコードを書きます。クラスは構造体に包み、メンバ関数は普通の関数で包みます。テンプレートも、外から見えないように別の関数で包みます。Cにない機能で書いてあるものは、Cの機能で置き換えます。また、名前修飾をしないように、コードをextern "C"で囲います。

具体的なコードが見たい場合は、Kyoto Cabinetのkclangc.hや、LevelDBc.hc.ccなどが参考になるでしょう。

SWIGを利用する

やっていることは自分でCのラッパーを書くことと同じですが、こちらは自動でSWIGが作ってくれます。SWIGのマニュアルの23.2.3 Generating CFFI bindings for C++ codeを読んでください。


SWIGとの連携

SWIGはCFFIにも対応しています。

$ cat kclangc.i
%module kclangc
%include "kclangc.h"

このようなインターフェイスファイルを書き、

$ swig -cffi -module kclangc kclangc.i

SWIGを実行することで、そのライブラリのCFFIでの定義を自動で作ることができます。


よくある質問

構造体を値渡しできませんか?

基本的にできません。CFFI User Manualの"14 Limitations"にも書いてあります。

CFFI経由で値渡しをするライブラリをどうしても使わないといけないなら、値渡しをしている関数をラッピングする参照渡しの関数をCで書いて、CFFIからはそれを利用してください。

無理矢理値渡しを再現する方法もあるようですが、扱いが難しいので、自分でそういう方法を思い付かないならおすすめできません。

C++の例外を捕捉できませんか?

できません。C++の側で例外をすべて捕捉するようにしておいて、例外に応じて特定の値を返すようにしたり、errnoのような仕組みを作ってください。

無名のコールバック関数を書けませんか?

書けません。CFFIでコールバック関数を定義するにはdefcallbackを使うしかありません。

どうしても無名のコールバック関数を使いたいときは、処理系独自のFFI用のAPIを使うことを検討してください。例えば、Steel Bank Common Lispではsb-alien:alien-lambdaを使うことで無名のコールバック関数を作れます。

foreign stringをforeign-freeで解放できますか?

2011年11月現在のCFFIでは、foreign-string-freeは単にforeign-freeを中で呼んでいるだけなので、解放することができます。

しかし、CFFI User Manualforeign-string-allocでは、割り当てたforeign stringはforeign-string-freeで解放するように書かれているので、将来的にforeign-freeでは正しく解放できなくなる可能性があります。

コードを保守する機会が増える可能性があるので、CFFI内部の仕様の変更に付き合う覚悟がなければ止めておいたほうが良いでしょう。


参考文献


Last modified : 2011/11/15 08:39:30 JST
CC0 1.0
Powerd by WiLiKi 0.6.1 on Gauche 0.9