Skip to content

Diplomat bindings

Diplomat generates the C, C++, and Python native interfaces from fatoora-ffi. The Rust core remains independent of the binding framework. The public Python API in api.py wraps the generated private fatoora._native extension and supplies decimal conversion, exceptions, and object lifetime management.

The bridge is divided by capability:

Source Responsibilities
common.rs Structured errors, strict text conversion, panic boundaries, owned optional text
crypto.rs Configuration, keys, CSRs, certificates, signing, owned bytes
invoice.rs Invoice builders, parsing, snapshots, totals, XML, hashes, validation
api.rs Credentials, ZATCA requests, response outcomes, validation messages

C headers live in bindings/c, C++ headers in bindings/cpp, and Nanobind sources in bindings/python/native/generated. The old C ABI and CFFI implementation have been removed. FfiLibrary and fatoora.native are no longer public entry points; import from fatoora or its documented Python modules.

Python contracts

Install from the repository root:

python -m pip install ./bindings/python

Building requires Rust, the repository's native library dependencies, CMake, Python development headers, and a C++20 compiler. Build isolation installs the pinned Nanobind dependency. The wheel bundles _native and fatoora_ffi.

Decimal arguments accept Decimal, decimal strings, or integers. Floats and booleans are rejected. Rust validates UTF-8 and rejects embedded NUL. Exceptions retain .code, .kind, and structured .details; unknown numeric error codes remain available. The facade copies error data before releasing its native owner.

Objects support close() and context managers. Closing releases the facade's native reference; subsequent operations raise InvalidInputError. Builder setters validate input before updating state, allowing correction after an input error. build() consumes the builder's inner state on success and validation failure. Signing consumes a finalized invoice, and SignedInvoice.into_xml() consumes a signed invoice. Their outer owners remain safe to release after consumption.

Invoice data, parties, addresses, line items, and response getters return owned snapshots. They remain usable after their source object is closed. Python copies native byte views into bytes through a small Nanobind adapter, without NumPy; XML and other text outputs are owned strings.

Blocking requests and concurrency

ZATCA methods are synchronous. The custom Nanobind bindings in client_bindings.cpp add private _blocking_* methods that release the GIL during requests. The Python facade acquires an RLock for every argument owner in a consistent order and holds those locks through the call. This prevents concurrent close or consumption of a handle while native code uses it. Other Python threads can continue running.

_native is an implementation detail. Calling its generated methods directly bypasses the facade's locking and error conversion. Native C and C++ callers must provide their own synchronization, including exclusive access during mutation or consumption. Mutable objects expose no borrowed child fields. Bytes.as_slice() is a read-only view valid for the lifetime of its immutable Bytes owner.

C and C++ ownership

Include InvoiceBuilder.h for C or fatoora/InvoiceBuilder.hpp for C++ and link fatoora_ffi. C++ types live in namespace fatoora; standalone clients require C++17. See the C contract and C++ contract for complete clients.

Generated C symbols use fatoora_Type_method, such as fatoora_InvoiceBuilder_build. This is a breaking ABI replacement. Recompile clients against the generated headers distributed with their library version.

Fallible results contain a success value or an owned BindingError. Inspect is_ok before reading the union. Destroy every owned object and error exactly once with its generated destroy function. C++ uses std::unique_ptr to release these owners automatically. Optional values use the generated optional representation; absence is distinct from an empty string.

String inputs are length-delimited byte views. Malformed UTF-8 and embedded NUL return InvalidInput errors. Enum-like inputs use validated integer values, so unknown values produce errors. Callers must still supply valid pointers and obey reference lifetimes.

Fallible Rust operations catch unwinding panics and return Internal errors, including when a panic payload's destructor panics. Abort-mode panics, allocation aborts, and invalid native pointers cannot be recovered through this boundary.

Generate and verify

Generated files are checked-in build inputs. Regenerate them after changing bridge signatures or documentation; do not edit generated files directly.

cargo install diplomat-tool --version 0.16.1 --locked
python scripts/generate_bindings.py
python scripts/generate_bindings.py --check
cargo test -p fatoora-ffi --locked
python scripts/check_native_bindings.py
python scripts/check_c_reference.py
python -m pip wheel --no-deps ./bindings/python -w /tmp/fatoora-wheels

Install the resulting wheel in a fresh virtual environment, then run wheel_smoke.py and the Python tests. Do not add the source package to PYTHONPATH. Verify that the installed extension loads its bundled shared library, with the build-tree library unavailable. Linux dependency entries must use a library basename with $ORIGIN; macOS uses @rpath and @loader_path.

check_native_bindings.py builds C11 and C++17 clients on Linux/macOS, passes a signed XML fixture to the C++ contract, and honors CC, CXX, and CARGO_TARGET_DIR. DIPLOMAT_TOOL selects an alternate path to the generator.

Versions are pinned: Diplomat macro and generator 0.16.1, runtime 0.16.0, and Nanobind 2.12.0. Update the macro and generator together. Nanobind's generated deleter uses internal headers, so its pin also requires cross-language tests.

The replacement C11 and C++17 contracts have passed locally on Linux. The Linux CPython 3.14 installed wheel passed all 53 Python tests, including HTTP concurrency checks, and the bundled-library smoke test. Windows, macOS, and other Python versions require their platform CI checks; local Linux results do not establish those outcomes. The parity map records capability and test coverage separately.