iT邦幫忙

2026 iThome 鐵人賽

0
Software Development

玩轉ABCL Lisp!用奇怪偏門的方式學習JavaFX系列 第 32 篇

lesson 23(S4): 單一檔案編譯成JAVA字節碼,保存成ABCL檔案

  • 分享至 

  • xImage
  •  

編譯lisp檔案➜使用abcl檔案加速載入

TL;DR

ABCL 可以用 compile-file 將 Lisp 原始碼編譯成 .abcl FASL 檔案。這種檔案包含 Java 位元組碼與 ABCL 載入資訊,不能直接當作 JAR 使用;本文示範如何編譯、載入單一檔案,以及編譯整個 ASDF 系統。

ABCL 的 FASL 格式

在取得ASDF系統資源路徑一文中曾提到,使用asdf:load-system時,實際載入的檔案可能不是原始Lisp檔案,因此不能總是依賴*load-pathname*或*load-truename*定位原始碼。

和其他Common Lisp實作一樣,ABCL也提供HyperSpec中的compile-file,可將原始碼編譯成方便快速載入的 FASL(Fast Load)檔案。由於 ABCL 執行於 Java 平台,編譯結果不是機器碼,而是 Java 位元組碼(Java bytecode);輸出檔案使用 .abcl 格式,而不是可直接使用的 .class 檔案。

ECL 也提供 compile-file。由於它以 C 語言實作,還支援多種輸出目標格式:

  1. Portable FASL file (.fasc)
  2. Native FASL file (.fas, .fasb)
  3. Object file (.o)
  4. Static library
  5. Shared library
  6. Executable file
    可參考:https://ecl.common-lisp.dev/static/files/manual/current-manual/System-building.html#Compiling-with-ECL-1

Common Lisp 可以直接載入原始碼,也可以先編譯再載入。因此,asdf:load-system不一定直接載入原始碼;依照實作和設定,它可能先編譯,再載入編譯結果。在我的環境執行abcl --noinform --noinit --load run-system.lisp --eval '(quit)',可以看到載入的是${HOME}/.cache/common-lisp/abcl-1.9.2-fasl43-linux-x64/目錄下的.abcl檔案。

其實Python、Lua等很多相似語言都存在這樣的中間態(.pyc、.luac)。

編譯單一檔案

以lesson1 - Hello abclFX中的main.lisp為例,在終端機執行以下指令:

abcl --noinform --noinit --eval '(compile-file "main.lisp")' --eval '(quit)'
# or
abcl --noinform --noinit --eval '(compile-file "main.lisp")' --batch

執行後會產生編譯結果main.abcl。.abcl檔案實際上是ZIP壓縮檔:

➜  lesson1 - Hello abclFX $ file main.abcl
main.abcl: Zip archive data, at least v2.0 to extract, compression method=deflate

使用unzip -l main.abcl查看壓縮檔內容:

Archive:  main.abcl
  Length      Date    Time    Name
---------  ---------- -----   ----
     3307  2026-10-06 22:29   __loader__._
     3287  2026-10-06 22:29   main_1.cls
     1371  2026-10-06 22:29   main_2.cls
     2869  2026-10-06 22:29   main_3.cls
---------                     -------
    10834                     4 files

以上結構和JAR檔案很像:兩者都是ZIP壓縮檔,也都包含Java位元組碼;但.abcl不能直接當作JAR檔案使用。

載入器檔案__loader__._的內容是以S-expression表示的純文字;副檔名為.cls的檔案則是Java class檔案的變形格式。

類似於有些JAR檔案會有MANIFEST.MF,以描述檔案特性。

$ file *.cls
main_1.cls: compiled Java class data, version 49.0 (Java 1.5)
main_2.cls: compiled Java class data, version 49.0 (Java 1.5)
main_3.cls: compiled Java class data, version 49.0 (Java 1.5)

使用 https://jar.tools 反編譯main_2.cls,可以看到以下內容:

