把 Commodore 64 BASIC 塞进 PostgreSQL:PL/CBMBASIC 的荒诞与认真

2026-07-03 32 预计阅读时间: 1 分钟
来源: postgr.es AI 摘要 Original link

Disclaimer: This article is an AI-assisted summary. Read it together with the original source when precision matters. The summary may omit context, version differences, or edge cases and is not official documentation.

预计阅读时间:8 分钟

如果你对 38911 BASIC BYTES FREE 有条件反射,PL/CBMBASIC 这件事大概会让你笑出声:PostgreSQL 现在可以把函数体交给 Commodore 64 BASIC V2 执行。不是仿制品,也不是语法糖,而是基于 cbmbasic 项目把 1982 年的 Microsoft/Commodore BASIC 解释器静态重编译成 C,再编进 PostgreSQL 扩展共享库。

每次函数调用都像一次内存里的开机:清零 64KB RAM、重置 CPU 寄存器、从 ROM 入口重新开始。听起来离谱,但代价大约只有 15 到 20 微秒,足够低到可以在表的每一行上调用。

这不是玩具外壳,而是真 BASIC

PL/CBMBASIC 的核心趣味在于:它尽量让数据库函数真的活在 C64 BASIC 的限制里。

你写函数时仍然需要行号,而且用户代码从第 10 行开始。第 0 到第 9 行由扩展保留,用来把 PostgreSQL 参数注入成 BASIC 变量赋值。

CREATE EXTENSION plcbmbasic;

CREATE FUNCTION hello(who text)
RETURNS text AS $$
10 PRINT "HELLO, ";WHO$;"!"
$$ LANGUAGE plcbmbasic;

SELECT hello('WORLD');
-- HELLO, WORLD!

参数会按 BASIC 的世界观映射:

  • text 参数 who 变成 WHO$
  • smallint 可以变成 16 位整数变量,例如 LIVES%
  • 其他数字大多落到 CBM 的 40 位浮点数里,约 9 位有效数字

这也意味着,字符串字面量、变量名和输出都会进入那个大写、短变量名、255 字符字符串上限的年代。

连 BASIC 的坏脾气也被带进来了

C64 BASIC V2 有一些非常“复古”的限制:关键字会在标识符内部被 tokeniser 识别,变量名只有前两个字符显著,TIST 是系统变量。

所以类似 TOTAL 这种名字会踩中 TOSCORE 会包含 ORBUDGET 会包含 GET。PL/CBMBASIC 的 validator 会在 CREATE FUNCTION 阶段提前报错,而不是让你运行时才陷进 1980 年代的调试噩梦。

可以这样理解:PostgreSQL 得到了一个带怀旧提示语的 BASIC 语法审查员。

OUT 参数:从 64KB 内存里捞结果

BASIC 程序结束后,变量还留在模拟的 64KB RAM 里。PL/CBMBASIC 对 OUTINOUT 参数的处理方式很直接:沿着 BASIC 自己的变量表走一遍,解码变量名、类型和值,再转换回 SQL 类型。

下面这个例子可以改造成任何小型数值计算函数:

CREATE FUNCTION divmod_cbm(
  num int,
  den int,
  OUT quot int,
  OUT rmd int
) AS $$
10 QUOT=INT(NUM/DEN)
20 RMD=NUM-QUOT*DEN
$$ LANGUAGE plcbmbasic;

SELECT * FROM divmod_cbm(47, 5);
-- quot | rmd
-- -----+-----
--    9 |   2

这不是现代扩展 API 的常规姿势,更像是“程序跑完后,从另一台机器的内存里 PEEK 一下结果”。但放在 C64 BASIC 这个语境里,它反而非常合理。

设备 8 变成了数据库

最有意思的设计是 I/O。C64 上磁盘驱动器通常是 device 8,程序通过 OPENINPUT#GET#PRINT#CLOSEST 状态变量跟它交互。

在 PL/CBMBASIC 里,device 8 就是 PostgreSQL 数据库。你 OPEN 的“文件名”是一条 SQL,通过 SPI 在当前事务里执行。

可以这样实践一个排行榜读取函数:

CREATE TABLE hiscores (
  name text NOT NULL,
  score int NOT NULL
);

INSERT INTO hiscores(name, score) VALUES
  ('ADA', 4200),
  ('GRACE', 3800),
  ('LINUS', 1200);

CREATE FUNCTION top_scores_cbm()
RETURNS text AS $$
10 OPEN 1,8,0,"SELECT NAME, SCORE FROM HISCORES ORDER BY SCORE DESC"
20 INPUT#1,N$,S
30 IF ST<>0 AND N$="" THEN 60
40 PRINT N$;" ";S
50 IF ST=0 THEN 20
60 CLOSE 1
$$ LANGUAGE plcbmbasic;

SELECT top_scores_cbm();

查询结果会像磁盘记录一样一条条流回来,记录以 CR 结尾,ST 在 EOF 时带上状态位。那种“读到没有为止”的循环,居然能原样搬进数据库函数。

更复古的是 secondary address 15:它原本是磁盘命令通道。在这里,你可以往它 PRINT# SQL 命令:

CREATE FUNCTION purge_low_scores_cbm()
RETURNS void AS $$
10 OPEN 15,8,15
20 PRINT#15,"DELETE FROM HISCORES WHERE SCORE < 1000"
30 INPUT#15,EN,EM$,RC,ES
40 CLOSE 15
$$ LANGUAGE plcbmbasic;

SELECT purge_low_scores_cbm();

状态记录会模拟原来的风格,例如 0,OK,<rows>,0。这很酷,也很危险:这是 untrusted procedural language,只适合超级用户安装和使用。

性能不是笑话,但边界很清楚

PL/Python 这类保持解释器热启动的语言当然更快。来源测试里,PL/Python 在轻量调用上大约快 14 到 19 倍;如果函数内部也要查询 100 行数据,差距缩小到约 6 倍,因为两边都要付 SPI 的成本。

但换个角度看,一个 1982 年的 8 位 BASIC 解释器,每次调用还完整“开机”,仍然能把调用成本压到十几微秒,并执行约每秒百万条 BASIC 语句,这已经很不可思议。

真正需要注意的是语义边界:

  • 没有 NULL,通常需要在 SQL 层用 COALESCE 处理
  • 字符串最长 255 字符
  • 浮点只有约 9 位有效数字
  • 输入设备不存在,INPUT 会报错
  • POKESYS 作用于模拟 64KB 内存,下一次调用会重新清零,但当前调用仍可能被你自己玩坏
  • 无限循环会被 PostgreSQL 的 statement_timeout 打断,因为 RUN/STOP 检查被接到了 CHECK_FOR_INTERRUPTS()

采用建议:把它当成工程标本,而不是生产默认项

PL/CBMBASIC 的价值不在于“让 PostgreSQL 多一种严肃业务语言”,而在于它展示了 PostgreSQL 扩展机制、SPI、过程语言 handler、错误处理和运行时隔离可以被玩到多远。

如果你想试,可以从三个方向入手:

  • 在本地 PostgreSQL 环境里安装扩展,跑简单纯计算函数
  • 用 device 8 读取小表,理解 SPI 查询如何被包装成 BASIC 文件 I/O
  • 故意写一个坏变量名或除零错误,观察 validator 和 ROM 错误如何进入 PostgreSQL 报错

别把它放进核心生产路径,除非你非常清楚 superuser、untrusted language 和 1982 年 BASIC 限制带来的后果。但作为一个能运行 SQL、能读变量表、能处理中断的怀旧工程实验,它漂亮得过分。

READY.


相关推荐