Mino:将受 Clojure 启发的易用性带入可移植的 C99
在可嵌入脚本语言的世界里,开发者常常需要在 Lua 等轻量语言的低开销和现代函数式语言强大的数据处理易用性之间做权衡。Mino 旨在通过提供一种受 Clojure 启发、用可移植的 C99 编写的 Lisp,来弥合这一差距,专为以最小摩擦点嵌入现有宿主应用而设计。
通过将小内存占用、持久不可变数据结构以及基于能力的安全模型相结合,Mino 为需要灵活、沙箱化脚本层且不想承担 JVM 或沉重运行时开销的开发者提供了一个有吸引力的替代方案。
分层嵌入方案
Mino 围绕“层级”(tiers)的概念设计,允许开发者根据需求在占用空间和功能之间找到恰当的平衡。这种模块化确保语言保持“足够小可以直接嵌入”,同时仍然“足够快以发挥作用”。
1. Floor(最低层)
这是运行时的最精简版本,专注于绝对必要的部分:读取器、求值器、垃圾回收器、持久集合、数值运算以及基础宏。它不包含 core.clj 的求值,因而占用空间约为 613 KB,初始化时间极快,仅 0.23 ms。
2. Standard(完整 Clojure 表面)
在 Floor 层之上,Standard 版加入了 Clojure 脚本作者期望的特性,包括正则表达式、大数、多方法、协议和 transducer。此层的占用略增至 728 KB,初始化耗时 5.3 ms。值得注意的是,这一层仍然是沙箱环境;除非宿主显式授权,否则没有 I/O 能力。
3. Standalone(完整套件)
Standalone 层是完整包装,包含 I/O、文件系统访问、软件事务内存(STM)、agents、异步能力、捆绑的 clojure.* 标准库以及 REPL。体积 987 KB,初始化时间 5.5 ms,提供了类似 Clojure 的完整体验。
性能与可移植性
Mino 的主要优势之一是冷启动性能。在 x86_64 Linux 上,Floor 层的墙钟时间冷启动(50 次运行的中位数)为 1.04 ms。这与 Lua 5.5(0.78 ms)相当,并优于 Janet(1.95 ms),因此非常适合对进程生成时间敏感的环境。
由于使用可移植的 C99 编写,Mino 可以嵌入几乎任何提供 C FFI 的宿主,包括 C++、Rust、Go、Java、.NET、Swift 和 Zig。它不依赖外部运行时、JIT 或守护进程;宿主进程对执行拥有完全控制权。
架构支柱
隔离运行时与内存安全
每个 Mino 运行时都是独立的故障域,拥有专属堆和垃圾回收器。为防止共享可变状态和潜在的竞争条件,跨运行时的数据移动通过深拷贝完成。这种架构使单个进程能够在完全隔离的情况下托管多个租户。
基于能力的互操作
安全性深植于运行时设计。全新的 Mino 运行时默认没有任何能力——没有文件系统访问、没有网络访问、也没有环境状态。嵌入者必须显式授权每项能力,并注册脚本可调用的具体宿主类型和方法。
受 Clojure 启发的易用性
对脚本作者而言,Mino 提供了使 Clojure 强大的高级抽象:
- 持久不可变数据结构:消除别名 bug 和“谁改了我的表”之类的惊喜。
- 惰性序列:能够高效处理潜在无限的数据流。
- 尾调用优化:确保递归函数(如
drain操作)在常量栈空间内运行。 - REPL 驱动工作流:支持实时检查和运行时配置修改,无需重新编译宿主应用。
实际实现:三角色工作流
Mino 明确划分了三类主要角色的职责:
- 应用开发者:嵌入运行时并通过授权特定能力来定义安全边界。
- C++ 工程师:向脚本公开宿主类型。宿主对象被包装为带类型标签的句柄,并自动进行 GC 清理。由于 API 使用直接的值指针而非栈索引,避免了传统 C 嵌入 API 常见的越界错误。
- 脚本作者:使用 Lisp 语法编写业务逻辑。它们可以像操作原生 Mino 类型一样操作宿主对象,使用关键字作为数据访问器,集合则可作为可调用的谓词。
潜在使用场景
鉴于其设计,Mino 特别适用于以下场景:
- 规则引擎与策略:将宿主状态暴露给 Mino 谓词,以实现声明式业务逻辑。
- 插件系统:加载用户编写的脚本,严格控制资源上限且无环境访问。
- 数据管道:在持久集合上组合
map、filter、reduce,实现结构共享。 - 游戏脚本:为玩家提供可编程控制台,配合沙箱和步数限制防止无限循环或崩溃。
- 交互式控制台:在应用内部嵌入 REPL,实现实时调试和运行时配置。
摘要: Mino 是一种轻量、可嵌入的 Lisp,旨在快速进行概念验证开发,提供类似 Clojure 的持久数据结构和隔离运行时,且占用极小的 C99 代码体积。
标题: Mino:将受 Clojure 启发的易用性带入可移植的 C99