実行ファイルの作り方の、仕組みと処理系ごとの詳しい方法について説明します。
ランタイムとは、Lispのプログラムを動かすために必要なもののことです。S式を読んだり、評価したり、といった中核の部分(カーネルと呼ばれたりします)と、その中核の部分を利用して動かす、基本的なライブラリなどが含まれる部分を合わせて、ランタイムと呼びます。
ANSI Common Lispで決められている、処理に必要な振る舞いのうち、どこまでを中核の部分に含めるか、どこまでをどの言語で書くかは、処理系によって異なります。本当に最低限の部分だけを中核としてCで書き、コンパイラまでLispで書いてしまう処理系もあれば、ライブラリの部分までCで書いてしまう処理系もあります。また、C++やJavaで書かれている処理系もあります。
実際のLispのプログラムは、このランタイムを利用して動きます。
イメージとは、コンパイルされた関数の定義や、変数の定義、型の定義、クラスの定義、処理系内部の状態など、その時点での処理系の状態を再現するために必要な、様々な情報のことです。コアイメージ、ヒープイメージなどと呼ばれたりもします。
狭い意味でのイメージは、こういった情報それ自体、あるいは、ひとつのファイルにまとめて保存したもののことを指しますが、CへのトランスレータがCのコンパイラを経由して作るオブジェクトファイルなども、目的は同じため、一種のイメージと言えます。
Lispの処理系が実行ファイルを作る方法は単純です。Cなどと本質的に同じで、結局のところは、ランタイムとイメージを組み合わせているだけです。詳細に差はありますが、どの処理系でもこれは変わりません。
ここでのトップレベルと、HyperSpecで定義されているトップレベルフォームとは関係ありません。ランタイムによるスタートアップコードが起動する、最初に行われる処理のことです。エントリーポイントとも呼ばれます。
ランタイムのトップレベルの処理は多くの場合REPL(式の読み込み→式の評価→結果の表示の繰り返し)ですが、ほとんどのアプリケーションでは、起動直後にREPLに入られても困ります。REPLが不要な場合、適切なトップレベルの処理を指定したり、REPLに入る前にプログラムを終了させたりする必要があります。
商用処理系などでは、ランタイムとイメージを組み合わせるとき、アプリケーションを配布するのに不要な部分、例えばコンパイラやデバッガなどを省いて、出来上がるファイルのサイズを抑える機能があります。これをTree-shakingと呼びます。
Common Lispは標準ライブラリが大きいので、できあがる実行ファイルも大きくなりがちですが、この機能がある処理系では多少大きさを抑えることができます。
ヒープ領域にあるオブジェクトを静的領域に移動することで、GCの効率を上げることができる機能です。イメージを保存するときにこの処理をすることで、効率良く動作するイメージを作ることができる場合があります。
イメージをファイルに保存する関数のsb-ext:save-lisp-and-dieでキーワード引数executableにnil以外の値を渡します。
トップレベルの処理はキーワード引数toplevelで指定した関数で、標準ではREPLです。
$ cat sbcl.lisp (defun hello () (format t "hello, world~%")) (sb-ext:save-lisp-and-die "hello-sbcl" :toplevel #'hello ; トップレベルをhelloに :executable t) $ sbcl --noinform --no-sysinit --no-userinit --load sbcl.lisp [undoing binding stack and other enclosing state... done] [saving current Lisp image into hello-sbcl: writing 6352 bytes from the read-only space at 0x20000000 writing 4064 bytes from the static space at 0x20100000 writing 59428864 bytes from the dynamic space at 0x1000000000 done] $ ./hello-sbcl hello, world
イメージをファイルに保存する関数のext:saveinitmemでキーワード引数executableにnil以外の値を渡します。
トップレベルの処理はREPLから変更できませんが、REPLを起動する前にキーワード引数init-functionに指定した関数を呼ぶことができるため、その関数の中でext:exitを呼んでプログラムを終了すれば、REPLには入りません。
$ cat clisp.lisp (defun hello () (print "hello, world") (ext:exit)) (ext:saveinitmem "hello-clisp" :quiet t ; バナーを表示しない :norc t ; 初期化ファイルをロードしない :init-function #'hello ; REPLの前にhelloを呼ぶ :executable t) $ clisp -norc clisp.lisp Bytes permanently allocated: 92,512 Bytes currently in use: 2,189,152 Bytes available until next GC: 546,473 $ ./hello-clisp "hello, world"
イメージをファイルに保存する関数のccl:save-applicationでキーワード引数prepend-kernelにnil以外の値を渡します。
トップレベルの処理はキーワード引数toplevel-functionで指定した関数で、標準ではREPLです。
$ cat ccl.lisp (defun hello () (print "hello, world")) (ccl:save-application "hello-ccl" :toplevel-function #'hello ; トップレベルをhelloに :prepend-kernel t) $ ccl --no-init --load ccl.lisp $ ./hello-ccl "hello, world"