我给嵌入式设备写了个 6MB 的 JavaScript 运行时(xuan)

我给嵌入式设备写了个 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、哪些设备模块?

致谢:地基是 Saúl Ibarra Corretgé 的 txiki.js,引擎是 Fabrice Bellard 的 QuickJS。

相关推荐
只睡四小时2 小时前
AI 生成 PPTX:11 页课件编译出 429 个形状
javascript·python·pptx·ai生成ppt·ooxml
shmily麻瓜小菜鸡3 小时前
Axios 中 params 与 data 的区别与原理
javascript·typescript
isha3 小时前
Univer拆解——从类与方法的视角分析
前端·javascript
变与不变8063 小时前
GET请求知识详解
前端·javascript
哎哟喂呢哈5 小时前
VedioJS 10
javascript·html5
夏幻灵13 小时前
JavaScript this 面试:五种绑定规则、优先级与常见面试陷阱
开发语言·javascript·面试
三天不学习14 小时前
Egg.js 4 突然爆火,原因是否归结于AI 原生落地需求爆发?
前端·javascript·全栈·egg.js
xcs1940515 小时前
前端 vue 的前端页面debugger 进不去
前端·javascript·vue.js
小宋102120 小时前
A2UI从零实战:Agent安全生成交互界面与事件回传
javascript·人工智能·安全·交互