How the Zig↔Python binding layer works (pyzig), including build-on-import, wrapper generation patterns, ownership rules, and where to add new exported APIs. Use when adding Zig-Python bindings, modifying native extensions, or debugging C-API interactions.
pyzig is the Zig↔Python interoperability layer used by Faebryk’s native modules (graph, sexp, faebryk typegraph, …).
There are three distinct layers to keep straight:
src/faebryk/core/zig/__init__.py (build-on-import + .pyi syncing)src/faebryk/core/zig/build.zig (builds pyzig.so + pyzig_sexp.so, generates stubs)src/faebryk/core/zig/src/pyzig/* (wrapper generation + minimal C-API surface)ato dev compile
python -c "import faebryk.core.zig; import faebryk.core.graph"
src/faebryk/core/zig/__init__.py (ZIG_NORECOMPILE, ZIG_RELEASEMODE, lock, stub syncing)src/faebryk/core/zig/build.zig (builds extensions + runs .pyi generator)src/faebryk/core/zig/src/pyzig/pybindings.zig (minimal CPython C-API declarations)src/faebryk/core/zig/src/pyzig/pyzig.zig (wrapper generation helpers)src/faebryk/core/zig/src/pyzig/type_registry.zig (global type-object registry)src/faebryk/core/zig/src/pyzig/pyi.zig (stub generation helpers)src/faebryk/core/zig/src/python/graph/graph_py.zigsrc/faebryk/core/zig/src/python/sexp/sexp_py.zigsrc/faebryk/core/zig/src/python/graph/*src/faebryk/core/zig/src/python/sexp/*src/faebryk/core/zig/src/python/faebryk/* (and friends)wrap_in_python(...) / wrap_in_python_simple(...).PyTypeObjects for the same Zig type (type_registry).__init__ (by default): many “reference” types are not meant to be user-constructed; pyzig often installs an init that raises.__zig_address__() to help debug pointer identity.src/faebryk/core/zig/src/pyzig/*src/faebryk/core/zig/src/python/**ato dev compile (imports faebryk.core.zig; editable installs compile-on-import)ZIG_RELEASEMODE=ReleaseFast|ReleaseSafe|Debug as neededsrc/faebryk/core/zig/gen/** gets updated (this is driven by src/faebryk/core/zig/__init__.py)python -m faebryk.core.graph (GraphView allocation/cleanup stress)ato dev test --llm test/core/solver (heavy user of graph + bindings via many subsystems).destroy() vs tp_dealloc calling .deinit()).free(...) path and document it.tp_dealloc that calls deinit..pyi surface accurate; many callers rely on types for navigation.src/faebryk/core/zig/__init__.py is responsible for:
ZIG_NORECOMPILE=1)pyzig.so and pyzig_sexp.so from src/faebryk/core/zig/zig-out/lib/.pyi files into src/faebryk/core/zig/gen/** (black + ruff)