架构
核心层与三套绑定的分层、各自的实现取舍,以及不 panic 的设计约束。
适合:开发者
x-iztro 是一份 Rust 核心加三套绑定。算法只实现一次, Python、Go 与 C 调用方拿到的是同一份计算结果。
分层
┌──────────────────────────────┐
│ Rust 核心库 │
│ astro/ 排盘、运限、宫位推算 │
│ star/ 安星 │
│ data/ 枚举、常量、数据表 │
│ i18n/ 六语言词表与双向查找 │
└──────────────┬───────────────┘
│
bridge.rs(编组与分派)
dto.rs(序列化契约)
│
┌────────────────────┼────────────────────┐
│ │ │
python.rs wasm.rs ffi.rs
PyO3 扩展 wasm32-wasip1 C ABI
│ │ │
Python 包 Go 包(wazero) C / C++ / 其他两个共用层各司其职:
| 层 | 职责 |
|---|---|
bridge.rs | 入参解析、按名分派、结果编组。Python 与 Go 走同一个函数,行为没有分叉的余地。它是 crate 内部模块,不在公开 API 面上 |
dto.rs | 序列化契约:camelCase 键、按盘面语言翻译的值,外加 *Key 标识与排盘上下文 |
绑定文件因此很薄——只剩语言特有的部分:wasm 的内存协定、PyO3 的异常类型。
三套绑定的取舍
Python:PyO3 原生扩展
Rust 侧用 pythonize 在 Python 对象与 Rust 结构体之间直转, Python 侧用 dataclass 包装成类型化 API。
- 编译为 abi3 wheel(
abi3-py310),一个 wheel 覆盖 Python 3.10 及以上 - 零运行期依赖,纯 stdlib(dataclasses + StrEnum)
- 没有 JSON 序列化往返,开销最小
Go:内嵌 WebAssembly
编译为 wasm32-wasip1,用纯 Go 的 wazero 运行时执行。
选它而不是 cgo 的理由是保留 Go 的交叉编译能力:cgo 会让 GOOS/GOARCH
交叉编译变得极其麻烦,还要求使用者本机有 C 工具链。wasm 方案下 go get 即用,
静态链接与容器构建都不受影响。
单个 wasm 实例不能并发使用,包内维护一个实例池(上限 GOMAXPROCS):
每个调用取一个空闲实例,用完归还,多 goroutine 之间不串行化。
wasm 模块只编译一次,编译产物落盘缓存在 os.UserCacheDir() 下,
所以只有机器上第一次是 ~200ms,之后每个进程的首次调用 ~30ms。
iztro.Warmup(ctx) 可以把这段冷启动提前到服务启动阶段,
iztro.Close(ctx) 归还全部实例内存。
wazero 的编译器后端只支持 amd64 与 arm64,其余架构走解释器,速度慢但结果相同。
每次调用另有一次 JSON 编解码与 wasm 内存拷贝,热路径上单次排盘在 0.5ms 量级。
C FFI
标准 C ABI,收 C 字符串、返回 JSON 字符串。
错误以 {"error":"..."} 返回,由 serde 生成以保证转义完备。
外层有 catch_unwind 兜底。
核心层不 panic
日期格式与存在性、公历年份范围、时辰索引在核心层校验,入口返回 Result;
性别、盘面语言、配置开关、标识这些以字符串传入的东西在绑定层解析时校验
(Rust 侧它们本来就是枚举)。两处都不 panic。
绑定层的 catch_unwind 只负责兜底库内部的缺陷,不承担参数校验职责。
这条约束由 wasm 目标决定
wasm 上 panic 会变成 trap,而 catch_unwind 在 wasm 上无效——兜不住。
更糟的是每次 trap 都会永久损耗模块实例的栈空间,累积之后连合法调用都会失败。
校验因此必须在更靠内的一层,三种编程语言共用同一道防线。
一致性怎么保证
三种编程语言的行为一致不靠纪律,靠三层结构约束:
bridge::query,入参解析与结果形状不可能分叉判断方法一律基于语言无关标识,因此同一条分析规则在三种编程语言上写出来、结果也相同。 标识约定见语言无关标识。
版本
| 项目 | 版本 |
|---|---|
| 对照的 iztro | v2.5.8(版本锁定) |
| Rust edition | 2024 |
| Python 要求 | 3.10 及以上 |
| Go 要求 | 1.22 及以上 |
许可
MIT。