About

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++ / other

The two shared layers have distinct jobs:

LayerResponsibility
bridge.rsArgument 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.rsThe 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:

One algorithm — all computation happens in the Rust core, and the bindings contain no Zi Wei logic at all
One marshaller — Python and Go call the same bridge::query, so argument parsing and result shape cannot fork
Paired assertions — every outward capability has a parity test on both the Python and Go sides, asserting the same values on the same chart

Predicates 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

ItemVersion
iztro referencev2.5.8 (version pinned)
Rust edition2024
Python requirement3.10 or later
Go requirement1.22 or later

For the current version number see crates.io and PyPI.

Licence

MIT.

On this page