y2q_actionman’s ゴミクズチラ裏

内向きのメモ書きを置いてます

CFFIの"CFFI-Wrapper"という隠し機能について

この記事は、Lisp Advent Calendar 2016 の22日目の記事として書かれました。

CFFIには、 CFFI-Wrapper という undocumented な機能があります。 Undocumented な機能ということで使用がはばかられますが、僕がかつて cl-libnuma という libnumaFFI バインディングを書いた際に、この機能を使わなければ実装できなかった事項がありました。 というわけでせっかくなので、拙作 cl-libnuma 実装を題材にして、この隠し機能について書いておこうと思います。

(2021-3-28 追記) id:guicho 氏のコメントで教えてもらいましたが、ついに CFFI のドキュメントにも記載された ようです。大手を振って使いましょう!

前置き:CFFIとは

最初に、CFFIについて軽く触れておきます。

CFFIは、 Foreign Funtion Interface (Lispとは別の言語で書かれた外部の関数を呼び出すためのインタフェース)にLisp処理系間での可搬性を持たせるためのライブラリです。FFIは、Lisp処理系ごとに作法の違いがありますが、CFFIを使うと処理系間の差異を吸収してくれます。

CFFIで外部の関数を呼び出すには、define-foreign-libraryuse-foreign-libraryを使って共有ライブラリをロードします。 以下は、cl-libnuma における共有ライブラリのロード部分です(library.lisp):

(define-foreign-library libnuma
  (:unix (:or "libnuma.so.1" "libnuma.so"))
  (t (:default "libnuma")))

(use-foreign-library libnuma)

この後、共有ライブラリ中の変数を使用する場合はdefcvar、関数を使用する場合はdefcfunをそれぞれ用いて、共有ライブラリの使用方法を宣言します。

以下は、numa.h にある int numa_max_possible_node(void);というC関数を呼び出すFFIを宣言します(binding.lisp) :

(defcfun numa-max-possible-node
    :int)

これをロードすると、以下のように共有ライブラリの関数の呼び出しが出来るようになります:

CL-USER> (cl-libnuma:numa-max-possible-node)
63   ; libnuma の関数経由で、環境依存の値が返される

詳しくは、日本語の資料があるので、そちらを参考して下さい。

前置き2:CFFI-grovel

CFFI を使うと、簡単に共有ライブラリ内の変数や関数を使用することが出来ますが、特にCライブラリを使用する場合には、Cヘッダファイルの定義も参照する必要があります。構造体の大きさやフィールドの並び順、enum値やマクロ定義された定数値などは、Cライブラリから取り出すことは出来ません。さらに、これらの値は環境に応じて変化します。このため、可搬性を確保するには、ハードコーディングするのではなく環境毎にCヘッダを解析して取り出す必要があります。

これを行うのが CFFI-grovel です。CFFI-grovel では、DSLを用いて必要な定数などを記述します。これを CFFI-grovel にかけると、Cコンパイラを通して必要な値を取り出してくれます。

以下は、C標準の limits.h ヘッダから、 CHAR_BIT 定数を抜き出す例です(grovelling.c):

(include "limits.h")            ; for CHAR_BIT

(constant (+CHAR-BIT+ "CHAR_BIT"))

これを CFFI-grovel でロードすると、 +CHAR-BIT+ という名前でこの定数を参照出来るようになります:

CL-USER> cl-libnuma::+char-bit+
8                                                                                                                                                                                                                                             

構造体やenum値についても同様に行えます。 詳しくは、CFFIのドキュメントを参照して下さい。

CFFI-wrapper

上述の CFFI-grovel を使うことで、共有ライブラリ中に存在しない定数値を使うことが出来るようになりました。しかし、共有ライブラリに存在しないものは定数だけではありません。関数形式のマクロや、 static inline 関数といったものも、Cヘッダファイルにしかなく、共有ライブラリ中には存在しません。これらを扱うには、追加の配慮が必要になるのです。

cl-libnuma で起こった問題

libnuma には、以下のような関数があります:

void numa_free_cpumask(struct bitmask *b);

ポインタを取って void を返す関数ですから、CFFIを使うと以下のように定義できるはずでした:

(defcfun numa-free-cpumask
    :void
  (b :pointer))

この定義で呼び出してみると、失敗してしまいました:

CL-USER> (cl-libnuma::numa-free-cpumask (cffi:null-pointer)) 

Attempt to call                                                                                                                                                                                                                               
#("numa_free_cpumask" 140046827622128 0 9 140046827622128 8) for                                                                                                                                                                              
which the definition has not yet been (or is no longer) loaded.
   [Condition of type SIMPLE-ERROR]

