用 sbcl, asdf 和 cl-launch 編寫可分發的 lisp 程序

轉載自:用 sbcl, asdf 和 cl-launch 編寫可分發的 lisp 程序

如果你認爲看完並且看懂了這五本書:


1.《Common Lisp: A Gentle Introduction to Symbolic Computation》
2.《Common Lisp: The Language 2nd》
3.《On Lisp》
4.《Practical Common Lisp》
5.《計算機程序的構造和解釋》

就能寫出完整的 Common Lisp 程序來,那就大錯特錯了(不過要看完上述五本書仍然是一件很艱難而且很耗時的事,這就是爲何成爲 Lisp程序員更難一些的主要原因:語言規模大、並且有自己獨特的編程風格)。事實上對於 C 程序員來說,上述五本書基本上只相當於 K&R的《The C Programming Language》而已,正如寫一個完整的 C程序還需要諸如編譯、調試、Makefile(或者全套的autotools)以及各種各樣的第三方庫那樣,編寫完整的 Common Lisp程序決不僅僅是打開一個 lisp 的交互環境然後輸入一個 (format t "Hello, world!~%") 那麼簡單。

要想讓自己寫的 lisp 程序像其他語言編寫的程序那樣運行,我們要解決的是兩個問題:

1. 用一個類似 Makefile 的系統來幫助編譯多文件組成的源代碼,以及方便地引用其他 Lisp 軟件包。
2. 將 lisp 基於交互的運行模式轉化爲可在操作系統命令行上直接運行的獨立可執行程序。

下面我將給出一個完整的代碼示例,以實現一個普通的命令行程序,他能輸出 hello world,但是如果提供了命令行參數例如 "a b c",它就會根據每個參數生成類似這樣的輸出:
hello a
hello b
hello c
另外,我的程序也有自己的默認配置參數,這個參數決定了當我不給出任何命令行參數時,程序在 "hello" 字樣後面輸出的是 "world". 以下是全部的過程:

A. 確保你使用的是 Debian 或者 Ubuntu 系統,否則下列操作不可能全部照搬。然後至少要安裝 sbcl 和 cl-launch 軟件包。
B. 在文件系統裏給這個命名爲 hello 的項目建一個項目目錄。(對我自己來說,放在 /home/binghe/lisp/src/hello 目錄下了)
C. 在項目目錄裏(以下如果沒有特別指明的話,所有文件都是建立在項目目錄裏的) 建一個 hello.asd 文件,作爲 asdf 自動編譯系統的加載入口,代碼如下:

;;;; -*- Lisp -*-

