読者です 読者をやめる 読者になる 読者になる

Calling Lisp From C

sbcl FFI

Cからlispの関数を呼ぶ方法を調べた。
最終的には、lisp環境を動的ライブラリ形式っぽく保存して、Cで作成した実行ファイルから任意の関数を呼び出せるようにしたいのだが(そこまでやるかどうかは分からないが...)、とりあえず今日は、lisp関数を呼び出すCの関数を作成して、そのC関数をsbclから呼び出すところまでは出来たので、その方法をメモしておく。


まずは準備。

> # sbcl-1.0.28-source.tar.bz2をダウンロード・解凍
> cd sbcl-1.0.28
> sh make.sh      # 環境依存のヘッダファイルなどをまとめて生成してくれる (もっと的確なコマンドがあるかもしれない)
> cd src/runtime
> cp funcall.c funcall.h


次に、lisp関数を呼び出すCの関数を定義(listのリバース関数)

// file: src/runtime/creverse.h
#include "funcall.h"  // <- この中で、lisp関数を呼び出すfuncall0 ~ funcall3が定義されている

// typedef int lispobj  ※ lispobjはintのエイリアス

/* listの要素を破壊的に反転する */
// list: 対象リストのアドレス。
// nil: listの末尾を判定するためのnil値。CのNULLとは異なる。
// cdrfn: cdr関数のアドレス。
// rplacdfn: rplacd関数のアドレス
lispobj creverse(lispobj list, lispobj nil, lispobj cdrfn, lispobj rplacdfn) {
  lispobj head = nil;
  lispobj tmp;

  while(list != nil) {
    tmp = funcall1(cdrfn, list);
    funcall2(rplacdfn, list, head);
    head=list;
    list=tmp;
  }
  return head;
}

動的ライブラリ作成。

> gcc -O3 -c creverse.c
> gcc -shared -Wl,-soname,libcreverse.so.0 -o libcreverse.so creverse.c


最後に、lisp側で上のC関数を呼び出すための準備を整える。

(sb-alien:load-shared-object ".../sbcl-1.0.28/src/runtime/libcreverse.so")

;; XXX: とりあえず、引数・戻り値は全部int型
(sb-alien:define-alien-routine creverse int (list int) (null int) (cdr int) (rplaced int))

;; 使いやすい用にラップする
;; sb-kernel:get-lisp-obj-address#  引数のlispオブジェクトのアドレス(4byte表現?)を取得する
;; sb-kernel:make-lisp-obj# 引数のアドレスからlispオブジェクトを作成・取得する
;; (sb-kernel:make-lisp-obj (sb-kernel:get-lisp-obj-address *some-object*)) == *some-object*
(defun c-reverse (list)
  (flet ((adr (obj) (sb-kernel:get-lisp-obj-address obj)))
    (values (sb-kernel:make-lisp-obj
             (creverse (adr list) (adr nil) (adr #'cdr) (adr #'rplacd))))))

実行

;; ちゃんと動作する
> (c-reverse '(1 2 3))
--> (3 2 1)

;; ただし、この実装はすごく遅い
> (defparameter *list* (loop repeat 1000000 collect (random 1000000)))
--> *LIST*

;; 普通のnreverse
> (time (length (setf *list* (nreverse *list*))))
Evaluation took:
  0.005 seconds of real time
  0.004000 seconds of total run time (0.004000 user, 0.000000 system)
  80.00% CPU
  14,537,898 processor cycles
  0 bytes consed
--> 1000000

;; c-reverse
> (time (length (setf *list* (c-reverse *list*))))
Evaluation took:
  1.169 seconds of real time
  1.168074 seconds of total run time (1.016064 user, 0.152010 system)
  99.91% CPU
  3,707,246,807 processor cycles
  0 bytes consed
--> 1000000

以上。


あと、今の時点ではほとんど気にしていないが、Cからlisp関数を呼び出す場合は(主にGCに関連して)いろいろ注意事項があるようなので、ちゃんと使う場合はsbclのマニュアルsbcl-x.x.x/src/runtime/funcall.cのコメントを熟読する必要がありそう。