/*
 * Decompiled with https://jar.tools
 */
package org.armedbear.lisp;

import org.armedbear.lisp.ArgumentListProcessor;
import org.armedbear.lisp.ClosureBinding;
import org.armedbear.lisp.CompiledClosure;
import org.armedbear.lisp.Lisp;
import org.armedbear.lisp.LispObject;
import org.armedbear.lisp.Load;
import org.armedbear.lisp.Symbol;

public final class main_2
extends CompiledClosure {
    static final Symbol SYM175 = (Symbol)Load.getUninternedSymbol(0);

    public final LispObject execute(LispObject var1) {
        ClosureBinding[] var3 = this.ctx;
        var3[0].value = var1;
        return Lisp.nonLocalGo(var3[1].value, SYM175);
    }

    public main_2() {
        ArgumentListProcessor.OptionalParam[] var1 = new ArgumentListProcessor.OptionalParam[0];
        ArgumentListProcessor.KeywordParam[] var2 = new ArgumentListProcessor.KeywordParam[0];
        ArgumentListProcessor var3 = new ArgumentListProcessor() /* <init> missing? */;
        super(new ArgumentListProcessor(null, 1, var1, var2, false, false));
        var3.setFunction(this);
    }
}

main_1.cls並未完整解讀成Java程式碼,仍保留大量類組語的操作內容。
至於main_3.cls和main_2.cls差不多,不過有個比較特別的片段:使用了Lisp.readObjectFromString(),參數內容同樣放了小段LISP程式碼的字串。(我好像看到了寫在HelloFX的:methods了XD)

(("main"
  #1=:VOID
  #2=NIL
  (#3=LAMBDA
   (#4=THIS)
   (#5=JSTATIC "launch" +APPLICATION+ (JOBJECT-CLASS #4# ) (JNEW-ARRAY "java.lang.String" 1))))
 ("start"
  #1#
  ("javafx.stage.Stage")
  (#3#
   (#4#  #6=STAGE)
   (PROG* ((#7=JAVA-VERSION
            (#5#  "getProperty" "java.lang.System" "java.version"))
           (#8=JAVAFX-VERSION (#5#  "getProperty" "java.lang.System" "javafx.version"))
           (#9=L (#10=JNEW +LABEL+ (FORMAT #2#  "Hello, JavaFX ~A, running on Java ~A." #8# #7# )))
           (#11=SCENE (#10#  +SCENE+ (#10#  +STACKPANE+ (JARRAY-FROM-LIST (SYSTEM::BACKQ-LIST #9# ))) 640 480)))
      (#12=JCALL "setScene" #6#  #11# )
      (#12#  "show" #6# )))))

載入 .abcl 檔案

.abcl檔案同樣可以用#'load載入:

(load "main.abcl")

呼叫load時也可以省略副檔名;ABCL會先嘗試載入.abcl,找不到時再嘗試.lisp。

(load "main")

編譯 ASDF 系統

接著以lesson14 - CalcFX為例,示範如何編譯整個ASDF系統:(asdf:compile-system "abcl-javafx")

(require :abcl-contrib)
(require :asdf)
(require :abcl-asdf)

;; 禁用output-translations,讓編譯結果直接在原始檔旁邊
(asdf:disable-output-translations)

(push #P"./" asdf:*central-registry*)

(asdf:compile-system "abcl-javafx")

下一篇會接著示範如何將整個 ASDF 系統打包成 JAR 檔案。


上一篇
lesson 22(S4): 實現JAVA介面的另一種方式:jmake-proxy
下一篇
lesson 24(S4): 怎麼把ABCL System打包成JAR檔案?
系列文
玩轉ABCL Lisp!用奇怪偏門的方式學習JavaFX 共 33 篇
圖片
  熱門推薦
圖片
{{ item.channelVendor }} | {{ item.webinarstarted }} |
{{ formatDate(item.duration) }}
直播中

尚未有邦友留言

立即登入留言