Architecture
The layering of the core and the three bindings, the trade-offs behind each, and the no-panic design constraint.
For: developers
x-iztro is one Rust core plus three bindings. The algorithm is implemented once, and Python, Go and C callers all receive the same computed result.
Layering
┌──────────────────────────────┐
│ Rust core library │
│ astro/ charting, horoscopes,│
│ palace derivation │
│ star/ star placement │
│ data/ enums, constants, │
│ data tables │
│ i18n/ six vocabularies and │
│ two-way lookup │
└──────────────┬───────────────┘
│
bridge.rs (marshalling and dispatch)
dto.rs (serialization contract)
│
┌────────────────────┼────────────────────┐
│ │ │
python.rs wasm.rs ffi.rs
PyO3 extension wasm32-wasip1 C ABI
│ │ │
Python package Go package (wazero) C / C++ / otherThe two shared layers have distinct jobs:
| Layer | Responsibility |
|---|---|
bridge.rs | Argument parsing, dispatch by name, result marshalling. Python and Go go through the same function, leaving no room for behaviour to fork. It is a crate-internal module and not part of the public API surface |
dto.rs | The serialization contract: camelCase keys, values translated into the chart language, plus the *Key identifiers and the charting context |
That keeps the binding files thin — all that is left in them is the language-specific part: wasm's memory protocol, PyO3's exception types.
Trade-offs in the three bindings
Python: a native PyO3 extension
On the Rust side, pythonize converts directly between Python objects and Rust structs; on the Python side, dataclasses wrap the result into a typed API.
- Compiled as an abi3 wheel (
abi3-py310), so one wheel covers Python 3.10 and later. - Zero runtime dependencies, pure stdlib (dataclasses + StrEnum).
- No JSON serialization round trip, so the overhead is the lowest of the three.
Go: embedded WebAssembly
Compiled to wasm32-wasip1 and executed by wazero, a runtime written in pure Go.
The reason for choosing that over cgo is keeping Go's cross-compilation: cgo makes
GOOS/GOARCH cross-compilation extremely awkward and demands a C toolchain on the user's machine.
With the wasm approach go get is all it takes, and static linking and container builds are
unaffected.
A single wasm instance cannot be used concurrently, so the package maintains an instance pool
(capped at GOMAXPROCS): each call takes a free instance and returns it afterwards, and goroutines
are not serialized against each other.
The wasm module is compiled only once, and the compiled artifact is cached on disk under
os.UserCacheDir(), so only the very first run on a machine costs ~200ms; after that the first call
in each process costs ~30ms. iztro.Warmup(ctx) moves that cold start to service startup, and
iztro.Close(ctx) releases all instance memory.
wazero's compiler backend covers amd64 and arm64 only; other architectures fall back to the interpreter — slower, same results.
Each call additionally costs one JSON encode/decode and a wasm memory copy; on the hot path a single chart is on the order of 0.5ms.
C FFI
A standard C ABI taking C strings and returning JSON strings. Errors come back as
{"error":"..."}, generated by serde so escaping is always complete. A catch_unwind sits around
the outside as a backstop.
The core does not panic
Date format and existence, the Gregorian year range and the hour index are validated in the
core, and the entry points return a Result. Gender, chart language, configuration switches and
keys — the things passed as strings — are validated in the binding layer as they are parsed (on the
Rust side they are enums to begin with). Neither place panics. The binding layer's catch_unwind is
a backstop for defects inside the library only; it carries no argument-validation duty.
This constraint comes from the wasm target
On wasm a panic becomes a trap, and catch_unwind does not work there — it cannot catch it. Worse,
every trap permanently consumes stack space in the module instance, and once enough have
accumulated even legitimate calls start failing. Validation therefore has to live further in, with
all three programming languages behind one line of defence.
Each language's error type is on the errors page for Rust, Python and Go.
How consistency is enforced
Consistent behaviour across the three programming languages does not rest on discipline; it rests on three structural constraints:
bridge::query, so argument parsing and result shape cannot forkPredicates rest on language-independent keys throughout, so one analysis rule written in any of the three programming languages produces the same result. The key conventions are on The language-independent key contract.
Versions
| Item | Version |
|---|---|
| iztro reference | v2.5.8 (version pinned) |
| Rust edition | 2024 |
| Python requirement | 3.10 or later |
| Go requirement | 1.22 or later |
For the current version number see crates.io and PyPI.
Licence
MIT.