(defpackage :hello-system (:use #:asdf #:cl))
(in-package :hello-system)

(defsystem hello
  :name "Hello World"
  :version "0.1"
  :author "Chun Tian (binghe)"
  :depends-on ()
  :components ((:file "package")
           (:file "config" :depends-on ("package"))
           (:file "hello" :depends-on ("config"))))

上述代碼有些類似於 Makefile,它首先定義了一個新的名爲 hello-system 的 package,使用了 asdf 包,以便在這個獨立的包裏可以定義一個獨立的 system,這是個 asdf 建議的好習慣,這樣我們所有的代碼將位於自己獨立的 package中,如果需要從 lisp 環境中清理出去就非常方便。

接下來的 defsystem 宏就定義了整個項目的代碼結構,以及一些無用的附加信息。重要的部分是components,它定義了三個有明確依賴關係的源代碼文件 package.lisp, config.lisp 和hello.lisp,一般而言,對於稍具規模的正規 lisp 程序,至少需要三個代碼文件:一個用來定義package,一個存放配置信息,一個放實際的業務邏輯代碼。如果此項目依賴於其他 asdf 格式的 lisp 軟件包,那麼寫在depends-on 段裏即可。

D. 定義 package,與 hello-system 不同,binghe.hello 這個 package 是用來實際放代碼的。以下是 package.lisp 文件的內容:

(in-package :hello-system)

(defpackage binghe.hello
  (:nicknames hello)
  (:use #:cl)
  (:export main *default-name*))

我定義了一個 binghe.hello 包,並且給這個較長的包名稱指定了一個較短的暱稱 hello,然後用 use段設置這個包可以訪問所有標準 Common Lisp 符號,根據 Lisp 標準他們位於 common-lisp 包裏,這個包的暱稱是cl。最後我導出了兩個 hello 包裏的符號作爲外部接口。

E. 定義配置代碼文件,用於指定默認輸出的名字:(這個文件在如此短小的項目裏毫無必要,只是出於演示目的)

(in-package :hello)

(defvar *default-name* "world")

F. 定義核心代碼文件:

(in-package :hello)

(defun main (args)
  (if (null args)
      (format t "hello ~A~%" *default-name*)
      (hello args)))

(defun hello (names)
  (when names
    (format t "hello ~A~%" (car names))
    (hello (cdr names))))

上述代碼裏有兩個函數定義,main 函數是整個程序的入口,入口參數是一個列表,如果列表爲空的話就產生默認輸出然後程序結束,否則就調用另一個函數hello 來實際產生針對每個列表元素的輸出,注意到這個函數我採用了尾遞歸的寫法,這在 lisp程序裏是非常自然的編程風格,完全沒有任何性能折損而且相比循環結構節省了顯式的循環變量。

G. 實際上如果代碼正確的話,現在在這個目錄裏運行 sbcl,然後輸入 (clc:clc-require :hello) 就可以編譯這個項目了:

binghe@localhost:~/lisp/src/hello$ ls -l
總計 20
-rw-r--r-- 1 binghe staff  53 2006-10-23 00:50 config.lisp
-rw-r--r-- 1 binghe staff 326 2006-10-23 00:48 hello.asd
-rw-r--r-- 1 binghe staff 226 2006-10-23 00:56 hello.lisp
-rw-r--r-- 1 binghe staff 161 2006-10-23 01:00 Makefile
-rw-r--r-- 1 binghe staff 122 2006-10-23 00:59 package.lisp
binghe@localhost:~/lisp/src/hello$ sbcl
This is SBCL 0.9.17, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
; in: LAMBDA NIL
;     (SB-KERNEL:FLOAT-WAIT)
;
; note: deleting unreachable code
;
; compilation unit finished
;   printed 1 note
CL-USER(1): (clc:clc-require :hello)

T
CL-USER(2): (quit)
binghe@localhost:~/lisp/src/hello$ sbcl
This is SBCL 0.9.17, an implementation of ANSI Common Lisp.
More information about SBCL is available at <http://www.sbcl.org/>.

SBCL is free software, provided as is, with absolutely no warranty.
It is mostly in the public domain; some portions are provided under
BSD-style licenses.  See the CREDITS and COPYING files in the
distribution for more information.
; in: LAMBDA NIL
;     (SB-KERNEL:FLOAT-WAIT)
;
; note: deleting unreachable code
;
; compilation unit finished
;   printed 1 note
CL-USER(1): (clc:clc-require :hello)

;;; Please wait, recompiling library...
; compiling file "/home/binghe/lisp/src/hello/package.lisp" (written 23 OCT 2006 12:59:11 AM):
; compiling (IN-PACKAGE :HELLO-SYSTEM)
; compiling (DEFPACKAGE BINGHE.HELLO ...)

; /var/cache/common-lisp-controller/1000/sbcl/local/home/binghe/lisp/src/hello/package.fasl written
; compilation finished in 0:00:00
; compiling file "/home/binghe/lisp/src/hello/config.lisp" (written 23 OCT 2006 12:50:45 AM):
; compiling (IN-PACKAGE :HELLO)
; compiling (DEFVAR *DEFAULT-NAME* ...)

; /var/cache/common-lisp-controller/1000/sbcl/local/home/binghe/lisp/src/hello/config.fasl written
; compilation finished in 0:00:00
; compiling file "/home/binghe/lisp/src/hello/hello.lisp" (written 23 OCT 2006 12:56:33 AM):
; compiling (IN-PACKAGE :HELLO)
; compiling (DEFUN MAIN ...)
; compiling (DEFUN HELLO ...)

; /var/cache/common-lisp-controller/1000/sbcl/local/home/binghe/lisp/src/hello/hello.fasl written
; compilation finished in 0:00:00

T
CL-USER(2): (hello:main nil)
hello world
NIL
CL-USER(3): (hello:main '("binghe" "netease" "sa"))
hello binghe
hello netease
hello sa
NIL


注意到編譯成功之後我立即測試了代碼,輸出看起來是正確的。由於 lisp 環境的初始所在包是cl-user,爲了引用其他包的函數我必須將包名也作爲函數名的一部分來使用:(hello:main ...) 或者(binghe.hello:main ...),測試結果對於空列表(nil) 和非空列表都是正確的,函數最後輸出的 NIL是函數的返回值,這個值只在交互環境下以求值爲目的運行函數時纔有意義,而我們調用 main 函數實際上是爲了得到副作用(標準輸出)而不是函數值。

H. 下面我們用 cl-launch 來生成可以在操作系統環境下執行的獨立程序,爲了方便起見,我使用了 make,做了一個真正的 Makefile:

hello: hello.asd *.lisp
    cl-launch -d hello.core -s hello -l sbcl -o hello --init "(hello:main cl-launch:*arguments*)"

clean:
    rm -f *~ hello hello.core *.fasl

注意 cl-launch 的各個參數,其中 -d 讓 lisp 環境 dump 出一個完整的 core文件以便加速程序的初始加載,這個參數對於大量引用了外部 lisp 包的情況特別有用,但對我們來說純粹是浪費,因爲 sbcl 會 dump出一個 20多兆的 core 文件來。
-s 參數用來加載 asdf 包,也就是我們剛剛做的 hello 包,藉此參數 cl-launch 就能加載我們所有的代碼了。-l參數設置使用的 lisp 平臺類型,cl-launch 還支持 cmucl 和 clisp 但是我們現在不用。-o設置了最後輸出的可執行腳本名。
--init 參數最重要,設置了程序的入口點。cl-launch 提供了一個命令行參數的約定入口 cl-launch:*arguments*以實現各種不同的 lisp 平臺的統一命令行參數支持,我要明確地讓這些命令行參數進入我們自己寫的 main 函數,並且這個函數首先執行。C語言實際上也有類似機制,那就是 main() 函數,實際上 C 編譯器把這個初始工作給偷偷做掉了並且不允許用戶修改這一行爲,Lisp則靈活一些。

於是我運行 make 命令,最後在項目目錄裏得到的文件如下:

cl-launch -d hello.core -s hello -l sbcl -o hello --init "(hello:main cl-launch:*arguments*)"
[undoing binding stack and other enclosing state... done]
[saving current Lisp image into /home/binghe/lisp/src/hello/hello.core:
writing 1912 bytes from the read-only space at 0x01000000
writing 1936 bytes from the static space at 0x05000000
writing 25640960 bytes from the dynamic space at 0x09000000
done]
binghe@localhost:~/lisp/src/hello$ ls -l
總計 25116
-rw-r--r-- 1 binghe staff       53 2006-10-23 00:50 config.lisp
-rwxr-xr-x 1 binghe staff     8054 2006-10-23 01:32 hello
-rw-r--r-- 1 binghe staff      328 2006-10-23 01:00 hello.asd
-rw-r--r-- 1 binghe staff 25681928 2006-10-23 01:32 hello.core
-rw-r--r-- 1 binghe staff      226 2006-10-23 00:56 hello.lisp
-rw-r--r-- 1 binghe staff      161 2006-10-23 01:00 Makefile
-rw-r--r-- 1 binghe staff      122 2006-10-23 00:59 package.lisp


注意,多了兩個文件。hello 是帶有可執行標誌位的腳本,hello.core 是 sbcl dump 出來的 corp 文件,內含整個Common Lisp 的語言實現以及我們自己寫的所有程序的二進制形式。實際上,商業 Lisp 實現與開源 Lisp實現的主要區別就在這裏,商業 Lisp 實現能 dump 出一個小得多的真正的可執行文件,其中只含有我們的程序用得着的那些 CommonLisp 語言實現部分,其他沒有用的東西在 dump 的時候直接扔掉了。

現在我們可以測試這個編譯成果了:

binghe@localhost:~/lisp/src/hello$ ./hello
hello world
binghe@localhost:~/lisp/src/hello$ ./hello a b c
hello a
hello b
hello c


現在,hello 和 hello.core 文件可以分發到其他安裝了 sbcl 和 cl-launch 的 Debian 系統下運行了。不過,還有很明顯的需求沒有滿足:
1) 能得到一個在沒有 sbcl 的 Linux 系統(包括非 Debian 的系統) 下也能運行的可執行程序嗎?
2) 能得到一個單一的可執行文件,完全脫離腳本嗎?

這兩個問題都是可以解決的,但是需要更多的工作,我將在下一篇文章裏介紹。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章