Architecture Overview¶
headerkit is organized around a unified, hook-driven pipeline: backends parse source units into an IR (Intermediate Representation) rooted at SourceUnit, optional transform hooks apply AST mutations or dialect adaptations, and writers consume the IR to generate target output.
The Pipeline¶
graph TD
A["Input Source / InputSpec"] --> B
B["Backend: parse_unit<br>(ParserBackend protocol)"] --> C
C["IR<br>(SourceUnit, Declaration, TypeExpr)"] --> D
D["Transformations: transform_unit<br>(Waterfall hook pipeline)"] --> E
E["Writer: write_output<br>(WriterBackend protocol)"] --> F
F["Output String<br>(CFFI cdef, ctypes, Cython .pxd, Nim, ...)"]
B -.- B1["e.g., LibclangBackend, TreeSitterBackend"]
E -.- E1["e.g., CffiWriter, CtypesWriter,<br>CythonWriter, NimWriter, LuaWriter, ..."]
Each stage is decoupled through the unified hook engine (headerkit.hooks). Backends know nothing about writers. Writers know nothing about backends. The IR is the contract between them.
Layer 1: Backends (Parsing)¶
A backend implements the ParserBackend protocol and converts C/C++ source code into IR.
from headerkit import ParserBackend
from headerkit.ir import Header
class ParserBackend(Protocol):
def parse(
self,
code: str,
filename: str,
include_dirs: list[str] | None = None,
extra_args: list[str] | None = None,
*,
use_default_includes: bool = True,
recursive_includes: bool = True,
max_depth: int = 10,
project_prefixes: tuple[str, ...] | None = None,
) -> Header: ...
@property
def name(self) -> str: ...
@property
def supports_macros(self) -> bool: ...
@property
def supports_cpp(self) -> bool: ...
Built-in Backend: LibclangBackend¶
The LibclangBackend uses LLVM's libclang to parse headers. It provides:
- Full C and C++ support (templates, namespaces, classes)
- Preprocessor handling (
#include,#define,#ifdef) - Source location tracking for error reporting
- Recursive include processing for umbrella headers
from headerkit import get_backend
backend = get_backend("libclang")
header = backend.parse(code, "myheader.h")
Backend Registry¶
Backends register themselves using register_backend():
from headerkit.backends import register_backend
register_backend("mybackend", MyBackendClass, is_default=False)
Registry functions:
| Function | Description |
|---|---|
get_backend(name=None) |
Get a backend instance (default if name is None) |
list_backends() |
List all registered backend names |
is_backend_available(name) |
Check if a backend is usable (real load test for libclang) |
register_backend(name, cls) |
Register a new backend |
See Writing Custom Backends for a complete guide.
Layer 2: IR (Intermediate Representation)¶
The IR is a tree of Python dataclasses rooted at Header. It is designed to be parser-agnostic: any backend that can parse C/C++ can produce the same IR.
Type Expressions¶
Type expressions (TypeExpr) represent C types as composable trees:
classDiagram
class TypeExpr {
<<protocol>>
}
class CType {
name: str
qualifiers: list[str]
}
class Pointer {
pointee: TypeExpr
qualifiers: list[str]
}
class Array {
element_type: TypeExpr
size: int | None
}
class FunctionPointer {
return_type: TypeExpr
parameters: list[Parameter]
is_variadic: bool
}
TypeExpr <|-- CType
TypeExpr <|-- Pointer
TypeExpr <|-- Array
TypeExpr <|-- FunctionPointer
Pointer --> TypeExpr : pointee
Array --> TypeExpr : element_type
FunctionPointer --> TypeExpr : return_type
| Class | Represents | Example |
|---|---|---|
CType |
Base type with qualifiers | int, const char, unsigned long |
Pointer |
Pointer to another type | int*, const char*, void** |
Array |
Fixed or flexible array | int[10], char[] |
FunctionPointer |
Function pointer | void (*)(int, char*) |
Types compose naturally:
from headerkit import CType, Pointer, Array
# const char*
const_char_ptr = Pointer(CType("char", ["const"]))
# int**
int_ptr_ptr = Pointer(Pointer(CType("int")))
# const char*[]
string_array = Array(Pointer(CType("char", ["const"])))
Declarations¶
Declarations (Declaration) represent top-level C/C++ constructs:
classDiagram
class Declaration {
<<protocol>>
name: str | None
location: SourceLocation | None
}
class Struct {
fields: list[Field]
is_union: bool
is_typedef: bool
}
class Enum {
values: list[EnumValue]
is_typedef: bool
}
class Function {
return_type: TypeExpr
parameters: list[Parameter]
is_variadic: bool
}
class Typedef {
underlying_type: TypeExpr
}
class Variable {
type: TypeExpr
}
class Constant {
value: int | str | None
is_macro: bool
}
Declaration <|-- Struct
Declaration <|-- Enum
Declaration <|-- Function
Declaration <|-- Typedef
Declaration <|-- Variable
Declaration <|-- Constant
| Class | Represents |
|---|---|
Struct |
Structs, unions, and C++ classes |
Enum |
Enumerations with named constants |
Function |
Function prototypes |
Typedef |
Type aliases |
Variable |
Global/extern variables |
Constant |
#define macros and const values |
The SourceUnit Container¶
SourceUnit (with backward-compatible alias Header) is the top-level container returned by all backends:
from headerkit.ir import SourceUnit
# SourceUnit fields:
# path: str -- original file path or synthetic name
# declarations: list[Declaration] -- all extracted declarations
# included_headers: set[str] -- basenames of included headers
# language: str -- source language (e.g., "c", "cpp")
# classification: str -- classification (e.g., "header", "source")
Layer 3: Writers (Output)¶
Writers convert SourceUnit IR into code, definitions, or packages. Concrete writers inherit from BaseWriter (which satisfies the underlying WriterBackend protocol).
BaseWriter unifies single-string rendering (write()) and multi-file package scaffolding (write_layout()):
from headerkit.ir import SourceUnit
from headerkit.scaffold import ProjectLayout, ScaffoldOptions
from headerkit.writers import BaseWriter, WriterOption
class MyWriter(BaseWriter):
name: str = "mywriter"
format_description: str = "My custom bindings"
def _render(self, unit: SourceUnit) -> str:
# Generate the primary output string
...
Writers declare their supported layout modes (supported_layouts) and configuration options (supported_options). Writer-specific options are passed to the constructor or via --writer-opt.
Built-in Writers¶
| Writer | Registry Name | Output | Primary Options |
|---|---|---|---|
CffiWriter |
cffi (default) |
CFFI cdef strings |
exclude_patterns: list[str] \| None |
CshimWriter |
cshim |
Pure C-ABI (extern "C") wrappers for C++ |
wrapper_header_name: str, catch_exceptions: bool |
CtypesWriter |
ctypes |
Python ctypes binding modules | lib_name: str |
CythonWriter |
cython |
Cython .pxd declarations |
-- |
DiffWriter |
diff |
API compatibility diff reports (JSON or Markdown) | baseline: SourceUnit \| None, format: str |
JsonWriter |
json |
JSON serialization of IR | indent: int \| None |
LuaWriter |
lua |
LuaJIT FFI bindings | -- |
MojoWriter |
mojo |
Idiomatic Mojo FFI bindings (sys.ffi.DLHandle) |
lib_name: str |
NimWriter |
nim |
Native Nim modules with {.importc.} pragmas |
header_file: str, cdecl: bool |
PromptWriter |
prompt |
Token-optimized output for LLM context | verbosity: str |
Writer Registry¶
Writers use the same registry pattern as backends:
from headerkit.writers import register_writer
register_writer("mywriter", MyWriterClass, description="My custom output format")
Registry functions:
| Function | Description |
|---|---|
get_writer(name=None, **kwargs) |
Get a writer instance; kwargs forwarded to constructor |
list_writers() |
List all registered writer names |
is_writer_available(name) |
Check if a writer is registered |
register_writer(name, cls) |
Register a new writer |
get_writer_info() |
Get metadata for all writers |
See Writing Custom Writers for a complete guide.
Design Principles¶
Parser-agnostic IR. The IR does not leak backend-specific details. A Struct from libclang looks exactly the same as a Struct from any other backend. This means writers work identically regardless of which backend produced the IR.
Composable types. Type expressions are recursive dataclasses that mirror how C types actually compose. const char** is Pointer(Pointer(CType("char", ["const"]))) -- no string parsing needed.
Best-effort output. Writers silently skip declarations they cannot represent rather than raising exceptions. This makes the pipeline robust against headers with exotic constructs.
Self-registering plugins. Both backends and writers register themselves at import time. Adding a new backend or writer requires zero changes to headerkit's core code. Just implement the protocol, call register_backend() or register_writer(), and your plugin is available through get_backend() or get_writer().