我给嵌入式设备写了个 6MB 的 JavaScript 运行时(xuan)
我一直很喜欢 Node.js------它能让我一个下午就撸完后端。
但我也写固件、跟边缘硬件打交道:OpenWrt 路由器、MIPS 板子、小型 ARM 网关。在 这类设备上,Node.js 往往太重了:100MB+ 的运行时、偏高的内存占用、还有能明显感觉 到的冷启动。
所以我做了 xuan(玄) ------一个面向物联网和嵌入式设备的轻量级 JavaScript 运行 时。它基于 QuickJS 构建,fork 自 tjs / txiki.js,采用 MIT 协议,仓库在这里: **github.com/topelinux/x...
这篇文章聊聊:它为什么存在、内部是怎么搭的、以及怎么上手。
为什么不能"直接用 Node"
在服务器上,Node 的开销都不叫事。但在只有 64MB 内存的小板子上,每一样都是问题:
- 体积:你不希望在一个 flash 分区很小的设备里塞一个 100MB 的运行时。
- 内存:驻留内存寸土寸金。
- 启动:如果程序是偶尔跑一次(类 cron 的采集任务、网关上的请求处理),冷启动 就是主要耗时。
我想要的是:在那种环境里跑起真正的 JavaScript,最好还能跑相当一部分 npm 生 态,而运行时的体积保持在个位数 MB。
xuan 是什么
xuan 内嵌了 QuickJS(Fabrice Bellard 写的、小巧又快的 ES2020+ 引擎),并补上 了实际应用需要的东西:
- Node.js 兼容的 API 层:很多现成的 npm 包几乎不用改就能跑
- 内置 IoT 模块:MQTT、串口、FFI
- 独立编译:把脚本编译成单个自包含的二进制文件
- 可交叉编译到 x86、ARM、MIPS、RISC-V
它是 txiki.js 的 fork------后者已经打好了 QuickJS + Node 兼容的底子。我聚焦的是物联 网/嵌入式这个方向,以及内置的设备模块。
内置模块,而不是现装 npm
在资源受限的设备上,运行时再去拉原生模块很痛苦。所以那些真正会用到的,直接内置:
- MQTT------发布/订阅,物联网的通用语言
- 串口------和 MCU、模组通信
- FFI------直接调用 C 库,复用已有代码
- GPIO / I2C / SPI------开发中
一个最小 MQTT 客户端就是很常规的 Node 写法:
javascript
const mqtt = require('mqtt');
const client = mqtt.connect('mqtt://broker.emqx.io');
client.on('message', (topic, msg) => {
console.log(topic, msg.toString());
});
client.on('connect', () => {
client.subscribe('iot/device/001/#', { qos: 1 });
setInterval(() => {
client.publish('iot/device/001/status', 'online', { qos: 1 });
}, 5000);
});
需要用 C 的时候,FFI 也能顶上:
javascript
import FFI from 'tjs:ffi';
const libc = new FFI.Lib(FFI.Lib.LIBC_NAME);
const atoi = new FFI.CFunction(libc.symbol('atoi'), FFI.types.sint, [FFI.types.string]);
console.log(atoi.call('123')); // 123
用 xuan compile 打包成独立二进制
还可以把应用嵌进运行时,只发一个文件:
bash
./build/xuan compile app.js myapp
./myapp # 不需要 Node.js,也不需要装运行时
这就是我想要的部署方式:在自己机器上编一次,把单个二进制拷到目标设备上就行。
一些数字
MacBook Air M1 上的数据(只测了可执行文件体积和冷启动,不是驻留内存测试):
| 运行时 | 冷启动均值 | 可执行文件大小 |
|---|---|---|
| xuan | 5.37 ms | 6.2 MB |
| bun | 8.82 ms | 58 MB |
| Node.js | 20.29 ms | 112 MB |
各 220 次、随机交错执行,-e "0",统计进程从启动到退出的 wall-clock 时间。当作 一个粗略参考就好,别当军备竞赛。
这些是在桌面 CPU 上测的;对嵌入式同学来说,真正有意思的是这个量级:个位数 MB 的运行时 + 毫秒级启动。
内部是怎么搭的
java
┌─────────────────────────────────────┐
│ xuan Runtime │
├─────────────────────────────────────┤
│ Node.js Compatible API Layer │
├─────────────────────────────────────┤
│ IoT Native Modules (MQTT/Serial/FFI)│
├─────────────────────────────────────┤
│ QuickJS Engine │
├─────────────────────────────────────┤
│ Embedded System (Linux/RTOS) │
└─────────────────────────────────────┘
JS 源码先用 esbuild 打包,再编译成 C,最后嵌进二进制。对依赖(libwebsockets、 QuickJS、libuv、mimalloc)的本地改动以 patch 形式存放在 patches/,构建时自动 应用,因此干净 clone 也能可复现地构建。
构建与运行
bash
git clone https://github.com/topelinux/xuan.git
cd xuan
git submodule update --init --recursive --force
npm install # 需要 esbuild 等,make 前必须先装
make # 产物在 ./build/xuan
./build/xuan run app.js
现状与下一步
目前可用的:Node 兼容层、MQTT、串口、FFI、xuan compile,以及交叉编译。下一步: GPIO、I2C、SPI,以及继续补 Node API 覆盖。
项目还很早期,非常希望听到反馈------尤其是常年在资源受限硬件上折腾的同学:你更关心 哪些板子、哪些 Node API、哪些设备模块?
- 仓库:github.com/topelinux/x...
- 协议:MIT
致谢:地基是 Saúl Ibarra Corretgé 的 txiki.js,引擎是 Fabrice Bellard 的 QuickJS。