moonbridge

    MoonBridge: generate installable Python packages from MoonBit modules, with typed stubs, GC-safe handles and a stable host ABI.

    ffi
    bindings
    python
    interop
    codegen
    wasm
    Download zip
    Author
    Version
    0.1.0
    License
    Apache-2.0
    Last updated
    19 hours ago
    Downloads
    8

    No dependencies

    #MoonBridge

    把 MoonBit 模块变成可 pip install 的 Python 包。

    给一个 moon.mod,MoonBridge 读取它的公开接口(moon info 产物),生成一个真正的 Python 包:模块、类、枚举、异常、.pyi 类型存根、docstring,加上 MoonBit 侧的运行时(句柄表 + 稳定宿主 ABI),最后打成 wheel。

    moon.mod ──moon info──▶ 接口 IR ──typemap──▶ 代码生成 ──▶ wheel │ MoonBit 运行时(句柄/ABI) │ wasm 模块 / native 库

    #Quick Start

    三条命令,从零到调用一个真实的 MoonBit 函数(Python 3.9+,无需了解 wasm、无需配置路径或 C 工具链):

    # 1. 安装 wheel(仓库已附带;也可用 bash scripts/wheel.sh 自行重建) pip install ./dist/moonbridge-0.1.0-py3-none-any.whl # 2. import -> install -> 调用 MoonBit 标准库 json 的函数 python - <<'PY' import moonbridge moonbridge.install() from moonbridge import moonbitlang__core__json as mjson print(mjson.valid('{"a": [1, 2, 3]}')) # True PY # 3. 完整闭环:11 条安装后断言(在仓库外运行可排除误导入本地 _gen/ 的可能) python scripts/wheel_smoke.py

    moonbridge.install() 会加载 wheel 内置的 wasm guest;第 3 步依次验证 JSON 往返、 对象构造与字段读写、list / dict / lambda 跨边界、类型化异常,以及结束时句柄归零。

    Quick Start 演示:30 秒,真实命令输出逐帧生成(非屏幕录制)

    动画里的输出取自干净 venv 的真实执行结果,只有 venv 的绝对路径缩写为 <venv>/...。 完整可复现链路是 bash scripts/e2e.sh,打包细节见 S6 打包。

    状态:S0 通过、S1 通过、S2 通过、S3 通过、S4 通过、S5 通过、S6 通过(2026-11-06)。 S0:调用 ABI、内存共享、宿主↔MoonBit 字节流、UTF-8 往返全部跑通,实测见 spikes/01-wasm-python。 S1:.mbti 解析器 + 类型图,在 501 个真实接口文件 / 17,793 条声明上 100% 通过, 见 S1 语料报告。 S2:类型映射 + Python 代码生成,501 个包生成 stub,mypy --strict 0 报错, 见 S2 映射与生成。 S3:跨包句柄与生命周期,真实 json 包端到端跑通,10 万次调用 0 泄漏、guest 内存逐字节不变, 见 S3 运行时。 S4:guest 侧也由生成器产出,对象能被构造、读、写;10 万次 × 6 个用例共 150 万帧, 内存曲线平直、live=0、dropped=0,见 S4 对象模型。 S5:Array / Map / Option / Result / 回调 / 异常字段全部可跨边界——Python 的 list、 dict、lambda 直接传,MoonBit 的 suberror 变成 Python 的 except 类, 见 S5 容器与回调。 S6:打成真正的 wheel——501 KB,508 个条目,requires = [] 的自写 PEP 517 后端, 可复现构建;pip install 之后 import moonbridge; moonbridge.install() 即可用, 见 S6 打包。

    开发复盘与「AI 可解释」证据索引:docs/development-retrospective.md — 关键架构决策(wasm 优先、Plan 模型)、AI 工具在开发流程中的角色,以及 「每一个技术选择该翻哪个文件、跑哪条命令才能解释」的对照表。


    #为什么要做这个

    MoonBit 已经能编译到 wasm / wasm-gc / js / native,但别的语言调用 MoonBit 仍然要靠手写胶水。 生态里现有的跨语言能力是零散的:

    目标语言现有方案形态
    Node.jsunmbt/napi-mbt生成 Node-API 绑定(仅 Node)
    Java / AndroidNanaloveyuki/ajniJNI 双向运行时(仅 Java)
    Cicyaaaww/moonbind从 C 头文件生成 MoonBit 绑定(反方向)
    TypeScriptmizchi/tsTS ↔ MoonBit 桥接
    Python无—
    Go / Rust / Swift / PHP无—

    MoonBridge 补的是这一层:不是给某个库手写一层包装,而是把"跨语言绑定"本身做成编译器式的生成器。 增量不在"又一个包装库",而在:

    1. 通用性:任意 MoonBit 模块一条命令生成,不是一次性胶水;
    2. 所有权协议:GC 托管对象通过句柄表跨越边界,宿主永远不会拿到悬垂指针;
    3. 类型保真:MoonBit 的 enum / Option / Result / 泛型映射成 Python 侧有类型的对象, 而不是 dict 与 Any;
    4. 可安装:产出符合 PEP 517 的 wheel,pip install 即用,不需要用户懂 wasm。


    #Spike 01 结论(已验证)

    验证项结果
    #export_name 导出mb_add / mb_fib / 缓冲与字符串函数全部可见
    线性内存导出需 options(link: {"wasm": {"export-memory-name": "memory"}})
    参数表示String / Bytes 参数是单个 i32 对象指针 → 必须走句柄协议
    宿主写入 → MoonBit 读写 16 字节,MoonBit 校验和 1626 一致
    MoonBit 写 → 宿主读读回 b'AAAAAAAA'
    UTF-8 往返你好, MoonBit 🚀 → 13 个码位 / 20 字节,往返字节一致
    每次调用开销本机 wasmtime-py 基线:nop 6.9 μs,单参 16.6 μs;MoonBit add 22.7 μs

    最后一行决定了架构:瓶颈在 Python 绑定层,不在 MoonBit。因此高频路径必须走 "批量调用 + 缓冲协议"(一次跨越处理 N 个元素),并且 native(C ABI) 后端要作为性能选项保留。 详见 ADR 0001。


    #S1:接口解析与类型图

    moon info 输出的 .mbti 是纯文本接口,但它是生成的、且随工具链版本演进的。 MoonBridge 用一个手写递归下降解析器把它读成 AST,再建成类型图:

    .mbti ──lexer──▶ token ──parser──▶ AST ──graph──▶ 类型图 │ 依赖顺序 / 互递归组 / 外部类型

    # 解析若干接口文件,逐文件汇报,任一失败则退出码非 0 moon run --target js src/cli -- path/to/pkg.generated.mbti ... # 跑真实语料(标准库 + 20 个已发布模块及其依赖) bash scripts/corpus.sh all

    已覆盖的语法(全部由真实语料逼出来,明细见 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 标识符)。

    AST ──typemap──▶ PyType ──render──▶ .pyi │ │ 降级阶梯(4 级,每级都记账) mypy --strict 校验

    # 全语料生成 + mypy strict(需要 python + mypy) bash scripts/gen.sh all bash scripts/gen.sh core # 仅标准库,离线可跑

    映射结果只有四种,按保真度从高到低:直接对应(Int→int)、透明擦除 (&T、T??)、句柄(包内类型 → 生成类,值留在 MoonBit 堆上,跨边界的是不透明整数)、 降级为 Any。第四级从不静默:每个 Any 都带一条原因记录,--gen 按原因汇总:

    stubs=501 failed=0 types=1937 funcs=4789 fields=3733 skipped=7 degraded: foreign=4812 unknown=890 result=130 higher-order=699 arity=0 async=81

    (S2 当时的数字。S3 引入世界索引后 foreign 降到 605、unknown 降到 255。)

    两处刻意的选择:函数类型降级为 Any 而不是 Callable(没有回调蹦床,写 Callable 等于承诺一条走不通的路);stub 之间互不 import(MoonBit 包图含 48 个环,跨包类型走 Any, 保证每个 .pyi 独立可检查)。完整映射表、命名规则、以及 mypy 逼出来的六类问题见 S2 映射与生成。

    #S3:跨包句柄与运行时

    前两段产出的是一份"说得对"的接口。这一段让它能真的调起来:跨包导入补上、句柄表跑起来、 wasmtime 接进来,然后拿真实的 moonbitlang/core/json 验一遍。

    .mbti ──▶ stub(.pyi) 类型检查器读的那一份 └──▶ module(.py) 解释器执行的那一份 ┐ ├─ 同一次遍历,签名不可能与函数体不符 世界索引 ──▶ from . import <stem> ┘ │ Python 对象 ──handle id──▶ 句柄表 ──▶ MoonBit 堆 ──▶ wasm guest ◀──批量回收 / close() / with──┘

    # 生成 + 端到端 + 10 万次调用基准,任一泄漏即退出码非 0 bash scripts/e2e.sh

    三个数字说明这一段到底做了什么:

    S2S3
    跨包类型降级为 Any4812 处605 处(剩下的真的不在语料里)
    生成的模块间导入边0566(276 个模块)
    10 万次调用的 guest 内存—64 KiB 不变,句柄表归零

    基准还抓到了三个都不报错、只体现在曲线上的 bug:0 号句柄被当成"没有对象"(第一份文档永不释放)、 导出函数的返回值泄漏(41 字节/次)、世代计数器回绕(每 1023 次漏一个)。三个都写进 S3 运行时,包括怎么二分定位的。

    #S4:生成 guest 与对象模型

    S3 的绑定只有一半是生成的:.py / .pyi 从接口推出来,guest 的 dispatch 表是手写的。 手写的表会漂——它不会因为编译失败被发现,只会在某次调用悄悄返回错的字段。S4 把这个缺口补上:

    ┌──────────── Plan ─────────────┐ .mbti ──▶ World ──▶ │ Sym[] 符号/形状/标签 │ ──┬──▶ .py / .pyi 调用方 (S1) (S3) │ HandleSlot[] 每类型一张句柄表 │ └──▶ guest.mbt 应答方 └───────────────────────────────┘

    一份计划,两半投影,两半不可能不一致。顺带把"看得见"变成"用得上":生成的类现在真的能 Position(3, 7)、读 line、写 cursor。

    S3S4
    guest 侧 dispatch手写 270 行生成 1161 行(--guest / --guest-out)
    句柄表手写 12 张按用到的具体类型生成
    生成类的可调用成员只有方法方法 + __init__ + @property + setter
    端到端断言调通 + 释放构造、读、写、句柄实参、dropped=0
    10 万次基准4 个用例6 个用例,150 万帧,内存平直

    验收又抓到一个不报错的泄漏: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 侧不需要任何新类型:

    Json.array([Json.number(1.0), Json.string("two")]) # list -> Array[Json] Json.object_({"a": Json.number(1.0)}) # dict -> Map[String, Json] mcb.call_with(lambda x: x + 2, 40) # callable -> Int32 id try: mjson.parse("{oops") except mjson.ParseError as exc: ... # suberror -> except 类

    回调是这里唯一需要新机制的东西:wire 上只有一个 Int32 id,guest 通过 wasm import 回头调用宿主,所以回调发生在 guest 栈上——没有协程、没有重放、没有第二个线程。

    S4S5
    wire 形状8 个标量递归树(WArr / WMap / WRes / WCall)
    guest 符号4954
    unmarshalable-param1514
    端到端断言2942

    验收抓到的是一类"只在重新生成之后才红"的 bug:Option[T] 的解码器就是 T 的解码器, 于是按名字去重时 WMaybe(WInt) 会把 dec_int 这个名字占住却什么都不写,真正的报错出现在 生成物的 dec_int is unbound。名字和形状不是一对一,这一课写在 S5 容器与回调 里,连同"回调通道被建立 ≠ 通得过" (debug_assert 在 release 下被编译掉,实测 0 次调用)一起。

    #S6:wheel

    前五个阶段的产物是"能跑的一堆文件"。S6 的验收只有一句:

    pip install ./dist/moonbridge-0.1.0-py3-none-any.whl python -c "import moonbridge; moonbridge.install()"

    构建系统是自己写的 128 行 PEP 517 后端,requires = []——用户不必为了装这个包先装 setuptools,断网也能构建。wheel 里是可移植的 wasm 与 494 个 .pyi,Tag: py3-none-any 由 Requires-Dist: wasmtime>=20 兜住平台差异。

    S5S6
    分发形态_gen/ 一个目录moonbridge-0.1.0-py3-none-any.whl(501 KB / 508 条目)
    构建依赖—0(标准库)
    构建可复现—两次构建 sha256 相同,且与 pip 驱动后端的结果相同
    安装后的初始化传 wasm 路径moonbridge.install()
    安装后功能断言—11 项全绿,live=0

    打包脚本最小心的地方是从仓库外跑冒烟测试:在仓库根目录 import moonbridge 会找到 _gen/,wheel 少一个文件也照样"通过"。细节见 S6 打包。

    #开发

    # MoonBit 侧 moon check --deny-warn moon test --deny-warn moon fmt --check # 语料(core 子集离线可跑;all 需要网络) bash scripts/corpus.sh core bash scripts/corpus.sh all # 生成 Python stub 并跑 mypy strict bash scripts/gen.sh all # 生成 + 端到端 + 基准(需要 python + wasmtime) bash scripts/e2e.sh BENCH_CALLS=20000 bash scripts/e2e.sh # 快速跑一遍 # 只打包:构建 wheel、装进临时 venv、从仓库外冒烟测试 bash scripts/wheel.sh bash scripts/wheel.sh --keep-venv # 留着 venv 检查 # Spike 复现(需要 Python 3.10+ 与 wasmtime) cd spikes/01-wasm-python/module && moon build --target wasm && cd .. python bridge_test.py

    CLI 目前在 js 后端运行(唯一不依赖 C 工具链的后端),文件读取只有一处 extern "js",其余代码与目标无关。

    工具链:MoonBit 0.1.20261023(moonc 0.10.17)/ Python 3.13 / wasmtime 50.0.1。


    #路线图


    #与现有生态的关系

    MoonBridge 不重新实现任何已有能力:JSON、CSV、正则、压缩等一律复用 mooncakes 上的包, 它只负责把它们的接口翻译到 Python。如果你在找一个 JSON 查询引擎,请用 moonbit-community/moonbit-jq;MoonBridge 的价值在于让那类库能被 Python 直接调用。


    #许可证

    Apache-2.0,见 LICENSE。