Clamiga: 用于 Amiga 的 Common Lisp
摘要
Clamiga 是一个用于 Amiga 系统和其他平台的 Common Lisp 实现,具有字节码虚拟机和交互式开发功能。
暂无内容
查看缓存全文
缓存时间: 2026/08/16 18:45
# Manfred Bergmann | 软件开发
来源:https://nnamgreb.de/blog/Clamiga+-+Common+Lisp+for+the+Amiga
### Clamiga —— Amiga 平台的 Common Lisp
继过去几个月的 ACE BASIC 系列文章,本文将介绍另一个我已投入半年时间的项目:**CL-Amiga**,简称 **Clamiga**。这是一个为 Amiga 家族构建的 Common Lisp 实现——支持经典 AmigaOS 3(基于 68k 架构)和 MorphOS(完全原生的 PPC 构建),AROS 和 AmigaOS 4 也许未来会支持——但它也能在 macOS(我将其作为主要开发环境)和 Linux 上运行。名字很简单:*Common Lisp for the Amiga* 缩写为 CL-Amiga,读起来就是“Clamiga”。而由于 *amiga* 在西班牙语/葡萄牙语中意为(女性)朋友(Amiga 用户应该都懂),这个名字一语双关:既是在你的 Amiga 上运行的 Lisp,也是你的 *amiga*(朋友);)。(项目链接见文末)
Clamiga 在 AmigaOS 3 上运行
### 简介 Common Lisp
Common Lisp 是至今仍在活跃使用的较老编程语言之一。是的,它在工业界及许多其他领域都有应用。ANSI 标准制定于 1994 年,此后未再变动——然而这门语言却出人意料地现代(通常符合 ANSI 标准的旧 Common Lisp 程序在现代编译器上也能编译)。它拥有完整的对象系统,支持多重分派(CLOS),条件系统超越了异常机制,宏允许你扩展语言本身,还有一种极其交互式、基于镜像的开发风格,让你能在运行中的系统里编译和重新定义函数。过去几十年许多“新”语言特性,在 Common Lisp 中早已存在。我在这里不再重复这些内容。如果你想得到一份合适的入门介绍,我几年前写过一篇入门手册:Common Lisp - Oldie but goldie (https://nnamgreb.de/blog/Common+Lisp+-+Oldie+but+goldie)。另外还有一篇关于 Common Lisp 中函数式编程的文章 (https://nnamgreb.de/blog/Functional+Programming+in+(Common)+Lisp),如果你感兴趣的话。
### 为什么又造一个 Common Lisp 实现?
市面上已有优秀的 Common Lisp 实现——SBCL、CCL、ECL、Clasp、CLISP(已多年未维护),甚至商业版本如 LispWorks 和 Allegro。
**但它们没有一个能在 Amiga 上运行。** 高性能的实现(SBCL、CCL)是绑定到现代架构(x86-64、ARM、PPC)的原生代码编译器,没有 68k 后端,且内存占用以十兆字节计。Clasp 基于 LLVM 构建。CLISP 在精神上最接近——一个用 C 编写的紧凑字节码解释器——但已多年未维护,也已有数十年没有 AmigaOS 构建了。Clamiga 专为运行在 m68k Amiga 上而构建。它有一个自包含的字节码虚拟机,使用可移植的 C 编写,没有外部运行时依赖——不需要 libffi、LLVM 或 C 编译器。作为一个完整的 Common Lisp 实现,其语言和运行时特性意味着,它无法在 Amiga 上与纯 C 或汇编程序比拼性能记录。因此,它并非用来编写需要超级流畅滚动等性能的游戏。m68k-JIT 尝试从中挤出更多性能,但有其极限。它有其他特性,例如完整的数值塔,开箱即用地支持从位(位向量)、有理数、复数到大整数。它拥有用于交互式开发的 REPL、调试器和检查器。利用条件系统、重启等机制(参见我之前的老牌精品文章)实现容错。它有套接字、线程、文件系统访问,以及原生实现到 AmigaOS API 的流,使得 Common Lisp 代码保持兼容性,通常无需移植到其他系统。CLOS(Common Lisp 对象系统)是我所见过的最先进的对象系统。如果你想,也可以进行函数式编程。但让我们看一些更技术性的东西。
### 下文提到的一些术语
- **ASDF** (https://asdf.common-lisp.dev/):Another System Definition Facility,是 Common Lisp 事实上的标准构建工具。它有点像 Java 世界中的 `make` 或 Gradle。它可以管理依赖图、版本控制等。大多数 Common Lisp “库”(在 ASDF 中称为“系统”)都是用 ASDF 构建的。实际上,当今所有维护中的 Common Lisp 实现都自带 ASDF,Clamiga 也不例外。
- **FASL**:FASt Load。当编译 Lisp 源代码(例如通过 `(compile-file "foo.lisp")`)时,会生成 FASL 文件。FASL 文件比 Lisp 源代码文件加载更快,因为它们已编译为序列化格式。所有 Common Lisp 变体都实现了 FASL,Clamiga 也是。Clamiga 生成的 FASL 文件,无论是在 m68k 还是 PPC 上生成的,在这两种架构之间可以互换。
- **Quicklisp** (https://www.quicklisp.org/beta/):一个 Common Lisp 的库管理器,包含超过 1,500 个库。Clamiga 附带了兼容层,使得(理论上)Quicklisp 上可用的许多库都能被使用。不过许多库需要更强的计算能力,可能超出了 m68020 的能力范围。
### 工作原理
Clamiga 是一个单遍编译器,将 S-表达式编译为字节码,由基于栈的虚拟机执行。一些设计决策直接源于 Amiga 的约束:
- **标签化 32 位值。** 每个 Lisp 值都是一个 32 位字。堆指针是相对于内存区域的偏移量,这使得整个对象模型保持 32 位清洁且紧凑(关于紧凑数组见下文)。
- **压缩式 GC。** 小堆很快就会碎片化。标记-清除收集器可以在碎片化阻碍分配时滑动压缩堆,因此即使在 4 MB 或 8 MB 内存上长时间运行也不会慢慢死掉。
- **架构无关的字节码。** 因为执行的是字节码,相同的编译后 Lisp 代码在 68k 和 PowerPC 上无需更改即可运行。编译好的 FASL 文件在 AmigaOS 3 和 MorphOS 之间是字节兼容的。可移植 C 核心的一个美妙副作用:完全相同的系统可以在 macOS 和 Linux 上构建和运行。开发、调试和大部分测试都在快速主机上进行,而结果在 Amiga 上的表现(大多数情况下)完全一致。
### 在经典 Amiga 上:与 4 MB 内存共存,嗯,更少也行
即使加速卡提供了更多 RAM,在 68k Amiga 上内存仍然常常是个限制,因此投入了大量工作来尊重这一点。完整的 Common Lisp 核心——CLOS、条件、数值塔、format、loop——启动仅需大约 **0.5 MB** 堆空间。编写简单程序时,`clamiga --heap 1M` 就足够了。默认值可以从这里按需扩展:
| 用例 | 堆 | 栈 |
| :--- | :--- | :--- |
| 简单程序(无 Quicklisp/ASDF) | `--heap 1M` | 64K(默认) |
| 小型到中型程序 | 4M(默认) | 64K(默认) |
| 加载 ASDF | `--heap 11M` | 64K(默认) |
| Quicklisp + quickload 库 | `--heap 24M` | `stack 128000` |
一些技术使得小堆变得实用:
- **紧凑字节数组:** `(make-array n :element-type '(unsigned-byte 8))` 每个元素存储 1 字节,而不是 4 字节的标签值,并且 GC 永远不会扫描其内容。在 8 MB 机器上,这使得 I/O 缓冲区、图形平面数据和音频样本大小缩减 4 倍,并且基本上免费收集。还有用于 16 位值的紧凑字节数组。
- **批量序列 I/O:** 对字节数组使用 `read-sequence` 和 `write-sequence`,每次 OS 调用移动整个块,而不是每个字节一次虚拟机往返。在 14 MHz 的 68020 上,这将加载 20 KB 资源文件的时间从几秒钟转变为文件 I/O 速度。
- **预编译启动 FASL:** 标准库和 CLOS 以预编译形式提供。在低端 020 基线设备上,这将冷启动时间从约 92 秒缩短至约 9 秒。如果栈对于深度嵌套的形式太小,Clamiga 会发出清晰的 `C stack nearly exhausted` 错误,告诉你增加栈大小——它不会破坏会话。
### m68k JIT
在 AmigaOS 构建版(68020+)中,Clamiga 在定义时将字节码函数转换为原生 m68k 机器码。VM 调度器然后直接跳入原生代码体,而不是解释字节码。转换器覆盖了指令集的广泛核心:整数运算、分支、列表操作、结构体槽访问、函数调用和自递归尾调用、闭包、多值、非局部退出、动态绑定以及 AmigaOS FFI。任何尚未处理的部分都会透明地回退到解释器——你永远不会注意到,事情只是运行。
一些 A/B 测试数据,在模拟的 A4000/68040 上测量,函数体相同,仅切换调度路径:
| 基准测试 | 形状 | 字节码 | JIT | 加速比 |
| :--- | :--- | :--- | :--- | :--- |
| `sum-to` | `tagbody`/`go` fixnum 循环 | 400 ms | 20 ms | 20.0x |
| `struct-loop` | 结构体槽循环读取 | 260 ms | 20 ms | 13.0x |
| `arith-chain` | 链式二元运算 | 300 ms | 40 ms | 7.5x |
| `call-loop` | 循环中函数调用 | 340 ms | 240 ms | 1.4x |
计算密集型代码获得最大收益。在现实世界的弹跳线图形演示中(源码仓库 examples/gfx/ 下)——该演示主要由对 `graphics.library` 的 FFI 调用主导——JIT 达到约 **615 FPS**,而字节码 VM 为 **500 FPS**。相比之下,编译后的 ACE BASIC 通过相同的 ROM 调用达到约 1900 FPS;剩余差距是动态、垃圾回收、标签值语言的结构性成本,而非代码生成。JIT 默认开启;`--no-jit` 可保留函数为纯字节码模式,如果你想比较或隔离某些东西。
### MorphOS:新一代 Amiga
MorphOS 构建版是一个完全原生的 PowerPC 二进制文件,使用 MorphOS SDK 的 GCC 编译——不是运行在模拟环境中的 68k 二进制文件(尽管 m68k 二进制文件也能工作)。线程、套接字以及整个 Amiga FFI/GUI/音频栈的工作方式与经典 AmigaOS 相同;Amiga 库调用通过 MorphOS 的 ABox 层从 PPC 代码分派到库基址。PPC 是 32 位且大端序,与 m68k 相同,因此在一个系统上编译的 FASL 文件可以在另一个系统上加载。MorphOS 构建版唯一省略的是 JIT,它仅限于 m68k——它像 macOS/Linux 构建版一样运行可移植的字节码虚拟机。但在 G4 或 G5 上,该虚拟机运行*非常快*。快到 MorphOS 成为完整的 **Quicklisp** 体验的特定目标:在那里安装客户端、下载发行版、并 quickload 带有完整依赖图的实际库是完全可行的,而在 14 MHz 的 m68020 上则不行。
Clamiga 在 MorphOS 上启动和运行
### 设置 Quicklisp
Quicklisp 是 Common Lisp 的事实上的库管理器,它能在 Clamiga 上运行。默认客户端还不知道这个实现,因此项目附带了一个小型兼容层以及一组维护的库分支,这些分支在 `#+cl-amiga` 特性分支后面提供了对 Clamiga 的一流支持——目标是一旦剩余的 API 差距缩小,就将它们上游化。安装是一次性的:
```lisp
(require "asdf")
(load "lib/quicklisp-install.lisp")
(cl-amiga-ql:install)
```
在任何后续会话中:
```lisp
(load #P"~/quicklisp/setup.lisp")
(load "lib/quicklisp-compat.lisp")
;; 加载 Alexandria 库
(ql:quickload "alexandria")
```
将此(样板加载代码)设置在 `~/.clamigarc` 初始化文件中,这样每次启动 Clamiga 时都会自动加载。
经通过 `quickload` 及其自身的 `asdf:test-system` 套件确认可用的库(选择)包括:
- [alexandria](https://github.com/kmx-io/alexandria) 事实上的标准工具集合
- [fiveam](https://github.com/lispci/fiveam) 我最喜欢的单元测试框架
- [FSet](https://github.com/mdbergmann/fset) 一个函数式(不可变)集合库(注意,Fset 需要 Clamiga 的一个特定实现版本,尚未上游化,仅在我的 fork 中)
- [str](https://vindarel.github.io/cl-str/) 字符串工具库
- [Drakma](https://github.com/edicl/drakma) HTTP/HTTPS 客户端
- [Hunchentoot](https://github.com/edicl/hunchentoot) Web 服务器(Drakma 和 Hunchentoot 必须在没有 SSL 的情况下启动,目前缺少与 Amiga SSL 的绑定)
### 从 Lisp 构建原生 Amiga GUI
Clamiga 提供了针对 Intuition、Graphics 和 GadTools 的 Lisp 绑定,通过 `require` 按需加载。这些绑定仍在进行中——它们从我的个人项目需求中发展而来,主要是弹跳线演示和我的《Lambda 传说》引擎(一个《吟游诗人传说》风格的地牢探索引擎),因此它们覆盖的是常见用例,而非完整的 API 表面。正在根据需要实现更多 AOS API(或特定的 MorphOS API)(欢迎拉取请求)。
打开一个窗口并在其中绘制看起来像这样:
```lisp
;;; hello-window.lisp
;;; REQUIREs 必须放在 DEFPACKAGE 之前:LOAD 一次读取并求值一个顶层形式,
;;; 因此在提及这些包的形式被读取时,包已经存在。
(require "amiga/intuition")
(require "amiga/graphics")
(defpackage :hello-amiga
(:use :cl)
;; 在此使用别名,以使包显式
(:local-nicknames (:it :amiga.intuition)
(:gfx :amiga.gfx)))
(in-package :hello-amiga)
(defun main ()
(it:with-window (win :title "Hello Amiga"
:width 320 :height 200
:idcmp it:+idcmp-closewindow+)
(let ((rp (it:window-rastport win)))
(gfx:set-a-pen rp 1)
(gfx:move-to rp 20 40)
(gfx:gfx-text rp "Hello from Clamiga!")
(it:event-loop win
(it:+idcmp-closewindow+
(msg) (return))))))
(main)
```
还有自定义屏幕、RTG 安全的屏幕外位图(带 blitter 合成)、GadTools 小工具和菜单、audio.device 播放——当抽象不够用时,还有通过手写的 68k 跳板对任何 AmigaOS 库的原始基于寄存器的调用。因此,即使缺少绑定,也没有什么无法触及。
AmigaOS 中的 Intuition 窗口。Clamiga 原生 GUI 示例
MorphOS 中的 Intuition 窗口,相同代码。Clamiga 原生 GUI 示例
### 检查器
Common Lisp 实现通常带有一个检查器,允许通过 REPL 检查运行时中的活动值。Clamiga 也有。下面的例子:
- 设置一个变量 `defvar` 并将其初始化为一个空的哈希表。
- 然后设置一个键值对 "foo"->"bar"。
- 然后我们可以通过 `(inspect *ht*)` 检查哈希表 `*ht*`。
- 这将显示哈希表的条目。
- 我们可以通过选择 `0` 条目进一步检查哈希表条目,该条目由 `car` 和 `cdr` 值组成,分别代表键和值。
- 每一个都可以进一步检查。
因此,基本上可以检查一个简单变量或整个树结构。
Clamiga 原生 GUI 示例
### 调试器、条件和重启
调试器也是 Common Lisp 实现的一个重要组成部分。引发一个类型为 `error` 的条件(除非被显式禁用)会打开调试器,除非它被 `handler-case`、`handler-bind`、`unwind-protect` 处理,或被 `ignore-errors` 形式忽略。
因此,从 REPL 中你可以直接:
```
COMMON-LISP-USER> (error "Hello error!")
Debugger entered: SIMPLE-ERROR: Hello error!
Backtrace:
0: (line 1)
Available restarts:
0: Return to top level
Debugger commands:
— invoke restart by number
:bt [n] — show backtrace (n frames, or "all")
:q — return to top level
:help — show this help
— evaluate a Lisp expression
Debug>
```
你将进入调试器。现在在这种情况下实际上没有太多可做的。回溯几乎是空的,没有可用的重启。让我们尝试一个更复杂的例子(来自文章 Common Lisp - Oldie but goldie (https://nnamgreb.de/blog/Common+Lisp+-+Oldie+but+goldie)):
我们首先定义几个条件(类似其他语言中的异常)。
```
COMMON-LISP-USER> (define-condition my-err1 () ())
MY-ERR1
COMMON-LISP-USER> (define-condition my-err2 () ())
MY-ERR2
COMMON-LISP-USER> (define-condition my-err3 () ())
MY-ERR3
COMMON-LISP-USER> (define-condition my-err4 () ())
MY-ERR4
```
然后我们定义一个函数 `lower`,它设置 3 个重启用例。
```
COMMON-LISP-USER> (defun lower (err-cond)
> (restart-case
> (error err-cond)
> (restart-case1 (&optional arg)
> (format t "restart-case1 arg:~a~%" arg))
> (restart-case2 (&optional arg)
> (format t "restart-case2 arg:~a~%" arg))
> (restart-case3 (&optional arg)
> (format t "restart-case3 arg:~a~%" arg))))
LOWER
```
现在让我们调用它:
```
COMMON-LISP-USER> (lower 'my-err1)
Condition: MY-ERR1
Available restarts:
0: RESTART-CASE1 - (restart-case1 (&optional arg))
1: RESTART-CASE2 - (restart-case2 (&optional arg))
2: RESTART-CASE3 - (restart-case3 (&optional arg))
3: Return to top level
Debug> 0
restart-case1 arg:NIL
NIL
```
我们选择了重启 0,它被调用了。注意参数默认是 NIL。我们可以传递一个参数:
```
COMMON-LISP-USER> (lower 'my-err2)
Condition: MY-ERR2
Available restarts:
0: RESTART-CASE1 - (restart-case1 (&optional arg))
1: RESTART-CASE2 - (restart-case2 (&optional arg))
2: RESTART-CASE3 - (restart-case3 (&optional arg))
3: Return to top level
Debug> 1
restart-case2 arg:
123
restart-case2 arg: 123
NIL
```
我们输入了 123 作为参数。这里展示了调试器的交互性和条件系统的强大——你可以在错误发生时决定如何处理,而不仅仅是让程序崩溃。
相似文章
Slisp: 简单的Lisp编译器 (Linux/amd64)
Slisp 是一个简单的编译器,它读取 Lisp 程序并生成适用于 Linux/AMD64 的独立汇编表示,支持基本原语、闭包和标准库。
@claugeAi: 我构建的项目 Clauge 是一个跨平台桌面应用(Rust + Tauri,约25MB,亚秒级冷启动),集成了7个开发工具……
Clauge 是一个开源跨平台桌面应用,使用 Rust 和 Tauri 构建,集成了多个开发者工具——包括并行编码代理、看板工作区和 API 客户端——全部具备 AI 能力。
cl-forth:用CL实现的Forth 2012标准
CL-Forth 是 Forth 2012 标准的 Common Lisp 实现,支持多种 Lisp 实现和操作系统,并提供独立可执行文件选项。
构建Clang后端并将Doom移植到我的自定义字节码虚拟机
作者复活了他的自定义字节码虚拟机(UVM),并借助AI解析文本LLVM IR构建了一个Clang后端,成功将Doom移植到该虚拟机上。
cl-bbs: 用Common Lisp重写的类schemeBBS文本公告板
cl-bbs 是一个用 Common Lisp 编写的高性能匿名文本公告板引擎,忠实复刻了原始 SchemeBBS 的风格。它提供格式化支持、图片预览以及零 JavaScript 渲染等功能。