Lume 是什么
先用一段人话说清它,再进源码。
Lume(读音 lu-mé,/luˈmeɪ/,两音节、重音在后)是一门面向 Agent 应用的 DSL:写一份 .lume 文件,编译进一个 C11 二进制,就同时拥有静态站点、JSON API、SSE 聊天和进程内 agent 工具。它不想再造一门通用语言,而是把「Web 服务器 + Agent 工具注册」这两层最容易写成长篇胶水代码的东西,收敛成语言级声明(示例全部来自 examples/hello.lume)。一句话说清目标:Lume 不是让 C 程序员写得更舒服,而是让 AI 能用极少的结构化代码,把 HTTP、数据、工具与 Agent 能力直接描述成语言。
server {
port = 8082;
workers = 2;
docroot = "./www";
}
get "/hello", (req) => {
return { message: "hello, world", path: req.path, method: req.method };
};
write "/items"; // 一条语句注册 POST/PUT/PATCH/DELETE 四个端点
tool "add", "Add two integers", { a: int, b: int }, (arg) => {
return { sum: arg.a + arg.b };
};
run();
三处设计值得先记住:
write "/items";不是框架 API,是语言级抽象。 一条语句注册四个写端点,默认回 JSON 确认({ action, method, got });要定制就传 handler。它声明的是「资源操作」而不是 HTTP 动词样板——你写的是「有/items这个可写资源」,四个动词由语言补齐,开发者不必再手排 PUT/POST/PATCH/DELETE 的胶水。- handler 返回 map、不写
body键,框架自动按200 application/json序列化——没有stringify(),没有手写 status/type。 tool的参数写裸类型关键字({ a: int, b: int }),注册时由 bridge 升级成 agent-httpd 要的 JSON schema——给模型看的协议由编译层生成,不是手写。
至于「为什么不是 Node / Python / Go」:不是性能——C 快是常识,本文不拿基准数字当卖点;是收敛。AI 大量写代码之后,HTTP 框架、JSON 序列化、部署脚本这些胶水恰恰是最容易写错、最难审的部分。Lume 的赌注是:把这些胶水沉进一门强类型 DSL,AI 要写对的面积小得多(--check 在编译期就拦掉大半错误),人要审的面积也小得多。
这份 .lume 经过的链路是:lexer → parser(AST) → typecheck → tree-walk 解释器(VM) → bridge.c → agent-httpd,最终落成一个静态链接的单二进制。下文解剖的,就是这条链路里最关键的两环。
待证假设
先把这个项目的边界说清楚:它是一个自包含的 agent DSL 服务器(原创代码,C11 单二进制),目标是让「写一份 .lume 脚本」就能同时拥有静态站点、JSON API、SSE 聊天和进程内 agent 工具,运行时零 Node、零独立 React 后端,进程自身不需要 nginx 作为应用服务器或静态资源服务器(它自带 HTTP 服务与 docroot)。不过有一个边界先说在前:lume 不自带 TLS,一旦要对外暴露又想上 HTTPS,生产环境仍得靠反向代理做 TLS 终结 + 鉴权(详见后文)。所以它不是「又一个 Web 框架」,而是「把 DSL 解释器直接焊进一个嵌入了 HTTP 能力的 C 二进制里」。
说明:本文解剖的是它正常运转时的设计与代码;下文只列源码能证明的结论——测试的「不崩」不会被解释成「证明没有泄漏」,文末有一张「源码对照」表收拢全部判据。
本文要验证的核心假设是:
bridge.c 把「DSL 世界」与「agent-httpd 世界」彻底解耦,让 Lume 只用写 DSL 语义;而 VM 在请求间复用、靠值栈即 GC 根 + 请求结束显式清栈来既快又不出错。
拆成三个可以对着代码反驳的子命题:
子命题一:桥接是单向翻译,不是混写。 DSL 的 route/tool/run 声明在 bridge.c 里被翻译成 agent-httpd 的 C 注册调用(agenthttpd_route / agenthttpd_tool / agenthttpd_run),解释器完全不碰 HTTP。判据在 src/bridge.c 的 route_shim / tool_shim 与注册函数。
子命题二:shim 契约是固定的。 路由/工具的 shim 返回 0 表示「已处理、框架负责序列化」;只有自流式 handler 才需要自己设 res->handled。违反任一条都会导致响应异常或静默失败。判据在 bridge.c 的 shim 实现与 ARCHITECTURE.md 的桥接层小节。
子命题三:VM 复用是安全的,不是「清不干净会泄漏」。 每个 worker 继承一份 VM 副本,请求结束后 vm_after_request 把 error / stack / call_result 全部清掉再交还复用;值栈本身就是 GC 根:清栈只是移除请求级的 GC root,真正的对象回收由 GC 在分配边界完成。判据在 bridge.c 与 value.c 的 gc_collect。
这三个命题都能在仓库里逐行核对。所以本文不堆砌基准数字,而是改用「设计意图 ↔ 代码实现」对齐的方式,把能坐实的讲清楚。
系统全景
Lume 是一个 C11 单二进制:src/ 里是一个可强类型化的脚本 DSL 解释器,静态链接兄弟项目 agent-httpd 的嵌入库 libagenthttpd.a(git submodule),再带上 frontend/ 用 esbuild 打出来的静态前端 www/,就成了一个自带 docroot 的完整 HTTP 服务器。一个进程 = 整站:静态资源 + DSL 路由 + JSON API + /react/api/chat SSE 聊天——进程自身就是 HTTP 服务器,不需要把 nginx / FastCGI 当应用服务器或静态服务器使,也不必单独部署一个 React 后端进程(不过公网 HTTPS 仍需反向代理,见后文边界)。
数据流的口诀(ARCHITECTURE.md 文末):
lexer → parser(AST) → typecheck(编译期强类型) → tree-walk interpreter(VM) → bridge → agenthttpd
其中解释器这一层(interp.c)是一个树遍历解释器,自带一个 VM:值栈(value stack)+ mark-sweep GC + jmp_buf 做函数返回的栈展开。它不生成字节码、不 JIT,就是老老实实遍历 AST、往值栈上压值。
进程与内存模型的关键决策有三件值得先记住:
- C11 单二进制:无运行时依赖、启动快、部署即一个文件;
--check可离线静态校验。 - DSL 而非 YAML/JSON 配置:路由/工具/服务器参数需要表达式、类型与复用能力,强类型在编译期拦截错误。
- VM 每进程一份,fork 前初始化:不可变 AST/Type 只读共享,VM 在 worker 间各自私有,请求间复用 VM,因此不需要跨 worker 的 VM 锁。
最后一条是后面两节的主角。
开发体验:把省心事焊进设计里
看完系统全景,先别急着扎进源码——这套设计对写业务的人意味着什么,才是值得先说清楚的。下面几条都是对照 src/ 与 DEVELOPMENT.md 能坐实的开发者体验,不夸大、也不回避代价。
1. 一份 .lume 顶一整套后端栈。 在脚本里写 route / tool / run / server{},编译进同一个 C 二进制后,你就同时拥有了静态站点 + JSON API + /react/api/chat SSE 聊天 + 进程内 agent 工具 + SSR 页面。不需要另起 Node 服务、不需要把 React 后端单独部署成一个进程——这个二进制自身就带了 HTTP 服务 + 静态文件服务,本地开发或跑 demo 时连 nginx 都不用装。
2. 编译期强类型 + 零副作用校验。 --check 会先 parse_program 再 type_check_program(后者永远执行,连直接执行路径也要过类型关),类型检查在第一条错误即停、停止后不执行。--check 在类型检查通过后立即退出、不产生任何副作用——所以「提交前跑一遍 make check」就能在运行之前拦掉大部分路由/工具的类型错误,而不是等到线上才崩。
3. --watch 热重载,而非「运行时改表」。 --watch 在每次编辑 <script.lume> 后重解析 + 重类型检查,再用 fork+exec 重启服务子进程(约 350ms,旧子进程优雅退出释放端口后新子进程才绑定),SIGUSR1 可强制走同一重启路径。之所以不能「运行时热补丁注册表」,机制见后文 VM 复用一节:注册窗口在 agenthttpd_run 之前,run 启动后注册表只读快照。lume 因此选择了「进程级重启」而不是「运行时改表」——代价是 watcher 每次编辑重解析的 AST/Type 树不释放,DEVELOPMENT.md 明说这是有意的 dev 工具泄漏(parse/typecheck 无 free 路径、且 Type 有共享引用,递归 free 会 double-free),长期挂机定期重启即可。这是诚实的取舍,不是 bug。
4. 离线 demo 引擎,开发聊天页不花钱。 LLM_API_KEY 为空时,框架走离线 canned 引擎:进程内生成 canned 回答、按 token 走 SSE 流式返回、不发起任何外部调用(DEVELOPMENT.md LLM 接线小节)。export LLM_API_KEY= 也能强行切回。tests/run_all.sh 开头就 export LLM_API_KEY=,保证 chat 测试永远走离线、不烧真 key。开发 /chat 页时,用 curl -N -X POST -d '{"message":"hi"}' localhost:8081/react/api/chat 就能跑通整条 SSE 链路,不必先接真模型。
5. 编辑器即插即用:VS Code 扩展市场搜「Lume DSL」即装。 插件(erishen.lume,源码就在仓内 editor/lume-vscode/)给 .lume 文件提供语法高亮(server / route / tool / func / let / return 等关键字、int / string 类型、内建函数、字符串转义,全部按 TextMate 语法着色)、// 与 /* */ 注释切换、括号配对与自动闭合。不想走市场也有本地两条路:把 editor/lume-vscode/ 软链进 VS Code 扩展目录(改语法文件后 Reload Window 即生效),或在仓根 make vsix 打出 lume-0.1.0.vsix 走 "Install from VSIX"。边界也要说清:这是纯 TextMate 语法包,没有 LSP——补全和诊断不来自编辑器;编译期诊断由 make check / --check 在终端给。编辑器管「看着舒服」,编译器管「写错没写错」。
6. 前端仍是标准 React 工程。 frontend/src 是 TypeScript + JSX + Tailwind,pnpm run dev 同时 watch 四个进程增量重打;tsc --noEmit --strict 在 build 前先跑,类型错误直接让 make ui 失败;esbuild 把 React 抽成共享 chunk-*.js(约 140kb),每页只剩 3–10kb 入口包,首访任一页后 React chunk 进浏览器缓存、跨页跨示例复用。换言之:服务端是 C 二进制、客户端是常规 React 工程,两套工具链互不绑架——改 DSL 不必碰前端构建,改 React 也不必动 C。
7. JSON API 近乎零样板。 路由返回一个不含 body 键的 map,框架自动按 200 application/json + 整张 map JSON 化返回(bridge.c 的 result_to_response);需要换 MIME 才写 {status?, type?, body},且 body 非字符串时自动 JSON 化,不用手写 stringify。静态页 URL 还能省掉 .html(GET /items → www/hello/items.html 回退,DEVELOPMENT.md JSON API 小节)——所以一个聊天页的地址可以就是 /chat,而不是 /chat.html。
这些便利是真实的,但边界也要说清:C 侧的内存安全靠 make asan(见后文)兜底;--watch 的泄漏是有意的;「运行时改业务」必须重启进程。它们属于「设计取舍换来的省心」,不是「魔法」。
桥接层 bridge.c
DSL 世界(Value / Node / VM)与 agent-httpd 世界(C 路由 / 工具)之间,靠 bridge.c 做翻译。它做三件事:路由注册、工具注册、内建函数种子。
路由注册:DSL 声明 → C 路由
DSL 里写 route "GET", "/p", func(req) { ... },bridge.c 在 agenthttpd_run 之前的注册窗口里,把它翻译成一次 agenthttpd_route(method, path, route_shim) 调用:
if (vm->route_count >= MAX_AL_ROUTES) return -1;
RouteRec *r = &vm->routes[vm->route_count++];
/* ... fill r->method / r->path / r->handler ... */
if (agenthttpd_route(method, path, route_shim) != 0) {
vm->route_count--; /* registration refused; roll back */
}
注意那个 route_count-- 回滚——agent-httpd 拒绝注册时(例如路由数超限),Lume 侧同步回退自己的记录,不会出现「C 侧没注册、Lume 侧却记了一笔」的漂移。
所有 DSL 路由共享同一个 C 回调 route_shim。它拿到 HTTP 请求后,先按 pattern 匹配当前 VM 里登记的路由,命中后用 interp 求值对应的 DSL handler(handler 返回 { status, type, body } 这类 map),再由 bridge 把 Value map 翻译成 agent-httpd 的响应结构。也就是说,解释器从头到尾只看到「一个请求 map 进、一个结果 map 出」,完全不知道自己在 HTTP 服务器里——这正是「单向翻译、互不混写」的子命题一。
write "/p"; 这种只声明路径、不写 handler 的写法,缺省落到 vm->default_handler(native_default_route),返回一个约定好的 JSON ack,不用每个端点都手写 handler。
工具注册:裸类型关键字 → JSON schema
工具注册是同一套思路,但多一道 schema 升级。tool "name", "desc", params, func 里的 params 在 DSL 里通常写成裸类型关键字,bridge.c 的 upgrade_tool_params 会把它升级成 agent-httpd 要的 JSON schema:
ToolRec *rec = &vm->tool_records[vm->tool_count];
/* ... fill name / desc / handler ... */
upgrade_tool_params(params_json, &schema); /* 裸类型关键字 -> {"a":{"type":"int"}} */
ctx->index = vm->tool_count;
if (!agenthttpd_tool_allowed(name)) { /* 白名单过滤 */ }
int rc = agenthttpd_tool(name, desc, rec->params, tool_shim, ctx);
vm->tool_count++;
这里有两处值得看:
- 白名单过滤(
agenthttpd_tool_allowed):Lume 侧的HARNESS_TOOLS_ALLOW环境变量在这里生效,没列进白名单的工具根本不会注册进 agent-httpd,自然也不会被 spawn 或调用。这是「运行时能力收敛」的第一道闸。 - 共享
tool_shim:和路由一样,所有 DSL 工具共用一个 C 回调,靠ctx->index回查到对应的 DSL handler 去执行。
shim 契约:返回 0 即「已处理」
这是 bridge 层最容易写错、也最该记牢的一条规则:
shim 返回
0= 已处理(框架负责序列化响应);不设res->handled(该标志跳过响应序列化,只用于自流式 handler)。
换句话说,绝大多数 handler 应该「算完返回一个 Value,让框架去序列化」,而不是自己往 res 里写字节。一旦误设了 res->handled,框架就会跳过序列化,客户端拿到空响应体——表现为「没报错,但拿不到东西」。这条契约是子命题二的核心,也是后面 VM 复用能成立的前提:shim 处理完一个请求,框架就调用 vm_after_request 清状态。
内建函数种子
bridge.c 在 fork 前还负责播一批内建函数种子:run/print/str/int/float/... 这些语言内建,以及 env/files/read_file/write_file/.../tools/skills/mcps 这类发现与 IO 类。后者会枚举 agent-httpd 的注册表与 .data/mcp-servers-router.json(write_file 原子写 + 0600,lock_file 走 flock 排他锁)。由于种子在 fork 前执行一次、所有 worker 继承同一份,这里不存在竞争。
VM 复用与 GC:请求之间怎么保持干净
这是全文最容易被「脑补出一个 bug」的地方。先给结论,完整源码细节放在文末附录,供要审 C 实现的读者对照。
进程模型:主进程解析完 .lume、注册完所有路由/工具,调用 agenthttpd_run(&cfg);agent-httpd 接管后默认 fork-per-connection——每个连接 fork 一个 worker,worker 继承一份 VM 副本(bridge.c 假设 VM 初始化发生在 fork 前)。AST/Type 只读共享、永不 free;VM 可写、worker 私有,所以请求间复用 VM 不需要任何锁。要强调的是:这不是通用高并发 Web Server 的性能结论,而是 agent-httpd 当前的进程隔离模型——Lume 选择 VM 在 fork 前初始化,换取 worker 间状态隔离和无需跨 worker 锁的 VM 访问。
注册窗口:agenthttpd_run 之前是唯一的注册时机,run 启动后注册表就是只读快照,再调 agenthttpd_route / agenthttpd_tool 是未定义行为。业务逻辑的任何变更都必须在启动前完成——这也是 --watch 选「fork+exec 重启子进程」而不是「运行时改表」的根本原因。
请求边界清栈:worker 处理完一个请求后,bridge 调 vm_after_request 把 VM 恢复到可复用状态:
static void vm_after_request(VM *vm) {
vm->error = false;
vm->error_msg[0] = '\0';
vm->stack_count = 0; /* 直接把值栈清空 */
vm->call_result = val_null(); /* call_result 重新初始化(留待每次函数调用自己设) */
}
清 error、清值栈(stack_count = 0)、重置 call_result,三件事。
值栈就是 GC 根:mark-sweep GC(对象挂在 VM 的 Obj 堆上)在分配新对象之前触发,mark 阶段从值栈、全局表、活跃环境链、已注册 handler 等根集出发标记存活对象。请求结束时 stack_count = 0 等于把临时值从根集摘掉——清栈负责移除请求级 GC root,GC 则在后续的分配边界执行真正的对象回收;两道闸分工明确,既快又稳。
改代码的铁律:架构文档规定值栈即 GC 根、不得让值「悬空」——分配一个要引用 V 的新对象前,V 必须一直留在栈上(或另有根),否则一次恰好插入的 GC 会把 V 回收掉,拿到野指针。完整推理见文末附录。
第二道闸:make asan 用 -fsanitize=address,undefined 单独构建 bin/lume-asan,越界与 UB 检测全开;LSan 泄漏检测按设计豁免(Type 生命周期 = 进程 + --watch fork+exec 重启会让临时分配被误报成泄漏),比肉眼审「有没有漏清字段」可靠得多。
还有一个常被忽略的点:lume 不自带 TLS。服务器只链接 libc、明确不链 OpenSSL,inbound 只有裸 HTTP——源码注释自己写着:server 只链接 libc,一条项目铁律(project rule: no OpenSSL)。监听是裸 TCP socket、全程没有证书处理。所以一旦要对外暴露又想上 HTTPS,README 自己就建议「放到带鉴权的反向代理后面」——生产环境通常还是得有一个 nginx / Caddy 做 TLS 终结 + 鉴权。换句话说,「不用 nginx」只在本地运行 / 内网 loopback 场景成立;上了公网 HTTPS,反向代理就是必选项,而不是可选项。
可复现:工具链与验证
要把上面的结论自己验证一遍,工具链是现成的:
| 命令 / 目标 | 用途 |
|---|---|
make check |
类型检查全部示例(demo/lang-basics/hello/hub/sqlite-write),确认 DSL 无编译期错误 |
make test |
构建 + 前端 + 单测(tests/smoke.c 解释器单测、tests/tools_driver.c 工具注册与 tools_dispatch JSON 往返)+ tests/run_all.sh 端到端 |
bin/lume --check <file> |
离线静态校验,零副作用 |
bin/lume --dump <file> |
打印 AST,检查树结构 |
make dev / make hub |
分别起 :8081 / :8083 两个 profile |
make asan |
ASan/UBSan 内存安全构建与回归 |
启动服务后可以直接用 curl 探测:GET /hello、GET /sum、POST /echo,以及聊天 POST /react/api/chat、静态页 /dsl、聚合页 /discovery。其中 make hub 起的 :8083 值得单独一看——它展示的就是 tsm-hub 的三类能力:skills、mcps、tools(5 个技能的 SKILL.md 语料、5 个 MCP、10 个网关内置工具),整本目录对客暴露,而承载它的只是 examples/hub.lume 这一个文件。
tests/run_all.sh(被 make test 调用)的 GC 压测部分是验证 VM 复用的关键用例:
# 4c. GC stress: many requests must not crash workers or corrupt state.
for _ in $(seq 1 150); do
curl -s -m 2 http://127.0.0.1:$PORT/sum > /dev/null
curl -s -m 2 -X POST -d '{"n":7}' http://127.0.0.1:$PORT/echo > /dev/null
done
kill -0 $SERVER_PID 2>/dev/null || fail "server died under load"
150 × 2 = 300 次请求,断言是 server died under load(服务器没被压垮)和 worker 进程仍在。它验证的是:在这组测试条件下,VM 跨请求复用没有出现明显崩溃或状态污染——是复用路径的回归闸门,而不是「复用正确」的证明。
源码对照速查表
把整篇收拢成一张「源码对照」清单:
| 结论 | 支撑 |
|---|---|
桥接是单向翻译:DSL 声明 → agenthttpd_route/agenthttpd_tool |
src/bridge.c:244-251(路由)、:388-423(工具) |
| 所有 DSL 路由/工具共用一个 C shim,靠 index 回查 handler | src/bridge.c:205(route_shim)、:344(tool_shim) |
shim 返回 0 = 已处理;res->handled 仅用于自流式 |
ARCHITECTURE.md §3.2 |
| 工具白名单在注册期过滤,未列不注册不 spawn | src/bridge.c:410(agenthttpd_tool_allowed) |
注册窗口在 agenthttpd_run 之前,之后只读快照 |
ARCHITECTURE.md §2.1 |
| VM 每进程一份、fork 继承副本且各 worker 私有,无需跨 worker 的 VM 锁 | ARCHITECTURE.md §1.1 / §2.1 |
vm_after_request 清 error / stack / call_result(含 stack_count = 0) |
src/bridge.c:25-31 |
| 值栈即 GC 根;GC 在分配前触发;阈值 2MB 起、翻倍至 1GB | src/value.c:21,33,80,103;src/interp.c:829 |
make asan 检越界/UB,按设计豁免 leak 检测 |
Makefile:296-306 |
| 300 请求压测存在,断言「不崩」而非「修泄漏」 | tests/run_all.sh:176-183 |
| 上面这张表只列能坐实的「设计意图 ↔ 代码实现」对照;对于无法用源码坐实定量结论的部分,本文不靠编数字凑篇幅。 |
为什么用 C11 单二进制,而不是 Node / Python + 框架?
这是设计取舍——把业务逻辑完全内聚在 .lume 脚本里,bridge.c 充当解释器与 libagenthttpd.a 之间的胶水层,统一承载静态站点、JSON API、SSE 聊天流和 SSR 页面。去掉多语言运行时后,部署体积和运维复杂度显著降低、启动快、单文件即可分发。代价是桥接层(尤其是 VM 生命周期与 GC 管理)的可靠性完全由 C 代码保证——所以项目另给了 make asan 这道内存安全闸。语言本身的「快」是常识,不当作卖点;能拿出手的是「设计目标 + 可验证的正确性」,而非未经测量的基准数字。
bridge.c 的 shim 契约是什么?违反了会怎样?
shim 是路由/工具处理器与 HTTP 框架之间的约定接口。正确做法是返回 0 表示「已处理」,由框架负责序列化响应;只有走自流式输出(自己往连接写字节)的 handler 才需要显式设置 res->handled。违反任一约定会导致响应异常或静默失败——例如返回非 0 时框架按「未处理」分支继续走,但不会抛错,仅表现为客户端拿不到预期响应体。所以绝大多数 handler 应该「算完返回一个 Value,让框架去序列化」。
VM 在请求间怎么复用?真的安全吗,会不会泄漏?
每个 worker(fork-per-connection)继承一份 VM 副本,请求结束后 vm_after_request 把 VM 恢复到可复用状态。它清三样:error、stack(stack_count = 0,直接把值栈清零)、call_result。值栈本身就是 GC 根:栈清零即移除请求级 GC root,真正的回收由 GC 在分配边界完成。真实代码和架构文档都写明它清的是 error/stack/call_result 三者。
工具注册什么时候有效?运行期还能改吗?
agenthttpd_run 启动之前是注册窗口。一旦 run 启动,框架对注册表做只读快照,此后任何调用 agenthttpd_route / agenthttpd_tool 的行为都是未定义的。内建函数的种子在 fork 前执行一次,所有 worker 继承相同种子,不存在竞争。白名单(HARNESS_TOOLS_ALLOW / HARNESS_SKILLS_ALLOW / MCP_ALLOW)在注册期过滤,未列的不注册、不 spawn。因此业务逻辑的变更必须在服务启动前完成——这也是 --watch 热重载选择「重启子进程」而非「运行时改表」的原因。
怎么验证 Lume 的内存安全,而不只是「看起来没崩」?
两层。第一层正确性:make test 跑 tests/run_all.sh,其中 300 请求 GC 压测断言服务器在压力下不崩、worker 不丢、状态不坏。第二层内存安全:make asan 用 AddressSanitizer + UndefinedBehaviorSanitizer 单独构建一份插桩二进制,检出 Lume 侧代码的堆/栈越界与 UB;它跑同一套 --check 全部示例 + 单测 + 工具派发,只是按设计豁免了 LSan 的 leak 检测(因为 Type 对象生命周期=进程、且 --watch 走 fork+exec 重启,临时检查类型会被误报)。越界/UB 检测全开,比肉眼看 vm_after_request 是否漏清字段可靠得多。
深入:VM 生命周期与 GC 的源码细节
这一节给要审 C 实现的读者;只关心「Lume 是什么」的可以跳过,不影响主线结论。
fork 之后每个 worker 一份 VM 副本。 同一条 agenthttpd_run 之后,会有很多 worker 进程,每个都拿着一份「只读 AST/Type + 一份可写 VM」的镜像。因为 VM 是 fork 继承的副本、各 worker 互不影响,VM 层面天然不需要跨 worker 的锁。
GC 的根集与阈值自适应。 gc_collect(src/value.c:77)的 mark 阶段从一组根开始标记存活对象,根集包括(value.c:80-89):
- 整个值栈:
for (int i = 0; i < vm->stack_count; i++) mark_value(vm->stack[i]); - 全局表
globals、活跃环境链active_envs、服务器配置server_config、默认 handler、所有已注册路由的 handler、所有已注册工具的 handler、以及call_result。
sweep 阶段回收未标记对象,bytes_allocated 回落,并把 gc_threshold 翻倍(value.c:103):
if (vm->gc_threshold < (1u << 30)) vm->gc_threshold *= 2;
阈值初值是 2 MB(src/interp.c:829 的 vm->gc_threshold = 2 * 1024 * 1024;),每触发一次 GC 翻倍,直到 1 GB 封顶。也就是说:对象少时几乎不 GC,分配压力上来后 GC 频率与阈值同步自适应,不会因为「每次分配都扫全堆」而拖慢。
「不悬空规则」的完整推理。 如果你要分配一个新对象 A,而 A 需要引用另一个值 V,那么在「A 分配」到「A 真正持有 V」之间,V 必须一直留在栈上(或另有根)。否则一次恰好插入的 GC 会把 V 当垃圾回收,A 就拿到一个野指针。这也反过来说明 vm_after_request 直接 stack_count = 0 的正确性:请求结束时栈上那些临时值已经没人引用了,把它们从根集里摘掉,下一次 GC 就能干净回收。
这是一次实验
最后把视角拉回来。这篇文章解剖的 bridge、VM、GC,服务的其实是一个更大的问题:如果以后主要由 AI 来写全栈代码,开发者的语言栈能不能收敛成 Lume + React? 服务端是一门强类型 DSL 焊进 C 二进制,客户端是标准 React 工程,两套工具链互不绑架(前文已讲);中间的胶水——HTTP 框架、序列化、部署脚本——被语言级声明吃掉。Lume 不打算替代任何通用语言;它赌的是另一件事:当写代码的主力变成 AI,「声明能力、编译期拦错、要审的面积小」比「生态大、写法自由」更要紧。make hub 把 tsm-hub 网关的能力目录对客暴露,已经是这个方向上的一次彩排。
项目地址
- GitHub:
https://github.com/erishen/lume(C11 单二进制 + agent-httpd submodule) - 文档:
ARCHITECTURE.md(系统构成与设计决策)、DEVELOPMENT.md(代码级开发约定与测试)、LUME.md(DSL 用户指南)随仓库提供。
发表回复