Clamiga: 用于 Amiga 的 Common Lisp

Hacker News Top 工具

摘要

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)

Hacker News Top

Slisp 是一个简单的编译器,它读取 Lisp 程序并生成适用于 Linux/AMD64 的独立汇编表示,支持基本原语、闭包和标准库。