在 REPL 中运行代码当然是不错的,但要简便的发行软件时,就需要构建一个可执行的软件了。
尽管各个Lisp解释器的实现不相同,但都可以创建出 可执行(self-contained executables) 程序,这是因为他们都是基于基础架构构建而成的。用户不需要安装 Lisp 解释器就可以直接执行程序。
在使用 SBCL 和 CCL 时构建时,程序几乎能够瞬间启动。
二进制文件的大小通常都会很大,这是因为里面包含了整个程序所需的依赖库、符号链接、函数的参数信息,还有编译器、调试器以及源代码的地址等详细信息。
注意,同样的也可以构建可执行的 web 应用。
构建可执行程序
SBCL
如何构建能够运行的软件取决于解释器(可以参见下面章节 Buildapp 和 Rowsell)。在 SBCL 中,如文档中提到的那样,构建方法如下:
(sb-ext:save-lisp-and-die #P"path/name-of-executable" :toplevel #'my-app:main-function :executable t)
sb-ext
是 SBCL 执行其他进程的一个插件。更多关于 SBCL 插件的用法,参见SBCL extensions 文档(里面大部分插件在其他的库中都是可移植的)
:executable t
表示创建一个可执行程序,而不是创建个镜像。当然,也可以将当前的状态保存为一个 Lisp 镜像,在之后可以重新恢复过来。镜像在进行计算密集型的工作时很有用。
不要在 Slime 中执行上面的代码,不然会出现一下的错误提示:
Cannot save core with multiple threads running.
直接启动 SBCL 然后在里面运行上面的代码就好。
假如创建的项目是依赖于 Quicklisp 的,那么必须按照下面的步骤进行:
- 确保 Quicklisp 已经安装并在启动 sbcl 时会自动加载(即完成了 Quicklisp 的安装)
- 加载了项目的 .asd 文件
- 安装了所有的依赖关系
- 构建可执行
代码大致如下:
(load "my-app.asd")
(ql:quickload :my-app)
(sb-ext:save-lisp-and-die #p"my-app-binary" :toplevel #'my-app:main :executable t)
如果是使用命令行或是 Makefile 的话,需要使用 --load
和 --eval
:
build:
sbcl --load my-app.asd \
--eval '(ql:quickload :my-app)' \
--eval "(sb-ext:save-lisp-and-die #p\"my-app\" :toplevel #'my-app:main :executable t)"
ASDF
上面已经介绍了基础部分,现在来介绍更方便的方法。基于 ASDF 的版本是 3.1,因此有了更简单的方法,就是 [make](https://common-lisp.net/project/asdf/asdf.html#Convenience-Functions)
命令,运行 make
后,程序会自动从 .asd 中获取相应的参数。不过需要在 .asd 中添加以下配置:
:build-operation "program-op" ;; leave as is
:build-pathname "<binary-name>"
:entry-point "<my-package:main-function>"
然后调用 asdf:make :my-package
.
因此,其 Makefile 是这样的:
LISP ?= sbcl
build:
$(LISP) --load my-app.asd \
--eval '(ql:quickload :my-app)' \
--eval '(asdf:make :my-app)' \
--eval '(quit)'
Roswell or Buildapp
Roswell, 一个解释器的管理器,可以在多个解释器中使用 ros build
命令。
也可以通过 Roswell 的 ros install my-app
命令来构建个可以安装的软件。具体用法参见官方文档。
最后介绍的是 Buildapp,一个经历了时间的验证依然流行的 ”配置和保存可执行的 Common Lisp 镜像的 SBCL 或 CCL 应用程序”。
使用示例;
buildapp --output myapp \
--asdf-path . \
--asdf-tree ~/quicklisp/dists \
--load-system my-app \
--entry my-app:main
很多程序都在使用 buildapp (比如说 pgloader),在 Debian 上可以通过命令 apt install buildapp
进行安装,但用了 asdf:make 或是 Roswell 的话就没必要安装 buildapp 了。
Web 应用程序
同样的,可以简单的创建可执行的 web 应用。该应用将作为一个 web 服务端,并可以使用命令行来运行:
$ ./my-web-app
Hunchentoot server is started.
Listening on localhost:9003.
注意这是运行在 web 服务器上的,而不是开发环境中,因此可以在 VPS(Virtual Personal Server)上直接运行然后访问这台 web 服务器。
还有一个注意点是,就是找到运行中的 web 应用然后将其放到前台。在 main
函数中,可以这样做:
(defun main ()
(start-app :port 9003) ;; our start-app, for example clack:clack-up
;; let the webserver run.
;; warning: hardcoded "hunchentoot".
(handler-case (bt:join-thread (find-if (lambda (th)
(search "hunchentoot" (bt:thread-name th)))
(bt:all-threads)))
;; Catch a user's C-c
(#+sbcl sb-sys:interactive-interrupt
#+ccl ccl:interrupt-signal-condition
#+clisp system::simple-interrupt-condition
#+ecl ext:interactive-interrupt
#+allegro excl:interrupt-signal
() (progn
(format *error-output* "Aborting.~&")
(clack:stop *server*)
(uiop:quit)))
(error (c) (format t "Woops, an unknown error occured:~&~a~&" c))))
在上面的代码中,使用了 bordeaux-threads
((ql:quickload "bordeaux-threads")
,命名为 bt
)和 uiop
,其中 uiop
是 ASDF 的一部分,默认会加载。这样就可以以一种可移植的方式退出(使用 uiop:quit
而不是 sb-ext:quit
,因为 uiop:quit
有个可选的返回代码)。
解释器生成可执行文件的大小及启动时间
SBCL 并不是唯一的 Lisp 解释器。ECL,一个嵌入式 Common Lisp 解释器,可讲 Lisp 程序编译成 C 语言,这样就能生成个小一点的二进制文件。
从 reddit 上的贴来看,ECL 生成的二进制文件是所有解释器中大小最小的,要比 sbcl 生成的要小一个数量级,但是启动时间要更长。
用 CCL 生成的二进制文件和 SBCL 生成的启动时间要差不多,但 CCL 生成的文件大小是 SBCL 一半。
| program size | implementation | CPU | startup time |
|--------------+----------------+------+--------------|
| 28 | /bin/true | 15% | .0004 |
| 1005 | ecl | 115% | .5093 |
| 48151 | sbcl | 91% | .0064 |
| 27054 | ccl | 93% | .0060 |
| 10162 | clisp | 96% | .0170 |
| 4901 | ecl.big | 113% | .8223 |
| 70413 | sbcl.big | 93% | .0073 |
| 41713 | ccl.big | 95% | .0094 |
| 19948 | clisp.big | 97% | .0259 |
SBCL 核心压缩(core compression)
SBCL 的核心压缩构建可以极大地减少应用程序二进制文件的大小。在示例中中,将文件大小从 120MB 缩小到 23MB,启动时间增加了 12ms,但启动时间仍然低于 50ms!
要注意的是安装的 SBCL 必须支持核心压缩,具体参见文档:http://www.sbcl.org/manual/#Saving-a-Core-Image
是这种情况吗?
(find :sb-core-compression *features*)
:SB-CORE-COMPRESSION
没错,这就是在 Debian 上的 SBCL 的结果。
SBCL
在 SBCL 中,需要给出 save-lisp-and-die
的参数,如 :compression
may be an integer from -1 to 9, corresponding to zlib compression levels, or t (which is equivalent to the default compression level, -1).
-1 到 9 的级别大概有 1MB 的差别。
ASDF
然而,我们更喜欢使用 ASDF(或者是说 UIOP)。在 .asd 文件中添加以下代码:
#+sb-core-compression
(defmethod asdf:perform ((o asdf:image-op) (c asdf:system))
(uiop:dump-image (asdf:output-file o c) :executable t :compression t))
Deploy
当然,Deploy 库也能用来构建完整独立的应用。如果可以的话,deploy
也会使用压缩技术.
deploy 专门用来构建依赖外部库的程序。deeply 会收集所有的外部共享库的依赖,比如说 bin
子目录下中的 libssl.so 共享库。
解析命令行参数
SBCL 将命令行的参数保存在 sb-ext:*posix-argv*
变量中。
但不同的解释器保存参数的变量名不一样,所以就会想要使用个三方库来处理这些差异。
同时也想要解析这些参数。
快速浏览一遍 awesome-cl#scripting 然后就开始讲解 unix-opts 库
(ql:quickload "unix-opts")
也可以通过 unix-opts
库的别名 opts
来调用。
通常分为两个阶段:
- 定义程序的参数,可选参数以及参数的类型(字符串,整型等),或者或短的名字和必须的参数。
- 解析(包括处理参数丢失或格式错误)。
声明参数
可以使用 opts:define-opts
来声明定义参数:
(opts:define-opts
(:name :help
:description "print this help text"
:short #\h
:long "help")
(:name :nb
:description "here we want a number argument"
:short #\n
:long "nb"
:arg-parser #'parse-integer) ;; <- takes an argument
(:name :info
:description "info"
:short #\i
:long "info"))
其中 parse-integer
是 CL 的内建函数。
以下是使用命令行的示例(其中帮助文档是自动生成的):
$ my-app -h
my-app. Usage:
Available options:
-h, --help print this help text
-n, --nb ARG here we want a number argument
-i, --info info
解析参数
通过 opts:get-opts
来获取参数,其中 opts:get-opts
会返回两个值:有效参数的列表和剩下的参数。之后就需要使用 multiple-value-bind
将这两个值绑定到变量;
(multiple-value-bind (options free-args)
;; There is no error handling yet.
(opts:get-opts)
...
可以给 get-opts
字符串列表来进行验证:
(multiple-value-bind (options free-args)
(opts:get-opts '("hello" "-h" "-n" "1"))
(format t "Options: ~a~&" options)
(format t "free args: ~a~&" free-args))
Options: (HELP T NB-RESULTS 1)
free args: (hello)
NIL
当传入未知的选项时,将会进入到 debugger。关于异常处理的部分马上会介绍到。
因此,option
是个 plist。我们可以通过 getf
和 setf
来操作 plist,这就是我们的逻辑。下面将通过 opts:describe
输出帮助文档,通过 exit
退出(一种可移植的方法)。
(multiple-value-bind (options free-args)
(opts:get-opts)
(if (getf options :help)
(progn
(opts:describe
:prefix "You're in my-app. Usage:"
:args "[keywords]") ;; to replace "ARG" in "--nb ARG"
(opts:exit))) ;; <= optional return status.
(if (getf options :nb)
...)
想要查看完整的例子的话,参见 official example 和 cl-torrents’ tutorial。
unix-opts 库中建议使用宏,因为宏的效果要好一些。下面来介绍异常处理。
格式错误及参数丢失处理
以下 4 中情况 unix-opts 无法处理,但可以抛出异常:
- 未知参数:抛出
unknown-option
异常 - 缺少参数:抛出
missing-arg
异常 - 格式错误:抛出
arg-parser-failed
异常。如参数是整型而传进来的是文本。 - 缺少必需选项: 抛出
missing-required-option
异常。
因此,就需要创建一些简单的函数来处理这些异常,然后通过 handler-bind
进行解析:
(multiple-value-bind (options free-args)
(handler-bind ((opts:unknown-option #'unknown-option) ;; the condition / our function
(opts:missing-arg #'missing-arg)
(opts:arg-parser-failed #'arg-parser-failed)
(opts:missing-required-option))
(opts:get-opts))
…
;; use "options" and "free-args"
在这里,我们想要处理各种异常情况,但有个更简单点的方法,就是将异常情况作为参数:
(defun handle-arg-parser-condition (condition)
(format t "Problem while parsing option ~s: ~a .~%" (opts:option condition) ;; reader to get the option from the condition.
condition)
(opts:describe) ;; print help
(opts:exit)) ;; portable exit
更多关于异常处理的,参考 第12章:异常处理
捕获终止信号(C-c)
先创建一个简单的二进制文件,运行,然后按 C-c
,查看 stacktrace:
$ ./my-app
sleep…
^C
debugger invoked on a SB-SYS:INTERACTIVE-INTERRUPT in thread <== condition name
#<THREAD "main thread" RUNNING {1003156A03}>:
Interactive interrupt at #x7FFFF6C6C170.
Type HELP for debugger help, or (SB-EXT:EXIT) to exit from SBCL.
restarts (invokable by number or by possibly-abbreviated name):
0: [CONTINUE ] Return from SB-UNIX:SIGINT. <== it was a SIGINT indeed
1: [RETRY-REQUEST] Retry the same request.
信号的命名是在实现之后:sb-sys:interactive-interrupt
。只需将程序添加到 hadler-case
中就可以:
(handler-case
(run-my-app free-args)
(sb-sys:interactive-interrupt () (progn
(format *error-output* "Abort.~&")
(opts:exit))))
然而,上面这段代码只能在 SBCL 中有效。之前已经介绍过 trivial-signal 但目前并不满足所需要的测试。因此可以像下面这样:
(handler-case
(run-my-app free-args)
(#+sbcl sb-sys:interactive-interrupt
#+ccl ccl:interrupt-signal-condition
#+clisp system::simple-interrupt-condition
#+ecl ext:interactive-interrupt
#+allegro excl:interrupt-signal
()
(opts:exit)))
其中, #+
开头的代码表示在不同的解释器中执行不同的操作。当然,也有 #-
。 #-
的作用是在 *features*
这个变量中去匹配后面的符号,也可以使用 and
、 or
和 not
对 #+
后面的符号进行组合。
二进制文件的可持续交付
在提交、发布时或者其他的操作是,也可以使用持续集成系统(Travis CI,Gitlab CI 等)。
更多参见 Continuous Integration。