SKILL.md
Pyzig Module
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:
- Python loader/glue:
src/faebryk/core/zig/init.py(build-on-import +.pyisyncing) - Zig build:
src/faebryk/core/zig/build.zig(buildspyzig.so+pyzig_sexp.so, generates stubs) - Zig binding utilities:
src/faebryk/core/zig/src/pyzig/*(wrapper generation + minimal C-API surface)
Quick Start
ato dev compile
python -c "import faebryk.core.zig; import faebryk.core.graph"
Relevant Files
- Python-side loader/build glue:
- src/faebryk/core/zig/init.py (ZIGNORECOMPILE, ZIGRELEASEMODE, lock, stub syncing)
- Zig build + stub generation:
- src/faebryk/core/zig/build.zig (builds extensions + runs .pyi generator)
- Core pyzig utilities:
- 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)
- Example consumers:
- src/faebryk/core/zig/src/python/graph/graphpy.zig - src/faebryk/core/zig/src/python/sexp/sexppy.zig
Dependants (Call Sites)
- Graph bindings:
src/faebryk/core/zig/src/python/graph/* - Sexp bindings:
src/faebryk/core/zig/src/python/sexp/* - TypeGraph bindings:
src/faebryk/core/zig/src/python/faebryk/*(and friends)
How to Work With / Develop / Test
Core Concepts
- Direct binding: pyzig calls the CPython C-API directly (no cffi/ctypes).
- Wrapper types: most exposed Zig structs become Python heap types via
wrapinpython(...)/wrapinpython_simple(...). - Global type registry: prevents re-creating Python
PyTypeObjects for the same Zig type (type_registry). - No direct
init(by default): many “reference” types are not meant to be user-constructed;pyzigoften installs an init that raises. - Debug handle: generated wrappers include
__zig_address__()to help debug pointer identity.
Development Workflow
- Edit Zig:
- binding helpers: src/faebryk/core/zig/src/pyzig/* - module wrappers: src/faebryk/core/zig/src/python/**
- Rebuild native modules:
- ato dev compile (imports faebryk.core.zig; editable installs compile-on-import) - set ZIG_RELEASEMODE=ReleaseFast|ReleaseSafe|Debug as needed
- If you changed stubs/output:
- ensure src/faebryk/core/zig/gen/** gets updated (this is driven by src/faebryk/core/zig/init.py)
Testing
- Smoke tests are usually through downstream modules:
- python -m faebryk.core.graph (GraphView allocation/cleanup stress) - ato dev test --llm test/core/solver (heavy user of graph + bindings via many subsystems)
Best Practices
- Assume mistakes segfault: treat changes here like unsafe systems programming.
- Be explicit about ownership:
- if a wrapper allocates Zig memory, define how it is freed (explicit .destroy() vs tp_dealloc calling .deinit()). - if you duplicate input buffers (sexp does), expose a free(...) path and document it.
- Don’t rely on Python GC for Zig arenas unless you intentionally installed a
tp_deallocthat callsdeinit. - Stub hygiene matters: keep the
.pyisurface accurate; many callers rely on types for navigation.
Build-on-import behavior (important)
src/faebryk/core/zig/init.py is responsible for:
- compiling extensions in editable installs (unless
ZIG_NORECOMPILE=1) - loading
pyzig.soandpyzig_sexp.sofromsrc/faebryk/core/zig/zig-out/lib/ - copying + formatting generated
.pyifiles intosrc/faebryk/core/zig/gen/**(black + ruff)