farlow.dev

将 OCaml 编译到 TI-84+ CE 计算器上的实现

2025年5月17日

在这篇文章中,我将解释我是如何编译一个 OCaml 程序,使其能够在 TI-84+ CE 计算器上运行的。🐪 一个在 TI-84+ CE 计算器上旋转的 3D 立方体

背景

OCaml 是一种有点小众的函数式语言,我在过去几年里很喜欢学习它。 自高中以来,我也是一个计算器爱好者,从事过像 PineappleCAS 这样的项目。 当时,计算器工具链只支持 C 和 (e)z80 汇编。 现在,有才华的人们已经建立了一个利用 LLVM 的工具链,因此你可以使用 C、C++、Rust、Zig 等语言进行编程。 值得注意的是,OCaml 不在此列表之列。 让我们修复它!

编译 OCaml

有适用于 x86、ARM 和 PowerPC 的 原生 OCaml 编译器。 还有一个字节码编译器,但它基本上没有文档记录(参见 interp.c)。 所有这些后端都需要一个运行时,其中包括垃圾回收器和原生函数,以便执行诸如打印到 stdout 和读取文件等有用的操作。 不幸的是,由于 OCaml 编译器缺乏死代码消除,这些工件往往很大。

我非常喜欢将整个 OCaml 程序编译成单个高度可移植的 ANSI C 文件的想法,我们可以使用计算器工具链(或任何 C 工具链!)来编译它。

这个想法并不新鲜:ocamlcc 将 OCaml 字节码编译为 C。 OMicroB 通过优化 OCaml 字节码并使用轻量级虚拟机,将 OCaml 编译到小型设备。

对于我的方法,我想要一些东西:

  1. 高度可移植
  2. 与 OCaml 构建系统良好交互
  3. 小巧 – 生成的代码和运行时应适合 TI-84+ CE 上的 256k RAM

请注意,此列表中缺少“高效”和“实用” :)

引入 Js_of_ocaml。 Js_of_ocaml 为几乎所有用 OCaml 编写的 OCaml 网站前端提供支持。 (是的,这是真的!)。 首先,将 OCaml 编译为字节码,然后 Js_of_ocaml 优化该字节码并将该字节码提升为 JavaScript。

总体思路如下:我们将为 Js_of_ocaml 编写一个新的后端,以发出 C 而不是 JavaScript。 然后,我们只需要添加几个原生函数和一个垃圾回收器。 这种方法的优点在于 Js_of_ocaml 得到了很好的维护,因此我们可以期望它能够跟上 OCaml 语言和字节码的变化。 Js_of_ocaml 还具有强大的死代码消除功能,并且得到 OCaml 的构建系统 dune 的一流支持。

新的 C 后端

因为大多数 JavaScript 构造可以很容易地映射到 C,所以我们基本上可以只使用真实的 Js_of_ocaml 后端作为参考,并修改 我们的 C 后端。 我们可以采用一些捷径,例如使用 goto 语句而不是重新实现复杂的逻辑来将事物映射到 if / else 块中。 唯一的难题是我们必须编写一个垃圾回收器。

垃圾回收

为了编写垃圾回收器,我们需要知道在垃圾回收时哪些对象是存活的。 原生编译器可以通过扫描堆栈和 CPU 寄存器来查找指向 OCaml 堆对象的指针来做到这一点。 我们不能这样做,因为我们希望我们的 C 文件尽可能可移植。 相反,我们将所有局部变量替换为对全局堆栈的显式读取和写入。 这样,我们就可以扫描全局堆栈,以便在进行 GC 时找到我们所有的活动对象。 这是一个我所指的概念示例:

value global_stack[1024];
value *bp = global_stack;
value *sp = global_stack;
void transmogrify(value string) {
 bp = sp;
 sp += 2; // 为 2 个局部变量分配空间
 bp[0] = caml_copy_string(string);
 bp[1] = caml_reverse_string(string);
 // 对这些局部变量执行一些操作
 sp = bp; // 释放这两个局部变量
}
void gc() {
 // 从 0 到 sp 扫描 global_stack
 // 并将这些值标记为“alive”
}

只要我们想要分配一个新的 OCaml 对象,我们就会考虑进行垃圾回收。 如果满足以下任一条件,我们将进行垃圾回收:

  1. 分配将使我们超过最大内存限制
  2. 分配将使我们超过上次回收后内存的 2 倍

当我们进行垃圾回收时,我们将进行简单的标记和清除。 首先,我们将扫描全局堆栈。 当我们遇到 OCaml 指针时,我们将遵循该指针以及该对象递归拥有的每个指针,将它们全部标记为存活。 完成后,我们将释放每个未标记的其他对象。

运行时

为了使 OCaml 代码能够执行有用的操作,我们可以使用 OCaml 中的 extern 关键字来调用我们编写的任何 C 函数。 然后,我们可以编写 C 函数来支持最小的 标准库 和一个小的 TI-84+ CE 库,该库可以执行诸如绘制到屏幕之类的操作。

构建系统

因为 Js_of_ocaml 在 OCaml 的构建系统中得到一流的支持,所以我们的 C_of_ocaml 也是如此。 我们可以符合人体工程学地为计算器编写一个 旋转立方体 OCaml 程序,并且 LSP 支持和 dune build 命令开箱即用。

结论

请随意查看 此处 针对简单的 OCaml fibonacci 程序生成的 C 代码。 你可以使用 gcc 编译它! C 文件的前半部分是运行时,后半部分是生成的代码。

许多 OCaml 功能(浮点数、异常等)不受支持。 实际上,旋转立方体演示使用定点数(带有 24 位寄存器!)。 也许将来我会尝试将这个项目与 wee 结合使用,以使 OCaml 在任何地方运行。 我希望你觉得这篇文章有趣! 感谢阅读。 所有源代码都可以在 此处 找到。