MoonBridge 把 MoonBit 模块变成可 pip install 的 Python 包:以 .mbti 接口为唯一输入,同一份计划生成带 .pyi 存根的 Python 代码与 wasm guest 运行时,交付 wheel。支持 list/dict/lambda/except 原生互操作;含 561 个真实接口语料、168 项测试与端到端基准。
版权所有:中国计算机学会技术支持:开源发展技术委员会
京ICP备13000930号-9
京公网安备 11010802047560号
MoonBridge
把 MoonBit 模块变成可
pip install的 Python 包。给一个
moon.mod,MoonBridge 读取它的公开接口(moon info产物),生成一个真正的 Python 包:模块、类、枚举、异常、.pyi类型存根、docstring,加上 MoonBit 侧的运行时(句柄表 + 稳定宿主 ABI),最后打成 wheel。为什么要做这个
MoonBit 已经能编译到 wasm / wasm-gc / js / native,但别的语言调用 MoonBit 仍然要靠手写胶水。 生态里现有的跨语言能力是零散的:
unmbt/napi-mbtNanaloveyuki/ajniicyaaaww/moonbindmizchi/tsMoonBridge 补的是这一层:不是给某个库手写一层包装,而是把”跨语言绑定”本身做成编译器式的生成器。 增量不在”又一个包装库”,而在:
enum/Option/Result/ 泛型映射成 Python 侧有类型的对象, 而不是dict与Any;pip install即用,不需要用户懂 wasm。Spike 01 结论(已验证)
#export_name导出mb_add/mb_fib/ 缓冲与字符串函数全部可见options(link: {"wasm": {"export-memory-name": "memory"}})String/Bytes参数是单个 i32 对象指针 → 必须走句柄协议b'AAAAAAAA'你好, MoonBit 🚀→ 13 个码位 / 20 字节,往返字节一致nop6.9 μs,单参 16.6 μs;MoonBitadd22.7 μs最后一行决定了架构:瓶颈在 Python 绑定层,不在 MoonBit。因此高频路径必须走 “批量调用 + 缓冲协议”(一次跨越处理 N 个元素),并且 native(C ABI) 后端要作为性能选项保留。 详见 ADR 0001。
S1:接口解析与类型图
moon info输出的.mbti是纯文本接口,但它是生成的、且随工具链版本演进的。 MoonBridge 用一个手写递归下降解析器把它读成 AST,再建成类型图:已覆盖的语法(全部由真实语料逼出来,明细见 S1 语料报告):泛型与 trait 约束、 函数类型、
T??、&Trait、async fn、raise/raise E/noraise/nocancel及其+组合、pub(all)/pub(open)/priv、mut字段、构造器标签字段、const字面量、type X = Y别名、重导出块、旧式import( … )块。类型图回答三个生成器必需的问题:先发哪个类型(
topo_order)、 哪些类型互相递归必须一起产(cycles,Tarjan SCC)、 哪些类型不在本包(external_names,交给宿主类型表)。S2:类型映射与 Python 代码生成
解析出的类型图要变成 Python 能理解的东西,中间要过两道关:类型映射(MoonBit 类型 → Python 注解)和命名(MoonBit 标识符 → 合法且不遮蔽的 Python 标识符)。
映射结果只有四种,按保真度从高到低:直接对应(
Int→int)、透明擦除 (&T、T??)、句柄(包内类型 → 生成类,值留在 MoonBit 堆上,跨边界的是不透明整数)、 **降级为Any**。第四级从不静默:每个Any都带一条原因记录,--gen按原因汇总:(S2 当时的数字。S3 引入世界索引后
foreign降到 605、unknown降到 255。)两处刻意的选择:函数类型降级为
Any而不是Callable(没有回调蹦床,写Callable等于承诺一条走不通的路);stub 之间互不 import(MoonBit 包图含 48 个环,跨包类型走Any, 保证每个.pyi独立可检查)。完整映射表、命名规则、以及 mypy 逼出来的六类问题见 S2 映射与生成。S3:跨包句柄与运行时
前两段产出的是一份”说得对”的接口。这一段让它能真的调起来:跨包导入补上、句柄表跑起来、 wasmtime 接进来,然后拿真实的
moonbitlang/core/json验一遍。三个数字说明这一段到底做了什么:
Any基准还抓到了三个都不报错、只体现在曲线上的 bug:0 号句柄被当成”没有对象”(第一份文档永不释放)、 导出函数的返回值泄漏(41 字节/次)、世代计数器回绕(每 1023 次漏一个)。三个都写进 S3 运行时,包括怎么二分定位的。
S4:生成 guest 与对象模型
S3 的绑定只有一半是生成的:
.py/.pyi从接口推出来,guest 的 dispatch 表是手写的。 手写的表会漂——它不会因为编译失败被发现,只会在某次调用悄悄返回错的字段。S4 把这个缺口补上:一份计划,两半投影,两半不可能不一致。顺带把”看得见”变成”用得上”:生成的类现在真的能
Position(3, 7)、读line、写cursor。--guest/--guest-out)__init__+@property+ setterdropped=0验收又抓到一个不报错的泄漏:
parse返回Json的句柄,而Json是 enum,render_enum从来没写moon_type_id——close()发出release_tag(0, id),guest 里没有 tag 0 的表, 什么都没释放,Python 侧却把这次释放算成功。只有Runtime.dropped看得见它。 细节见 S4 对象模型。S5:容器、回调与异常
到 S4 为止,能调的函数都是标量函数:
Array、Map、Option、Result、函数值全在unmarshalable-param这一格里。S5 把这一格清掉,Python 侧不需要任何新类型:回调是这里唯一需要新机制的东西:wire 上只有一个
Int32id,guest 通过 wasm import 回头调用宿主,所以回调发生在 guest 栈上——没有协程、没有重放、没有第二个线程。WArr/WMap/WRes/WCall)unmarshalable-param验收抓到的是一类”只在重新生成之后才红”的 bug:
Option[T]的解码器就是T的解码器, 于是按名字去重时WMaybe(WInt)会把dec_int这个名字占住却什么都不写,真正的报错出现在 生成物的dec_int is unbound。名字和形状不是一对一,这一课写在 S5 容器与回调 里,连同”回调通道被建立 ≠ 通得过” (debug_assert在 release 下被编译掉,实测 0 次调用)一起。S6:wheel
前五个阶段的产物是”能跑的一堆文件”。S6 的验收只有一句:
构建系统是自己写的 128 行 PEP 517 后端,
requires = []——用户不必为了装这个包先装 setuptools,断网也能构建。wheel 里是可移植的 wasm 与 494 个.pyi,Tag: py3-none-any由Requires-Dist: wasmtime>=20兜住平台差异。_gen/一个目录moonbridge-0.1.0-py3-none-any.whl**(501 KB / 508 条目)moonbridge.install()live=0打包脚本最小心的地方是从仓库外跑冒烟测试:在仓库根目录
import moonbridge会找到_gen/,wheel 少一个文件也照样”通过”。细节见 S6 打包。开发
工具链:MoonBit
0.1.20261023(moonc0.10.17)/ Python 3.13 / wasmtime 50.0.1。路线图
moon info,覆盖 struct / enum / 泛型 / trait / 别名.pyi,mypy --strict0 报错Array/Map/Option/Result/Callable全部可跨边界,与现有生态的关系
MoonBridge 不重新实现任何已有能力:JSON、CSV、正则、压缩等一律复用 mooncakes 上的包, 它只负责把它们的接口翻译到 Python。如果你在找一个 JSON 查询引擎,请用
moonbit-community/moonbit-jq;MoonBridge 的价值在于让那类库能被 Python 直接调用。许可证
Apache-2.0,见 LICENSE。