cffi を学ぶ(4) C のライブラリを使う

cffi を学ぶ(1) 変数と配列 - TIPS
cffi を学ぶ(2) 配列の続きと関数 - TIPS
cffi を学ぶ(3) Cのライブラリとコールバック - TIPS
cffi を学ぶ(4) C のライブラリを使う - TIPS
cffi を学ぶ(5) 構造体 - TIPS
cffi を学ぶ(6) 構造体の続きと translate-*-foreign - TIPS

参考
レファレンス CFFI User Manual
日本語の入門サイト http://cl.cddddr.org/index.cgi?CFFI

前回は自作のライブラリを書いて使うのをやったが一般のライブラリでもやることは同じ。
目標だった OpenGL を動かしてみる。

全部やるのは大変なのでビットフィールドや OpenGL の関数などは必要最小限のものだけ定義する。簡単な説明はコメント文でつけた。
とりあえず動かすのが目標なので Lisp 的な書き方は全然していない。

以下のコードを gl-cffi.lisp という名前で保存して repl で

(load "gl-cffi.lisp")

として三角形が回転していれば合格。ウインドウの起動に少し時間がかかるかもしれない。終了は Esc で。

;;;
;;; gl-cffi.lisp
;;;

(load "~/quicklisp/setup.lisp")

(ql:quickload "cffi")

(defpackage :scratch
  (:use :common-lisp :cffi))

(in-package :scratch)

;;
;; gl, glu, glut をロードする
;; 前回の自作のライブラリをロードするのと同様
;; 以下は cl-opengl の library.lisp から流用
;; ライブラリのファイル名は環境によって違うかもしれないので注意
;;

(define-foreign-library opengl
  (:darwin (:framework "OpenGL"))
  (:windows "opengl32.dll" :convention :stdcall)
  (:unix (:or "libGL.so.4" "libGL.so.3" "libGL.so.2" "libGL.so.1" "libGL.so")))

(use-foreign-library opengl)

;; darwin の glu は opengl の中に含まれているので不要
(define-foreign-library glu
  (:windows "glu32.dll") ; XXX?
  ((:and :unix (:not :darwin)) (:or "libGLU.so.1" "libGLU.so"))
  ((:not :darwin) (:default "libGLU")))

(use-foreign-library glu)

(define-foreign-library glut
  (:darwin (:framework "GLUT"))
  (:windows "freeglut.dll")
  (:unix (:or "libglut.so" "libglut.so.3")))

(use-foreign-library glut)

;;
;; ビットフィールドの定義
;; 詳細は上記サイト参照
;; gl.h などのヘッダファイルにある値を使う
;;
;; C のヘッダにはグループごとの名前は付いていないが 
;; Lisp で使うには名前をつけるべき。名前は自分で適当に決める。
;; foreign-bitfield-value で値を参照するときにシンボル名が必要になるため

