pyzig
community[skill]
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.
$
/plugin install atopiledetails
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(ZIG_NORECOMPILE,ZIG_RELEASEMODE, lock, stub syncing)
- Zig build + stub generation:
src/faebryk/core/zig/build.zig(builds extensions + runs.pyigenerator)
- 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/graph_py.zigsrc/faebryk/core/zig/src/python/sexp/sexp_py.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
wrap_in_python(...)/wrap_in_python_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/**
- binding helpers:
- Rebuild native modules:
ato dev compile(importsfaebryk.core.zig; editable installs compile-on-import)- set
ZIG_RELEASEMODE=ReleaseFast|ReleaseSafe|Debugas needed
- If you changed stubs/output:
- ensure
src/faebryk/core/zig/gen/**gets updated (this is driven bysrc/faebryk/core/zig/__init__.py)
- ensure
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()vstp_dealloccalling.deinit()). - if you duplicate input buffers (sexp does), expose a
free(...)path and document it.
- if a wrapper allocates Zig memory, define how it is freed (explicit
- 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)
technical
- github
- atopile/atopile
- stars
- 3177
- license
- MIT
- contributors
- 41
- last commit
- 2026-04-03T20:56:37Z
- file
- .claude/skills/pyzig/SKILL.md