SwigでC共有ライブラリのCFFIラッパー生成を試す
はじめに
cl-sdl2を使っていると、TEXTINPUT周りのイベントを取り扱おうとした際、Activeかどうかの判定などで使う関数がsdl2のネームスペースでアクセスできないような形になっていて使い辛いなと感じてます。
もしかするとリファレンスを見落としていたり使い方を間違っているのかもしれないんですが、
sdl2-ffi.functions.sdl-is-text-input-active
sdl2-ffi.functions:sdl-set-text-input-rect
のような関数は、
sdl2:is-text-input-active
sdl2:set-text-input-rect
のように書けたら良いのにと思ってます。
あと、ECLの場合cl-sdl2-ttfでsdl2をrequireしている箇所を削除しないとasdf:make-buildでバイナリにコンパイルして実行する際エラーが発生してしまうので、cl-sdl2-ttfもどうにかしたいです。Issueで修正箇所を送ってみたんですが、メンテナーの方はあまりアクティブに活動されてないようなのでしょんぼり……。
「そうだ、とりあえずCFFIを触ってみよう」
本格的に言語バインディングを作ろうとすると、それぞれの言語の作法に合わせた設計が必要になってくると思うんですが、ひとまずどういうものなのかやってみます。CFFIラッパージェネレーターを探すと、以下の3つが見つかりました。
このうち、verrazanoは開発中止でcl-autowrapを推してます。cl-autowrapがメジャーなようなんですが、ClangとLLVMが必須?のような説明で面倒だったので、BISONを使っている一番楽そうなSwigを選択しました。
環境
- Debian GNU/Linux 10.2
- ECL 16.1.3
- Swig 3.0.12
Swigを使う
SwigはCFFIを使ってCで書かれた共有ライブラリを呼び出す際、ヘッダファイルから各種言語、環境向けのラッパーを生成してくれるツールです。メンテナー不在かサポートが不十分なため、CFFI向けの機能は最新版では一旦取り除かれていますが、Debianパッケージマネージャでインストールできる3系のバージョンであればCFFIが有効なため、それを使って生成してみます。
$ sudo apt install bison swig
テスト用Cプログラムの作成
以下のようなc言語のコードを用意します。2点間の距離を求めるdistance関数と、点を示すpointという構造体を定義しています。
#pragma once
typedef struct {
double x;
double y;
} point;
double distance(point* p1, point* p2);
#include <math.h>
#include "mylib.h"
double distance(point* p1, point* p2) {
return sqrt(pow(p2->x - p1->x, 2) + pow(p2->y - p1->y, 2));
}
共有ライブラリとしてコンパイル
$ gcc -fpic -shared mylib.c -o mylib.so
Swigでラッパーを生成
Swig3.0のドキュメントのCFFIサポートを確認し、lisp用のファイルを生成するための定義ファイルを記述します。
%module mylib
%include "mylib.h"
最初の%module
は、この共有ライブラリラッパーのモジュール名を示し、%include
でヘッダーファイルを指定します。次のコマンドで、mylib.lispという名前でラッパーが生成されます。
$ swig -cffi -module mylib mylib.i
;; 〜Swig固有の出力は省略〜
(cffi:defcstruct point
(x :double)
(y :double))
(cffi:defcfun ("distance" distance) :double
(p1 :pointer)
(p2 :pointer))
mylib.lispを使うためのコードを書く
(pushnew (merge-pathnames #P"src/c/" *default-pathname-defaults*) cffi:*foreign-library-directories*)
(cffi:define-foreign-library mylib
(t (:default "mylib")))
(cffi:load-foreign-library 'mylib)
(defun main()
(cffi:with-foreign-objects ((p1 '(:struct point))
(p2 '(:struct point)))
(cffi:with-foreign-slots ((x y) p1 (:struct point))
(setf x 1.0 y 1.0)
(format t "p1(~A,~A)~%" x y))
(cffi:with-foreign-slots ((x y) p2 (:struct point))
(setf x 2.0 y 2.0)
(format t "p2(~A,~A)~%" x y))
(format t "~A~%" (distance p1 p2))))
最初のpushnew
は、CFFIに対して共有ライブラリの場所を追加しています。ここでは、src/cディレクトリ以下にmylib.soを置いているので、そのディレクトリを追加しました。
(cffi:define-foreign-library mylib
(t (:default "mylib")))
(cffi:load-foreign-library 'mylib)
上記のコードは、mylib.soを読み込みmylibという名前で取り扱うことを定義し、ライブラリを読み込んでいます。本体はmain関数の部分で、p1
、p2
というpoint構造体のポインタを作成し、それぞれのxとyに値をセット。最後にC言語側で定義したdistanceを呼び出しています。
実行結果は以下のようになります。
CL-USER> (load "mylib.lisp")
CL-USER> (load "main.lisp")
CL-USER> (main)
p1(1.0d0,1.0d0)
p2(2.0d0,2.0d0)
1.4142135623730951d0
NIL
うまく動いている感じなので、ひとまずこういうシンプルな形であればSwigのCFFI向けの機能は使えそうです。SDLのバインディングはものすごく大変そうなのでコピーして自分専用に修正したほうが早いのかもしれませんが、迷い中……。