拓冰建站拓冰建站
首页 / 资讯中心 / 正文

Choq:用 Cherry 与 QuickJS 构建轻量级 ClojureScript 运行环境

这次我们来看一个刚出现在 Hacker News Show HN 区域的新项目Choq。项目标题写得很简洁——Choq: Cherry (ClojureScript) on QuickJS。翻译过来就是把 Cherry 这个 ClojureScript 方言编译器接到 QuickJS 这个轻量 JavaScript 引擎上跑。问题不在于“能不能写 Lisp 风格代码”而在于当 Node.js 和 JVM 都显得太笨重的时候你还能不能继续用 Clojure 语法写业务逻辑。Choq 想给出的答案是可以而且整套链路可以做得非常轻。先给结论。从项目定位和公开技术背景来看Choq 的核心价值有四点第一运行环境不依赖 JVM也不依赖完整 Node.js 运行时第二Cherry 编译产物是标准 JavaScript体积和静态分析可控第三QuickJS 以低内存占用和快速启动著称适合嵌入到宿主程序里第四如果你在做边缘设备、插件系统、游戏脚本、无服务函数这类场景这套组合有实际工程价值。本文会围绕五个方面展开先拆解 Choq/Cherry/QuickJS 三者各自承担的角色然后给出核心能力速览接着按通用工程路径准备环境、部署启动再走一遍编译和运行的功能验证最后聊宿主嵌入、资源占用、常见问题和最佳实践。需要提前说明的是Choq 仍属于新项目公开资料有限下面凡是涉及具体命令、接口形态和性能参数的部分我都会给出通用验证思路并以你实际拿到的 README 和源码为准不替你编造体验。1. Choq 是什么编译器与运行时的组合思路1.1 三个组件各解决什么问题Cherry 是一个把类 Clojure 语法编译成 JavaScript 的编译器。它和完整版 ClojureScript 最大的差异是Cherry 的编译器本身更轻更强调输出“可读、可静态分析、可 tree-shaking 的 JavaScript”而不是一个全量运行时。你写的是 Clojure 风格的表达式但它最终产出的是可以在浏览器、Node.js 或任何 JS 引擎里跑的普通 JS 代码。QuickJS 是一个可嵌入的轻量 JavaScript 引擎由 Fabrice Bellard 开发。这是一个在嵌入式场景和资源受限环境里非常常见的引擎特点是启动快、内存占用低、支持 ES2020 大部分特性并且提供了简洁的 C API。很多游戏引擎、Edge 网关和插件系统都用它来做脚本层。Choq 出现在这两个项目之间作用是把 Cherry 的编译产物和 QuickJS 运行环境打通。从标题推断Choq 解决的不是“Cherry 怎么写代码”的问题也不是“QuickJS 怎么嵌入宿主”的问题而是“编译完成后怎么让代码在 QuickJS 里跑得顺、接得上”的问题。1.2 这个组合真正解决什么痛点如果你只在浏览器和 Node.js 里开发可能感受不到 Choq 的必要性。但一旦进入嵌入式、边缘计算、桌面应用插件这类环境情况就变了Node.js 运行时太大部署到小设备上很吃力。JVM 的内存占用和启动时间在嵌入式场景基本不可接受。直接用 JavaScript 写复杂业务逻辑维护成本高缺类型约束和结构化表达。用 C/C 写宿主脚本引擎开发效率低又要重新造一套语言。Choq 的思路是用 Cherry 负责“人性化”的部分用 QuickJS 负责“轻量化”的部分。宿主程序只需要内嵌一个 QuickJS就能执行由 Cherry 编译出来的 JS。开发者保留了 Clojure 方言的语法习惯宿主程序也拿到了可控的脚本入口。1.3 从项目性质看它更适合什么形态落地从目前的信息看Choq 更可能以“运行时集成项目”或“宿主嵌入工具”的形态出现而不是一个面向普通用户的 Web 服务。也就是说你大概率不会直接下载一个 Choq 的 App 来用而是把它作为依赖集成到自己的项目里或者用它提供的命令行工具完成编译和运行。这意味着如果你是一名 C、C、Rust 或 Go 开发者并且正在为你的应用程序寻找脚本方案Choq 这类项目值得关注。它解决的是“给宿主程序加一个类 Lisp 脚本层”的问题。2. 核心能力速览下表根据项目标题、公开技术常识和同类型项目的一般工程路径整理实际参数请以 Choq 官方仓库为准。能力项说明项目类型编译器 可嵌入运行时集成项目核心定位在 QuickJS 中运行 CherryClojureScript编译产物前端语言Clojure 方言Cherry 语法编译产物标准 JavaScript运行引擎QuickJS支持 ES2020 的轻量 JS 引擎是否依赖 JVM否是否依赖完整 Node.js 运行时编译环节可能需要运行环节不需要是否适合浏览器编译产物本身支持浏览器和任何 JS 环境是否提供 REST/HTTP API公开信息未明确更可能是宿主嵌入 API而非 Web 服务是否支持批量任务无内置批量队列可通过脚本循环或宿主调度实现GPU/显存要求无属于 CPU 与内存场景一键启动整合包未见相关材料大概率需要手动装配目标用户嵌入式/边缘设备开发者、宿主程序脚本插件、轻量 Lisp 爱好者从这张表能看出Choq 的重点不在算法和模型而在工程集成。它的门槛不在显存、显卡这些 AI 项目常见的硬件要求而在于你是否愿意搭建一条“Clojure 方言 - JS - QuickJS”的工具链。3. 技术背景Cherry 与 QuickJS 为什么是天生一对3.1 Cherry面向体积控制的 ClojureScript 方言Cherry 属于 Clojure 生态里比较新的思路默认你写的是 Clojure 风格的代码但编译目标始终是 JavaScript。它和 ClojureScript 的不同点在于Cherry 更激进地投向 JS 平台利用原生 JS 的数据结构和运行机制减少运行时依赖。举个例子ClojureScript 针对不可变数据结构和持久化集合会有自己的运行时实现但 Cherry 在大量场景下会直接使用 JS 的数组和对象只有在需要时才生成辅助函数。这种取舍的结果就是编译产物体积更小转译后的代码更容易被 JavaScipt 引擎优化。对于 QuickJS 这种内存敏感的运行环境Cherry 的“小而紧”输出有明显优势。如果你写惯了 Clojure 的defn、let、map、reduce那么 Cherry 的上手成本很低。如果你从来没接触过 Lisp第一次打开 Cherry 源码也不会太懵因为它的形式非常统一括号内第一个位置是函数名后面是参数。3.2 QuickJS嵌入式场景的脚本引擎标配QuickJS 在很多底层项目里已经是“标准答案”。它的单个可执行文件很小用 C 语言实现支持模块化、BigInt、async/await等现代 JS 特性。更重要的是QuickJS 提供了稳定且简洁的 C API宿主程序可以很方便地创建多个运行时上下文给不同的插件分配独立的沙箱环境。从内存角度看QuickJS 一个空上下文的开销远小于 V8启动也是毫秒级。这并不意味着 QuickJS 性能全面超过 V8但它在“低内存、快速启动、可控运行时”这个细分方向上是很有竞争力的。3.3 组合后的工程价值Cherry 加 QuickJS 的组合等于把“语言表达力”和“运行环境轻量”两个优势叠加起来。宿主程序用 Cherry 写业务脚本编译成 JS 后在 QuickJS 里跑。整个链路里运行时只需要一个很小的 JS 引擎不需要 JVM不需要 Node 的一堆内置模块不需要为脚本单独设计一套解释器。这对工具类软件尤其有价值。比如一个命令行工具可以内嵌 QuickJS 让用户通过 Cherry 脚本定制数据处理流程一个游戏客户端可以用 QuickJS 跑策划配置和玩法逻辑一个边缘网关可以在资源受限的容器里跑 JavaScript 形式的规则引擎。Choq 就是这条链路的“组装者”。4. 适用场景与使用边界4.1 适合谁第一类嵌入式开发者和边缘计算工程师。设备内存有限、CPU 一般但需要一点脚本扩展能力QuickJS 是成熟方案Cherry 提供更简洁的语法。第二类桌面端和工具链开发者。你的 C、Rust、Go 程序需要允许用户写插件或脚本但不想把 V8 塞进来也不想自己写语言解析器。内嵌 QuickJS再让用户用 Cherry 写脚本是一个合理方案。第三类Clojure 生态的玩家。想在浏览器、Node、QuickJS 等多个环境里统一用 Clojure 方言表达逻辑Cherry 本来就是为了跨平台而设计的Choq 再补上嵌入式场景这一环。第四类对运行时体积敏感、愿意做编译链优化的工程团队。Cherry 的输出可以被 tree-shaking配合 QuickJS 可以做极简部署包。4.2 不适合谁如果项目需要复杂的 Clojure 生态库或者需要完全对等的 ClojureScript 运行行为Cherry 就不太适合。Cherry 的野心是“够用且精简”不是“全量兼容”。如果脚本层需要大量 Node.js 内置模块比如fs、http、child_processQuickJS 默认不带这些。你需要在宿主层自己注入能力或者选择 Node.js 嵌入方案。如果你的诉求是“零学习成本所有开发都用 JS”那直接写 JS 可能更实际。引用一个 Cherry/Clojure 层会增加编译步骤和心智负担。4.3 合规与安全边界把 Choq 这类脚本引擎集成到产品里时有几条底线必须明确用宿主程序执行外部脚本必须考虑沙箱隔离不能让脚本随意访问宿主内存和系统命令。如果脚本能力被用于用户设备上的自动任务必须获得用户授权保证业务合规。如果代码里涉及第三方素材、模型、接口密钥必须遵循对应授权协议。不要把脚本引擎用在绕过平台限制、窃取数据、破坏系统的场景里。5. 环境准备与前置条件环境准备没有统一答案因为 Choq 的最终形态取决于它选择 React 还是 Rust 绑定方向。这里给出通用检查清单适合在拿到仓库后快速判断自己缺什么。5.1 操作系统Linux、macOS、Windows 理论上都可以。QuickJS 本身是 C 实现的Cherry 编译器跑在 Node.js 上所以只要三端有对应工具链就能工作。嵌入式设备上的交叉编译需要额外处理不在本文范围。5.2 工具链清单组件用途说明Git拉取源码必需Node.js 18 及以上运行 Cherry 编译器需要 npm 或 npxC 编译器编译 QuickJS 或 Choq 宿主代码Linux/macOS 自带Windows 需要 MinGW 或 MSVCQuickJS 可执行文件运行编译后的 JS可直接下载 release 或自行编译Rust/C/Go 工具链可选根据 Choq 实现语言决定取源码时看 README5.3 磁盘与内存磁盘空间方面预留 500MB 以上比较稳妥包含 Node 依赖和 QuickJS 构建临时文件。内存方面没有硬性要求普通开发机完全够用。5.4 端口与网络Choq 不是默认 Web 服务端口概念不适用。但如果你在网络受限的内网环境安装依赖需要提前准备好 npm 镜像或离线依赖包。6. 安装部署与启动方式6.1 获取 Cherry 编译器Cherry 的编译器以 npm 包形式分发。包名可能带作用域也可能不带你需要以官方 README 为准。通用安装方式是# 创建测试目录 mkdir choq-demo cd choq-demo # 初始化 package.json npm init -y # 安装 Cherry 编译器实际包名请按 README 调整 npm install cherry --save-dev安装完成后执行这个命令确认 CLI 可用npx cherry --help如果你看到类似Usage: cherry [options] [input]的输出说明编译器装好了。6.2 获取 QuickJS方法一直接用系统包管理器# macOS brew install quickjs # Debian/Ubuntu 下尝试安装 libquickjs-dev sudo apt install libquickjs-dev方法二官方源码编译git clone https://github.com/bellard/quickjs cd quickjs make sudo make install编译完成后验证qjs --help如果qjs命令能输出帮助信息说明 QuickJS 可以用了。6.3 编译并运行第一个 Cherry 程序先创建.cljs源文件mkdir -p src touch src/main.cljs写入下面这段测试代码(ns demo.main) (defn square [x] (* x x)) (println square 6 (square 6))然后编译# 通用编译命令骨架参数按 Cherry CLI 说明调整 npx cherry src/main.cljs -o output/main.js最后用 QuickJS 执行qjs output/main.js预期看到square 6 36这一步是整个链路最核心的验证。只要跑通说明“Cherry 编译 - QuickJS 运行”的路径已经通了后面所有业务逻辑都是在这个框架里扩展。6.4 关于一键启动从公开信息看Choq 大概率不提供一键启动整合包。这不是缺点因为它面向的是开发者集成场景。如果后续作者提供了 Docker 镜像或 release 二进制使用方式会简单很多但目前不需要假设。7. 功能测试与效果验证下面给出一套通用验证流程覆盖编译、运行、互操作、批处理四个维度。每个测试都包含输入、操作、预期结果和失败排查思路。7.1 基础编译运行测试输入文件(ns test.basic) (defn fibonacci [n] (if ( n 2) n ( (fibonacci (- n 1)) (fibonacci (- n 2))))) (println (fibonacci 10))执行流程npx cherry src/fib.cljs -o output/fib.js qjs output/fib.js预期结果输出55。判断标准编译阶段没有报SyntaxError。运行时没有抛出ReferenceError。数字输出正确。这个测试主要验证 Cherry 对基础表达式、条件分支和递归调用的支持。7.2 JavaScript 互操作测试Cherry 既然最终面向 JS 平台就必须能用原生 JS 的能力。测试代码(ns test.interop) (def arr (array 1 2 3)) (.push arr 4) (js/console.log arr length: (.-length arr)) (println last: (.at arr -1))这段代码用到了原生数组、方法调用和属性访问。如果在 QuickJS 下能输出arr length: 4和last: 4说明互操作层没问题。需要留意的是不同 Cherry 版本对js/特殊命名空间和方法调用语法的支持度可能不同。如果这段代码在你的版本里报错优先去 Cherry 官方文档查互操作写法。7.3 批量数据处理测试快速验证脚本层能不能做批量任务写一个简单的 map-reduce 风格函数(ns test.batch) (defn process-batch [items] (- items (map #(* % %)) (filter #( % 10)) (reduce ))) (println (process-batch [1 2 3 4 5 6]))预期输出是4 9 16 25 36 90。这个测试验证map、filter、reduce等核心集合函数在 QuickJS 里的行为。7.4 模块化测试如果 Cherry 支持模块编译可以再创建一个模块文件(ns lib.util) (defn add [a b] ( a b))然后在主文件里引用它。具体引用语法由 Cherry 模块方案决定可能是:require也可能是原生的import输出。测试目的是确认多文件项目的编译和加载没问题。如果你的项目只有单个脚本文件这个测试可以跳过。7.5 失败排查方向编译阶段报错通常是 Cherry CLI 参数不对或源码语法不兼容。运行时提示ReferenceError说明编译产物里引用了 QuickJS 不支持的全局变量需要注入或替换。运行时报Not Implemented说明 Cherry 编译出的代码用到了 QuickJS 未实现的 API。输出结果和预期不一致优先检查 Cherry 语法与 Clojure 的细微差异。8. 宿主程序嵌入与接口能力8.1 Choq 不一定提供 HTTP API很多人在评估工具时第一反应是“有没有 REST API”但 Choq 不是面向 Web 服务的工具。它的“接口”更可能是宿主语言 API也就是你通过 C、C、Rust、Go 等语言调用 QuickJS然后把 Cherry 编译后的 JS 加载进去执行。如果你需要把脚本能力暴露成 HTTP 服务通常是自己包一层 Web Server再调用 Choq 提供的嵌入式入口。8.2 C 宿主嵌入的通用骨架假设你要用 C 写宿主QuickJS 的官方 API 大致长这样#include quickjs.h #include quickjs-libc.h #include stdio.h #include string.h int main() { JSRuntime *rt JS_NewRuntime(); JSContext *ctx JS_NewContext(rt); js_std_add_helpers(ctx, argc, argv); const char *code console.log(hello from choq); JSValue val JS_Eval(ctx, code, strlen(code), input, JS_EVAL_TYPE_GLOBAL); if (JS_IsException(val)) { js_std_dump_error(ctx); } JS_FreeValue(ctx, val); js_std_loop(ctx); JS_FreeContext(ctx); JS_FreeRuntime(rt); return 0; }这段代码是 QuickJS 的标准嵌入骨架不是 Choq 的特定 API。实际使用时你需要读取 Cherry 编译后的 JS 文件内容替换掉code变量或者用JS_Eval多次执行模块代码。8.3 Rust 宿主嵌入的通用骨架如果 Choq 提供 Rust 绑定或者你想直接用rquickjs组装类似方案骨架如下use rquickjs::{Context, Runtime}; fn main() - Result(), Boxdyn std::error::Error { let rt Runtime::new()?; let ctx Context::full(rt)?; ctx.with(|ctx| { ctx.eval::(), _(console.log(hello from cherry on quickjs))?; Ok::_, rquickjs::Error(()) })?; Ok(()) }Rust 绑定最大的优势是内存安全。宿主程序可以为每个插件建一个独立的 QuickJS Runtime插件崩溃时不至于影响主进程。8.4 “批量任务”怎么落地Choq 本身没有批量队列概念但脚本层天然适合做批量处理。你可以写一个 Cherry 函数接收一个数据集执行循环处理。在宿主程序里为每个任务创建新的 QuickJS 上下文隔离执行。用 Shell 脚本循环调用qjs处理多个文件。for file in ./input/*.js; do qjs $file ./output/$(basename $file).log done这种方案简单可靠但缺少任务队列和失败重试。生产环境建议在宿主层实现任务调度。9. 资源占用与性能观察9.1 如何测量 QuickJS 的内存占用运行编译产物时可以在 Linux 下用/usr/bin/time观察最大常驻内存/usr/bin/time -v qjs output/main.js输出里看这两项Maximum resident set size运行时的峰值内存。Elapsed (wall clock) time整体耗时。macOS 可以这样/usr/bin/time -l qjs output/main.js需要注意的是QuickJS 的内存占用和你加载的 JS 代码复杂度直接相关。一个空脚本和一个加载大量数据结构的脚本数字会差很多。不要用一次测试结果去推导全部场景。9.2 Cherry 编译产物的体积Cherry 的核心卖点之一是产物体积小。你可以在编译后观察 JS 文件大小ls -lh output/main.js如果你的项目引入了较多标准库函数体积会上升。稳妥的做法是设置一个体积预算用构建脚本检查每次编译结果是否超标。9.3 影响性能的关键因素代码里是否有重计算。Cherry 编译产物本质是 JS所以算法复杂度、循环次数、数据结构选择都直接影响运行时间。是否大量使用eval或动态生成函数。QuickJS 对这种操作支持有限能预编译就预编译。宿主程序是否同时创建大量 Runtime。每个 Runtime 都有独立开销线程和上下文数量需要控制。是否在脚本中频繁操作大字符串。JS 引擎的字符串拼接策略在 QuickJS 里同样需要注意。9.4 降低内存占用的思路一次任务只创建一个 Runtime执行完再销毁。脚本内部尽量用原生数组避免复制大量中间对象。不加载用不到的全局函数宿主注入 API 时按需添加。对沙箱脚本设置内存上限QuickJS 有JS_SetMemoryLimit之类的接口具体方法看官方文档。10. 常见问题与排查方法下面这张表覆盖“依赖安装、编译、运行、嵌入”几个阶段最常见的坑。问题现象可能原因排查方式解决方案npx cherry命令找不到包名安装错误或未安装执行npm list cherry --depth0查看按 README 确认实际包名和作用域重新安装qjs命令不存在QuickJS 未安装或未加入 PATH执行which qjs重新编译安装 QuickJS或将二进制加入 PATHCherry 编译报语法错误源码里有 Clojure 标准库不支持的写法查看报错行号和片段删除不支持的语法改用 Cherry 文档推荐写法JS 文件在 Node 里正常在 qjs 里报错QuickJS 缺少对应内置 API在 qjs 中执行console.log(typeof 某个API)在宿主层注入缺失 API或替换实现运行时提示undefined is not a function互操作调用方式不对打印目标对象类型检查属性访问和方法调用语法批量脚本中途崩溃某个文件数据异常或语法不兼容单文件逐条执行查看报错在宿主层加 try-catch 和重试逻辑宿主程序内存持续增长每次执行脚本没释放 Runtime查看宿主日志和内存曲线确认每次任务结束调用 Runtime 销毁 API依赖下载很慢网络源问题检查 npm 源配置切换镜像源或离线安装实际排查时最重要的是分阶段定位到底是编译器不认还是引擎不认还是互操作层不匹配。先在npx cherry编译阶段解决所有静态问题再用node output/main.js验证 JS 在通用引擎里能跑最后再切到qjs测试嵌入式环境。这样能最大限度减少变量。11. 最佳实践与使用建议11.1 第一次跑通先稳定最小配置把“Cherry 源文件 - 编译 - qjs 运行”这条链路固定成几个命令不要一开始就引入复杂模块、构建工具和宿主嵌入。最小可运行配置是你的排错基准。11.2 目录分离建议按下面结构管理项目choq-demo/ ├── src/ # Cherry/Clojure 源码 ├── output/ # 编译后的 JS ├── host/ # 宿主程序代码 ├── scripts/ # 编译和运行脚本 └── logs/ # 批量任务日志源码、产物、宿主、日志分开放后续排查问题能省很多时间。11.3 给宿主脚本做沙箱保护任何支持用户脚本的宿主程序都必须考虑沙箱。至少要限制脚本能访问哪些全局对象。脚本运行时长。脚本内存上限。脚本是否允许访问文件系统和网络。QuickJS 允许宿主精细控制上下文但控制逻辑要你自己写。11.4 批量任务要加日志和重试批量处理数据时建议每个任务都记录输入文件名、输出结果、耗时和错误信息。失败任务不要直接吞掉至少重试一次并把完整错误写入日志。11.5 管线化编译如果业务复杂可以用 npm scripts 固定编译命令{ scripts: { build: npx cherry src/main.cljs -o output/main.js, run:qjs: qjs output/main.js } }这样既能在本地快速复现问题也为后续接入 CI 留下入口。11.6 涉及人脸、声音、版权素材时虽然 Choq 本身不直接处理图像和语音但如果你的脚本层承载了这些业务必须确认素材来源合法、已获得相关人物和版权方授权不得把脚本能力用于违规用途。12. 总结与下一步Choq 最值得尝试的地方不在于它的 CI 和代码量而在于它指出了一个非常务实的组合用 Cherry 写脚本、用 QuickJS 当运行时让宿主程序在极轻量的前提下获得类 Clojure 的脚本能力。这对嵌入式、边缘计算和工具类软件是一个有吸引力的方向。如果你打算上手第一步建议先跑通“Cherry 编译 - qjs 运行”的最小链路用一个纯函数脚本验证环境。第二步再尝试宿主嵌入把编译产物加载到 C 或 Rust 程序里执行。第三步才是设计批量任务、沙箱策略和 API 注入。最容易踩的坑有两个一是把 Cherry 当成完整 ClojureScript 来用遇到不支持的库函数后产生挫败感二是直接跳到宿主嵌入结果分不清问题是出在编译器还是出在 QuickJS API。按“最小链路 - 互操作 - 嵌入 - 批处理”的顺序推进会顺畅很多。后续如果你玩熟了可以继续扩展的方向包括给宿主程序做一个插件系统让第三方开发者用 Cherry 写扩展脚本对比 QuickJS 和其他嵌入式 JS 引擎的启动时间和内存占用把编译管道接入 CI只允许通过产物体积检查和静态分析的代码进入发布流程。这类探索比单纯“跑通 Demo”的价值大得多。
分享:

看完干货,该让你的企业上线了

免费需求沟通 · 48 小时内出具建站方案 · 河南本地可上门