(defbitfield (ClearBufferMask :uint)
  (:gl-depth-buffer-bit #x100)
  (:gl-depth-buffer #x100)
  (:gl-accum-buffer-bit #x200)
  (:gl-accum-buffer #x200)
  (:gl-stencil-buffer-bit #x400)
  (:gl-stencil-buffer #x400)
  (:gl-color-buffer-bit #x4000)
  (:gl-color-buffer #x4000)
  (:gl-coverage-buffer-bit-nv #x8000))

(defbitfield (DisplayMode :uint)
  (:glut-rgb			0)
  (:glut-rgba			0)
  (:glut-index			1) 
  (:glut-single			0) 
  (:glut-double			2) 
  (:glut-accum			4) 
  (:glut-alpha			8) 
  (:glut-depth			16)
  (:glut-stencil		32))

(defbitfield (BeginMode :uint)
  (:gl-points                         #x0000) 
  (:gl-lines                          #x0001) 
  (:gl-line-loop                      #x0002) 
  (:gl-line-strip                     #x0003) 
  (:gl-triangles                      #x0004) 
  (:gl-triangle-strip                 #x0005) 
  (:gl-triangle-fan                   #x0006) 
  (:gl-quads                          #x0007) 
  (:gl-quad-strip                     #x0008) 
  (:gl-polygon                        #x0009))

(defbitfield (MatrixMode :uint)
  (:gl-modelview                      #x1700) 
  (:gl-projection                     #x1701) 
  (:gl-texture                        #x1702))

;;
;; OpenGL の関数を定義
;;

;; mask <=> ClearBufferMask
(defcfun "glClear" :void (mask :uint))

(defcfun "glClearColor" :void (red :float) (green :float) (blue :float) (alpha :float))

;; mode <=> BeginMode
(defcfun "glBegin" :void (mode :uint))

(defcfun "glEnd" :void)

(defcfun "glFlush" :void)

(defcfun "glColor3f" :void (red :float) (green :float) (blue :float))

(defcfun "glVertex2f" :void (x :float) (y :float))

;; mode <=> MatrixMode
(defcfun "glMatrixMode" :void (mode :uint))

(defcfun "glLoadIdentity" :void)

(defcfun "gluOrtho2D" :void (left :double) (right :double) (botton :double) (top :double))

(defcfun "glutInit" :void (argcp :pointer) (argv :pointer))

;; mask <=> DisplayMode
(defcfun "glutInitDisplayMode" :void (mask :uint))

(defcfun "glutInitWindowSize" :void (width :int) (height :int))

(defcfun "glutCreateWindow" :int (title :string))

(defcfun "glutDisplayFunc" :void (f :pointer))

(defcfun "glutReshapeFunc" :void (f :pointer))

(defcfun "glutIdleFunc" :void (f :pointer))

(defcfun "glutKeyboardFunc" :void (f :pointer))

(defcfun "glutMainLoop" :void)

(defcfun "glutPostRedisplay" :void)

(defcfun "glutSwapBuffers":void)

;;
;; 初期化関数とコールバック
;;

(defun init ()
  (glClearColor 0.0 0.0 0.0 0.0))

;; 単にグローバル変数でいいが、ここではクロージャによるタイマー
(defun gen-timer (dt)
  (let ((tt 0.0))
    #'(lambda () (incf tt dt))))

(defparameter *timer* (gen-timer 0.001))

(defcallback display :void 
  () ;; 空の引数リスト
  (let ((tt (funcall *timer*)) (rad 2.094395)) ;; PI*2/3 = 2.094395
    (glClear (foreign-bitfield-value 'ClearBufferMask '(:gl-color-buffer-bit)))

    (glBegin (foreign-bitfield-value 'BeginMode '(:gl-triangles)))
    (glColor3f 1.0 0.0 0.0)
    (glVertex2f (cos tt) (sin tt))
    (glColor3f 0.0 1.0 0.0)
    (glVertex2f (cos (+ tt rad)) (sin (+ tt rad)))
    (glColor3f 0.0 0.0 1.0)
    (glVertex2f (cos (- tt rad)) (sin (- tt rad)))
    (glEnd)

    (glutSwapBuffers)))

(defcallback reshape :void 
  ()
  (glMatrixMode (foreign-bitfield-value 'MatrixMode '(:gl-projection)))
  (glLoadIdentity)
  (glMatrixMode (foreign-bitfield-value 'MatrixMode '(:gl-modelview)))
  (glLoadIdentity)
  (gluOrtho2D -1.5d0 1.5d0 -1.5d0 1.5d0))

(defcallback keyboard :void
    ((key :unsigned-char) (x :int) (y :int))
  (declare (ignore x y))
  (cond 
    ;; stdlib の exit(0) を呼ぶ
    ((= key 27) (foreign-funcall "exit" :int 0)) ;; 27 = #\Esc
    (t nil)))

(defcallback idle :void 
  ()
  (glutPostRedisplay))

(defvar *program-name* "gl-cffi.lisp")

(defun main ()
  ;; glutInit(&argc, &argv) 
  (with-foreign-objects ((argcp :int) (argv :pointer))
    (setf (mem-ref argcp :int) 1) ;; 引数がプログラム名だけなので 1
    (with-foreign-string (str *program-name*)
      (setf (mem-ref argv :pointer) str)
      (glutInit argcp argv)))

  (glutInitDisplayMode (foreign-bitfield-value 'DisplayMode '(:glut-rgb :glut-double)))
  (glutInitWindowSize 500 500)
  (glutCreateWindow *program-name*)

  (init)
  (glutDisplayFunc (callback display))
  (glutReshapeFunc (callback reshape))
  (glutKeyboardFunc (callback keyboard))
  (glutIdleFunc (callback idle))

  (glutMainLoop))

;; 実行
(main)

まとめ:

ウインドウをマウスで閉じるボタンが出てくれない。
アプリを閉じた後 repl の接続が切れてしまう。
glutInit や foreign-bitfield-value の部分が面倒なのでマクロを書くべき。
gl, glu, glut をパッケージに分けるべき。 (cl-opengl を使えばいいが)

Lisp から C を使うのはシームレスと言っていいくらい自然にやれる。
Lisp パッケージがなくても自分でヘッダファイルをみて何とかやれそう。
一般向けのパッケージにするのでなければ使う関数だけ定義しておいて少しずつ拡張していくといいかも知れない。