uvbook for Lisp -- Hello World

ここでは,libuvのチュートリアル本,uvbookの第2.2節に載っている以下のHello WorldコードをLispで実装する手順を解説します.まず始めにCFFIを使って直接libuvの関数を呼び出す方法を解説し,最後にlibuvラッパーのcl-libuvを使った実装方法を解説します.間違い,コメント等ありましたら,@waterloo_jpまでお願いします.

#include <stdio.h>
#include <stdlib.h>
#include <uv.h>

int main() {
    uv_loop_t *loop = malloc(sizeof(uv_loop_t));
    uv_loop_init(loop);

    printf("Now quitting.\n");
    uv_run(loop, UV_RUN_DEFAULT);

    uv_loop_close(loop);
    free(loop);
    return 0;
}

ライブラリのロード

Cで書かれたlibuvの関数を呼び出す前に,libuvライブラリをロードする必要があります.libuvがシステムにインストールされている場合,cffi:load-foreign-library関数を使ってロードすることができます.ファイル名を直接指定するよりも,拡張子を省略して:default指定すると,環境に応じてライブラリがロードされます.

(require 'cffi)
(cffi:load-foreign-library '(:default "libuv"))

OSに応じてロードするライブラリを細かく指定したい場合は,cffi:define-foreign-library関数でライブラリを定義した後,cffi:use-foreign-libraryを使ってロードします.cffi:use-foreign-libraryで複数回ロードするとエラーが出るので下の例のようにロード済みかどうか確認します.

(cffi:define-foreign-library libuv
  (:darwin  (:or "libuv.1.dylib" "libuv.dylib"))
  (:unix    (:or "libuv.so.1" "libuv.so"))
  (:windows (:or "libuv.dll" "libuv-1.dll"))
  (t        (:default "libuv")))

(unless (cffi:foreign-library-loaded-p 'libuv)
  (cffi:use-foreign-library libuv))

uv_loop_t構造体変数の準備

Cのサンプルコード6行目にある以下のコードを実装します.

    uv_loop_t *loop = malloc(sizeof(uv_loop_t));

まず始めに,size_t型をCFFIで使えるように設定し,次にメモリの割当方法を学び,最後にポインタ変数を作成します.

型の定義方法

libuvのAPIドキュメントにはuv_loop_t構造体の詳細は示されていませんが,代わりに同じページの下の方に,uv_loop_tの大きさをsize_tで返すuv_loop_size(void)関数があります.Cの組込み型は通常CFFIから利用可能ですが,size_tは何故か利用できないので,まずsize_tと同じ大きさのunsigned int型をエイリアスとしてCFFI版teypdefしておきましょう.

(cffi:defctype size_t :uint)

C関数のコール方法

Cの関数をcallするには2通りの方法があります.CFFIを使って直接関数を起動する方法と,CFFIでCの関数にLispシンボルをバインドしてLisp関数として起動する方法です.

1つ目の直接起動する方法はcffi:foreign-funcallを使う方法です.下の例の様にcffi:foreign-funcallに関数名,戻り値型,(引数型 引数値)から成る引数リストを指定して呼びます.

(cffi:foreign-funcall "uv_loop_size" size_t)

2つ目のLispシンボルにバインドして呼ぶ方法はcffi:defcfunマクロを使います.,cffi:defcfunマクロを使って以下のように定義すると,アンダースコアがハイフンに変わったLisp関数が定義されます.

(cffi:defcfun "uv_loop_size" size_t)  ; uv-loop-sizeにバインド
(uv-loop-size)                        ; Lisp関数として起動

uv_loop_sizeは何回も呼ばないので,ここではcffi:foreign-funcallを使った1番目の方法を採用することにします.

メモリの割当て方法

Lispからメモリを割り当てる場合,型に合わせたメモリ割り当てと,バイトサイズを指定したメモリ割り当ての2通りの方法があります.型を指定してその大きさのメモリを割り当てる場合には,cffi:foreign-allocwith-foreign-objectsを使います.バイトサイズを指定してメモリを割り当てるには,with-foreign-pointerを使います.

CFFIでメモリを割り当てる際にはバイト(8bit)単位で指定します.そこで,size_t型のバイト数を取得する必要があります.CFFIから利用可能なCの組込み型は,例えば(cffi:foreign-type-size :int)でバイト数を取得できますが,先ほど作成したエイリアスsize_tを使う場合にはクォートが必要です.後でコードが読みやすくなるように,ここではuv_loop_t型のバイト数を*size*に格納しておきましょう.

(* (cffi:foreign-type-size 'size_t)
   (cffi:foreign-funcall "uv_loop_size" size_t))

バイト数を指定してメモリを割り当てるにはcffi:with-foreign-pointerマクロを使います.以下のコードで本体処理部分でのみ*size*バイトのメモリがuvloopにバインドされます.メモリは通常スタックに割り当てられwithブロックを抜けると解放されます.

(cffi:with-foreign-pointer (uvloop *size*)
   '処理本体)

バイト数を指定する代わりに型を指定してメモリを割り当てるcffi:with-foreign-objectもあります.型の後ろに整数を指定すると,与えられた型が連続した領域を確保することができます.

(cffi:with-foreign-object
    (uvloop 'size_t (cffi:foreign-funcall "uv_loop_size" size_t))
  '処理本体)

malloc()の様にヒープにメモリを割り当てたい時にはcffi:foreign-allocを使います.この関数はバイト数ではなく型をとるので,任意の大きさを得るには:countキーワードで連続した領域数を指定します.この方法でメモリを割り当てた場合,cffi:foreign-freeで解放する必要があります.

;; メモリを割当てポインタを*uvloop*にバインド
(defparameter *uvloop*
  (cffi:foreign-alloc 'size_t
		      :count (cffi:foreign-funcall "uv_loop_size" size_t)))
;; メモリ解放
(cffi:foreign-free *uvloop*)

ここでは,1番始めに紹介した,cffi:with-foreign-pointerを使って実装することにします.

イベントループの初期化

ここまででuv_loop_t型へのポインタ変数を用意できました.次はサンプルCプログラムの7行目の以下のコードを実装します.

    uv_loop_init(loop);

uv_loop_init関数の定義は,int uv_loop_init(uv_loop_t* loop)です.引数がポインタの場合,CFFIでの型指定は:pointerを使います.Lispシンボルとバインドして呼び出したいなら,

(cffi:defcfun "uv_loop_init" :int (loop :pointer))

で,ポインタを引数loopに取り,intを返すuv-loop-init関数が定義できます.呼び出し方法はハイフンに注意して(uv-loop-init uvloop)です(uvloopは前節でメモリ割当てしたポインタ変数).

直接呼ぶ場合には,引数と戻り値の記述順序がcffi:defcfunと逆で,

(cffi:foreign-funcall "uv_loop_init" :pointer uvloop :int)

で呼出せます.先ほどのメモリ割当てコードと合わせるとここまでの処理は以下の通りです.

;; install libuv and cffi first.
(require 'cffi)

(cffi:load-foreign-library '(:default "libuv"))

(cffi:defctype size_t :uint)

(defparameter *size* (* (cffi:foreign-type-size :uint)
			(cffi:foreign-funcall "uv_loop_size" size_t)))

(cffi:with-foreign-pointer (uvloop *size*)
  (cffi:foreign-funcall "uv_loop_init" :pointer uvloop :int))

ポインタ変数について

上の例ではポインタを取る関数にuvloopを渡しました.この例では出て来ませんが,ポインタが指し示す先を得る方法も説明しておきましょう.ポインタの参照先を得るにはcffi:mem-refを使います.例えば,char型のポインタ変数ptrの参照先は以下で得られます.

(cffi:mem-ref ptr :char)

ptrchar配列の場合,ptr[3](cffi:mem-ref ptr :char 3)でアクセスします.参照先に値を設定する場合は以下のように書きます.

(setf (cffi:mem-ref ptr :char 3) (char-code #\a))

CFFIによる実装仕上げ

残りのCコードは以下の通りです.

    printf("Now quitting.\n");
    uv_run(loop, UV_RUN_DEFAULT);

    uv_loop_close(loop);
    free(loop);

最後のメモリ解放はcffi:with-foreign-pointerを使った場合,自動的に行われるので必要ありません.

uv_run()関数のシグニチャはint uv_run(uv_loop_t* loop, uv_run_mode mode)で,uv_run_mode型はここで以下の様に定義されているenum型です.

typedef enum {
    UV_RUN_DEFAULT = 0,
    UV_RUN_ONCE,
    UV_RUN_NOWAIT
} uv_run_mode;

Cのuv_runを起動するときに引数でUV_RUN_DEFAULTの値を渡さなければいけないので,CFFIからCのenum型を定義し用意します.

(cffi:defcenum :uv-run-mode
  (:uv-run-default 0)
  :uv-run-once
  :uv-run-nowait)

uv_loop_close(loop)intを返す関数です.以上をまとめるとHello WorldサンプルコードのLisp版は以下のようになります.

;; install libuv and cffi first.
(require 'cffi)
(cffi:load-foreign-library '(:default "libuv"))

(cffi:defctype size_t :uint)
(defparameter *size* (* (cffi:foreign-type-size :uint)
			(cffi:foreign-funcall "uv_loop_size" size_t)))
(cffi:defcenum :uv-run-mode
  (:uv-run-default 0)
  :uv-run-once
  :uv-run-nowait)

(cffi:with-foreign-pointer (uvloop *size*)
  (cffi:foreign-funcall "uv_loop_init" :pointer uvloop :int)
  (format t "Now quitting.~%")
  (cffi:foreign-funcall "uv_run" :pointer uvloop :uv-run-mode :uv-run-default)
  (cffi:foreign-funcall "uv_loop_close" :pointer uvloop :int))

cl-libuvを使った実装

CFFIの実装を理解していれば基本的には同じです.cl-libuvではアンダースコアの関数名をハイフンに変えているだけです.

CFFIを用いた前節までの実装では,malloc(sizeof(uv_loop_t))を実装しました.libuvには他言語からの利用を考慮してuv_loop_tインスタンスを初期化してポインタを返すuv_default_loop()関数が用意されています.ここではこの関数を用いてメモリアロケーションを回避しています.

uv_run_mode型はcl-libuv/bindings.lispファイルでCのenum型として定義されています.Cのenum変数にアクセスするには,cffi:foreign-enum-valueに型名を指定してアクセスします.cl-libuvでは,lispifyマクロ(cl-libuv/wrapper.lispで定義)でenum変数名の先頭からuv-を取り除き:keywordパッケージにinternするので,UV_RUN_DEFAULT:RUN_DEFAULTでアクセスできます.

(require 'cl-libuv)

(defparameter *loop* (uv:uv-default-loop))
(uv:uv-run *loop* (cffi:foreign-enum-value
                        'uv:uv-run-mode :run-default))
(format t "Now quitting.~%")

(uv:uv-loop-close *loop*)