# Cross-extension interoperability
The public `siderust_py::interop` Rust module lets an independently compiled
PyO3 extension accept and return the canonical `siderust.Observer` and
`siderust.Direction` classes from the installed Python package.
## Supported baseline
- Siderust: `0.12.x`
- qtty: `0.8.x`
- tempoch: `0.7.x`
- affn: `0.10.x`
- PyO3: `0.29.x`
- siderust-py library outputs: `cdylib` and `rlib`
Use maturin 1.9.4 or newer. PyO3's `extension-module` feature is deliberately
not enabled in Cargo manifests: maturin sets `PYO3_BUILD_EXTENSION_MODULE` for
extension builds, while ordinary Rust tests and consumers remain linkable.
## Why the boundary uses primitives
A PyO3 `#[pyclass]` belongs to the extension module that registered it. If a
downstream shared library compiles another copy of siderust-py's private
wrapper, that copy is not the same Python type as the installed package's
class.
The interop API instead imports `siderust._siderust` and invokes narrow bridge
functions there. The canonical extension performs its own type checks and
constructs its own objects. Only primitive scalar values (`f64` payload fields
and the `u32` protocol version) cross the shared-library boundary.
- `ObserverParts` carries east-positive WGS84 geodetic longitude in degrees,
north-positive latitude in degrees, and ellipsoidal height in metres. It
reconstructs exactly `Geodetic<ECEF>`.
- `DirectionParts` carries right ascension and declination in degrees in the
Siderust `direction::ICRS` frame.
## Protocol compatibility
The canonical extension exposes a private integer `_bridge_protocol_version`.
The Rust interop API checks it before calling any bridge hook. Protocol 1
defines the primitive representations documented above. A missing, malformed,
or different protocol produces `ImportError` with the expected and installed
versions instead of failing later with an obscure missing-function error.
The Rust `siderust-py` dependency linked into a downstream extension and the
installed Python `siderust` package must support the same bridge protocol.
This protocol version, rather than the package semantic version, is the
compatibility contract.
## Cargo setup
```toml
[lib]
crate-type = ["cdylib"]
[dependencies]
pyo3 = "0.29"
siderust-py = "0.2"
```
For unreleased siderust-py changes, pin a Git dependency to an immutable tag or
full commit hash rather than tracking a moving branch:
```toml
siderust-py = { git = "https://github.com/Siderust/siderust-py.git", rev = "<40-character commit SHA>" }
```
For adjacent checkouts during workspace development, use
`siderust-py = { path = "../siderust.py" }`.
## Observer example
```rust
use pyo3::prelude::*;
use siderust_py::interop::{observer_from_python, observer_to_python, ObserverParts};
#[pyfunction]
fn inspect_observer(value: &Bound<'_, PyAny>) -> PyResult<(f64, f64, f64)> {
let observer = observer_from_python(value)?;
let parts = ObserverParts::from(&observer);
Ok((parts.longitude_degrees, parts.latitude_degrees, parts.height_metres))
}
#[pyfunction]
fn copy_observer(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
observer_to_python(py, &observer_from_python(value)?)
}
```
`copy_observer()` returns the installed package's actual class, so
`type(result) is siderust.Observer` is true.
## Direction example
```rust
use pyo3::prelude::*;
use siderust_py::interop::{direction_from_python, direction_to_python, DirectionParts};
#[pyfunction]
fn inspect_direction(value: &Bound<'_, PyAny>) -> PyResult<(f64, f64)> {
let direction = direction_from_python(value)?;
let parts = DirectionParts::from(&direction);
Ok((parts.right_ascension_degrees, parts.declination_degrees))
}
#[pyfunction]
fn copy_direction(py: Python<'_>, value: &Bound<'_, PyAny>) -> PyResult<Py<PyAny>> {
direction_to_python(py, &direction_from_python(value)?)
}
```
Objects of the wrong canonical type raise Python `TypeError`. Downstream code
should use these functions rather than depending on siderust-py's private
`#[pyclass]` implementation types.