打开网易新闻 查看精彩图片

QuickJS 这个引擎只有 200KB,却有人拿它搭出了完整运行时。不是玩具,是真的能跑 setTimeout、文件异步读写、多线程 I/O。

这事像什么?像有人用乐高基础款拼出了能遥控的车。零件全是现成的,但组合方式让人意外。

从引擎到运行时:差的不只是 API

引擎只管执行 JS。V8、JavaScriptCore 都是如此——它们不认识文件、网络、定时器。

运行时才是完整环境。Node.js、Bun 都在引擎外包裹了事件循环、任务队列、平台能力。这次作者选的包裹材料是 QuickJS,由 FFmpeg 作者 Fabrice Bellard 写的那个。

目标很明确:用 C 语言从零搭一个微型运行时,支持 console.logprocess.uptime()setTimeout、同步/异步文件读取,外加事件循环和 I/O 线程池。

QuickJS 官方其实带了个基础壳子 qjs.c,但作者没碰。完全重写,为了理解每一层怎么咬合。

启动:最小的可运行骨架

启动:最小的可运行骨架

嵌入 QuickJS 的最小代码长这样:创建运行时(JSRuntime)、创建上下文(JSContext)、读文件、执行、捕获异常。

核心就 20 行 C 代码。作者管这个可执行文件叫 andjs——显然是在向谁致敬。

打开网易新闻 查看精彩图片

运行效果已经能看:./andjs example-uncaught-throw.js 会抛出堆栈跟踪,和 Node 报错格式几乎一致。但此时还只能看错误,正常输出一片黑。

第一块拼图:console.log 怎么接进来

第一块拼图:console.log 怎么接进来

宿主函数(Host Function)是桥梁。C 函数注册到 JS 全局环境,JS 调用时实际执行的是 C 代码。

实现 console.log 需要处理变长参数、JS 值到 C 字符串的转换、循环引用检测。QuickJS 提供了 JS_ToCString 和迭代器 API,但字符串拼接和格式化得自己写。

作者这里埋了个细节:JS 字符串可能包含 null 字节,不能用普通 strlen,必须走 QuickJS 的长度感知接口。 这种坑,踩过的人才懂。

注册完成后,console.log("runtime booted") 终于能打印到终端。第一步通了。

定时器与事件循环:单线程的舞蹈

setTimeout 不是简单的睡眠。它需要:1)记录回调和延迟时间;2)不阻塞主线程;3)到期后精准唤醒执行。

作者用最小堆(min-heap)管理定时器队列,C 标准库没有,手搓了一个。堆顶永远是最近要到期的那个。

事件循环的核心逻辑是轮询:检查定时器堆 → 执行到期回调 → 检查是否有异步 I/O 待处理 → 计算下一次唤醒时间 → 休眠等待。没有任务时,poll() 或条件变量让出 CPU。

打开网易新闻 查看精彩图片

这里有个反直觉点:QuickJS 本身是单线程的,但 I/O 可以扔给线程池 作者实现了 4 线程的 worker pool,专门处理 fs.readFile 这类异步操作。主线程只负责调度和 JS 执行,避免阻塞。

文件系统:同步与异步的双面设计

文件系统:同步与异步的双面设计

fs.readFileSync 直接走 C 的 fopen/fread,在 JS 调用栈上同步完成。简单粗暴,但会阻塞事件循环——所以有了异步版本。

fs.readFile 返回 Promise。内部把任务塞进线程池队列,C 层做完 I/O 后,通过异步句柄(Async Handle)通知主线程,再由主线程把结果 resolve 给 JS。

Promise 的状态转换需要手动操作 QuickJS 的 Promise 对象:创建时挂起,线程池回调时调用 JS_Call 执行 resolve。这套流程在 V8 里封装得很厚,QuickJS 暴露得更 raw,也更透明。

作者展示了最终能跑的代码片段:启动后打印运行时间,100ms 后同步读 Makefile、异步读 Makefile,两个长度一起输出。同步和异步混写,行为符合预期——这对一个 200KB 引擎上的手写运行时来说,不算 trivial。

整个项目代码量?作者没说具体数字,但从描述看,核心 C 代码在数百行级别。QuickJS 本身编译后约 200KB,最终可执行文件控制在 1MB 以内。

对比 Node.js 的 100MB+,这不是一个量级的东西。但作者的目的也不是替代,而是理解:如果剥掉 libuv、剥掉 npm、剥掉生态,一个 JS 运行时的最小内核长什么样?

GitHub 上这个项目没有 star 数截图,但 Hacker News 讨论区有人留言:"这让我想起了 2009 年的 Node,那时候 Ryan Dahl 也在解释什么是事件循环。"

另一个评论更直接:"现在我知道 Bun 为什么能这么快了——他们大概也是这么从头数过每一行。"

你上次从零开始理解一个技术栈的底层,是什么时候?