C ABI
powerio-capi builds a cdylib exporting pio_*. It is how every non-Rust consumer reaches powerio: PowerIO.jl resolves these symbols by dlsym from a pinned artifact, and the Julia, Python, and Rust surfaces are held to the same behavior by the tests in powerio-capi/src/lib.rs.
powerio-capi/include/powerio.h is the reference. It is generated by cbindgen and checked in; regenerate it with scripts/capi-header-regen.sh rather than editing it, and CI fails on a diff. The header’s own comment block states the naming, ownership, and error grammars and the vocabulary rule for bus, node, and branch, and it is authoritative where this page and it disagree.
The surface in one sitting
PioError *error = NULL;
PioModule *module = pio_parse_file("case9.m", NULL, &error);
if (!module) { /* pio_error_code(error), pio_error_message(error) */ }
const char *kind = pio_module_kind(module); /* "balanced_network" */
PioBalancedNetwork *net = pio_module_balanced_network(module, &error);
size_t n = pio_balanced_network_n_buses(net);
PioDiagnostics *findings = pio_module_diagnostics(module, &error);
for (size_t i = 0; i < pio_diagnostics_len(findings); i++)
puts(pio_diagnostic_code(findings, i));
char *text = pio_module_write_str(module, "matpower", NULL, &error);
pio_string_release(text);
pio_diagnostics_release(findings);
pio_balanced_network_release(net);
pio_module_release(module);
Parse produces a module handle; typed accessors produce independently owned child handles; every handle type has a retain/release pair, release(NULL) is a no-op, and releasing a parent never invalidates a child. Every fallible entry point takes a PioError ** out parameter and every entry point catches panics at the boundary. Borrowed strings and spans stay valid until their owner’s last release; owned char * results release with pio_string_release.
Versioning
PIO_ABI_VERSION is one integer with one meaning: change an existing signature or an existing documented behavior and it increments. A consumer compares pio_abi_version() against the macro it was built against and refuses a mismatch before calling anything else. The comparison is equality, not a floor, so there is no partial compatibility to negotiate.
Additive change does not bump it. New data arrives as a new symbol, a new Arrow table id, or a new key in a versioned JSON document. Arrow tables are append only: existing PIO_ARROW_TABLE_* ids and column order do not change, a new table takes the next id, new columns append at the end, and a consumer addresses columns by name.
A JSON document reachable through the ABI counts as documented behavior. Reshaping one bumps the integer even though no signature moves, because the alternative is a binding that passes the handshake and then reads null for a key it mirrors.
The per-version history, the classified v5 to v6 delta, and the porting table live in the Developer Guides: ABI history and symbol replacement.
Optional surface
Every optional entry point is behind a Cargo feature and is probed at runtime with pio_has_feature. Only arrow and dist also gate a header guard: a build missing one of those two exports nothing for it, so dlsym fails rather than the call misbehaving. matrix, gridfm, and prob symbols always link regardless of build features (pio_dc_data_* among them); pio_has_feature is the only probe for those three.
| feature | guard | adds |
|---|---|---|
arrow | PIO_ARROW | pio_balanced_network_to_arrow and the Arrow catalog |
matrix | none | the balanced matrix Arrow tables |
gridfm | none | GridFM Parquet dataset parsing through the one parse |
dist | PIO_DIST | the multiconductor value family and its network handle |
prob | none | the calculation families and the DC branch data (pio_dc_data_*) |
Released binaries carry all five. pio_build_info reports which ones a given library was built with, alongside its version, ABI integer, foreign schema versions, and error category tokens.