エラーメッセージによると、「ロードしてない関数だから呼べないよ」と言っています。しかしこの時、他の numa.h の関数は呼び出すことが出来ていました。 さらに、この numa_free_cpumask() を呼び出すだけの小さなCコードを書いてみると、コンパイルエラーもなく、正しく動作していたのです!

「そんなことってあり得るの??」と numa.h ファイルを見てみると、以下のような記述がありました:

static inline void numa_free_cpumask(struct bitmask *b)
{
        numa_bitmask_free(b);
}

この関数は、 static inline 関数として定義されていました。static inline 関数は、コンパイル時にインライン化されてしまい、共有ライブラリには存在しなくなります。このために、共有ライブラリから取り出す defcfun 経由での呼び出しは失敗してしまうのでした。Cコンパイラで使用できるのは、当然ながらヘッダファイルの定義を見ていたためでした。

cl-libnuma で起こった問題への対処

さて、この numa_free_cpumask() へのFFIはどう記述すればよいでしょうか。

最初に思いつくのは、Cヘッダファイル中にある static inline 関数定義をそのまま写し取り、 Lisp 側でハードコードすることです。今回の場合は、別の関数へと処理を丸投げするだけですから、簡単に実装できます。 しかし、将来的に libnuma のバージョンアップにより、 static inline 関数定義が変わったり、そもそも static inline 関数でなくなってしまう可能性もあります。そのような事になれば重大な不具合を引き起こしますし、更新の度に対応するのは大きな手間になってしまいます。

このバージョンアップの手間を削減するには、以下のような方法が考えられます。 まず、この static inline 関数を呼び出すだけの Cコードを用意し、それをコンパイルして共有ライブラリを作成しておきます:

#include <numa.h>
void numa_free_cpumask_cffi_wrap(void* b)
{
  return numa_free_cpumask(b);
}

次に、この共有ライブラリをdefine-foreign-library等でロードします:

(cffi:define-foreign-library :cl-libnuma-wrapping
  (t "cl-libnuma-wrapping.so"))  ; 共有ライブラリのファイル名は cl-libnuma-wrapping.so とした
(cffi:use-foreign-library :cl-libnuma-wrapping))

最後に、 defcfun でここで作成したライブラリの関数を呼び出すようにすれば、完成です:

(defcfun ("numa_free_cpumask_cffi_wrap" numa-free-cpumask)
    :void
  (b :pointer))

これなら、自動的に libnuma の更新に追随できます。しかし、Cコードを書いたり、共有ライブラリを作ってロードする等の手間が出来てしまい、これはこれで面倒です。なんとかする方法はないでしょうか?

CFFI-wrapper を使う

CFFI-wrapper は、まさにこの問題を解決するための機能です。 CFFI-wrapper を使うと、Cコードを書く事なく、上記の解決策 — ラッパーとなるC共有ライブラリを作成し、ロードし、defcfun 定義を作成する — を自動的に行ってくれるのです!

cl-libnuma では、この numa_free_cpumask() へのFFI定義を以下のように行っています(wrapping.lisp):

(include "numa.h")
;; 諸事情あって、 numa-free-cpumask* という名前で定義している
(defwrapper (numa-free-cpumask* "numa_free_cpumask") :void
  (b :pointer))

このように記述したファイルを、 ASDF 経由で以下のように読み込むだけで、全てが完了してしまいます!(cl-libnuma.asd):

(asdf:defsystem :cl-libnuma
  ; 省略
     ;; :soname オプションで共有ライブラリ名を指定する
     (cffi-grovel:wrapper-file "wrapping" :soname "cl-libnuma-wrapping")
  ; 後略

とても面倒そうだったライブラリ作成などが、数行で終わってしまいました!

まとめと残件

CFFI-wrapper は、限定的ですが面倒を大幅に減らしてくれる可能性がある CFFI の機能です。 しかし、 undocumented であり、実際の使用例もほとんどありません。筆者も使えそうと気付いたときに使用例を検索しましたが、1,2件しか見つかりませんでした。また、この用例が当てはまることもあまりなさそうではあります。 とはいえ、もしかしたら役に立つこともあるかもしれないということで、記事におこしてみました。

さて、実はまだ書きたい事があります。cl-libnuma には、 libnuma のある機能に対応するため、 CFFI-wrapper の文法を拡張してなんとかしようとして失敗したコードの残滓が残されています。その解説をしたいのですが・・ 本日中に上げないと Advent Calendar 的にまずいので、明日以降の遠い未来に追記しようと思います。