Writing Custom Writers¶
headerkit writers convert IR into output strings. headerkit ships with seven built-in writers (cffi, ctypes, cython, diff, json, lua, prompt), and you can create custom writers for any additional target format: documentation generators, language-specific bindings, or anything else you need.
The WriterBackend Protocol¶
Every writer must implement the WriterBackend protocol:
from headerkit.ir import Header
from headerkit.writers import WriterBackend
class WriterBackend(Protocol):
def write(self, header: Header) -> str: ...
@property
def name(self) -> str: ...
@property
def format_description(self) -> str: ...
Method and Property Details¶
write(header) -- Convert a Header IR object into the target format string. Writers should produce best-effort output, silently skipping declarations they cannot represent. Writers must not raise exceptions for valid Header input.
name -- A human-readable name for the writer (e.g., "markdown"). This is the string users pass to get_writer().
format_description -- A short description of the output format (e.g., "Markdown API documentation"). Used by get_writer_info().
Writer-Specific Options¶
Configuration options belong on the writer's __init__(), not on write(). This keeps the protocol simple and type-safe:
class MarkdownWriter:
def __init__(self, include_source_locations: bool = False) -> None:
self._include_locations = include_source_locations
def write(self, header: Header) -> str:
# Use self._include_locations here
...
Users pass options through get_writer():
Registering a Writer¶
Use register_writer() to add your writer to the global registry:
from headerkit.writers import register_writer
register_writer(
"markdown",
MarkdownWriter,
description="Markdown API documentation",
)
Parameters:
name-- The lookup key forget_writer(name)writer_class-- The class implementingWriterBackendis_default-- IfTrue, this becomes the default writerdescription-- Short description; falls back to the class docstring's first line if not provided
Unique names
register_writer() raises ValueError if a writer with the same name is already registered. Choose a unique name for your writer.
Complete Example: Markdown Documentation Writer¶
Here is a complete writer that generates Markdown documentation from a parsed C header:
"""Generate Markdown API documentation from headerkit IR."""
from __future__ import annotations
from headerkit.ir import (
Constant,
Declaration,
Enum,
Function,
Header,
Struct,
Typedef,
Variable,
)
from headerkit.writers import register_writer
class MarkdownWriter:
"""Writer that generates Markdown API documentation."""
def __init__(self, include_source_locations: bool = False) -> None:
self._include_locations = include_source_locations
def write(self, header: Header) -> str:
lines = [f"# API Reference: `{header.path}`", ""]
# Group declarations by kind
structs = [d for d in header.declarations if isinstance(d, Struct)]
enums = [d for d in header.declarations if isinstance(d, Enum)]
functions = [d for d in header.declarations if isinstance(d, Function)]
typedefs = [d for d in header.declarations if isinstance(d, Typedef)]
constants = [d for d in header.declarations if isinstance(d, Constant)]
if structs:
lines.append("## Structures")
lines.append("")
for s in structs:
lines.extend(self._format_struct(s))
if enums:
lines.append("## Enumerations")
lines.append("")
for e in enums:
lines.extend(self._format_enum(e))
if functions:
lines.append("## Functions")
lines.append("")
for f in functions:
lines.extend(self._format_function(f))
if typedefs:
lines.append("## Type Aliases")
lines.append("")
for t in typedefs:
lines.append(f"- `{t.name}` -- alias for `{t.underlying_type}`")
lines.append("")
if constants:
lines.append("## Constants")
lines.append("")
for c in constants:
if c.value is not None:
lines.append(f"- `{c.name}` = `{c.value}`")
else:
lines.append(f"- `{c.name}`")
lines.append("")
return "\n".join(lines)
def _format_struct(self, s: Struct) -> list[str]:
kind = "Union" if s.is_union else "Struct"
lines = [f"### `{s.name}` ({kind})", ""]
if s.fields:
lines.append("| Field | Type |")
lines.append("|-------|------|")
for field in s.fields:
lines.append(f"| `{field.name}` | `{field.type}` |")
else:
lines.append("*Opaque type*")
lines.append("")
return lines
def _format_enum(self, e: Enum) -> list[str]:
name = e.name or "(anonymous)"
lines = [f"### `{name}`", ""]
if e.values:
lines.append("| Constant | Value |")
lines.append("|----------|-------|")
for v in e.values:
val = str(v.value) if v.value is not None else "(auto)"
lines.append(f"| `{v.name}` | {val} |")
lines.append("")
return lines
def _format_function(self, f: Function) -> list[str]:
params = ", ".join(
f"{p.type} {p.name}" if p.name else str(p.type)
for p in f.parameters
)
if f.is_variadic:
params = f"{params}, ..." if params else "..."
lines = [
f"### `{f.name}`",
"",
f"```c",
f"{f.return_type} {f.name}({params});",
f"```",
"",
]
if self._include_locations and f.location:
lines.append(
f"*Defined at {f.location.file}:{f.location.line}*"
)
lines.append("")
return lines
@property
def name(self) -> str:
return "markdown"
@property
def format_description(self) -> str:
return "Markdown API documentation"
# Self-register
register_writer("markdown", MarkdownWriter, description="Markdown API documentation")
Using Your Writer¶
Once registered, your writer is available through the standard API:
from headerkit import get_backend, get_writer, list_writers
# List all available writers
print(list_writers()) # ['cffi', 'ctypes', 'cython', 'diff', 'json', 'lua', 'prompt', 'markdown']
# Use your writer
backend = get_backend()
header = backend.parse(code, "mylib.h")
writer = get_writer("markdown", include_source_locations=True)
docs = writer.write(header)
print(docs)
Handling IR Types¶
When writing a custom writer, you need to handle the various IR types. Here is a reference for the type-dispatch pattern:
from headerkit.ir import (
Array,
Constant,
CType,
Enum,
Function,
FunctionPointer,
Header,
Pointer,
Struct,
Typedef,
Variable,
)
def convert_type(t):
"""Convert a TypeExpr to your target format."""
if isinstance(t, CType):
# Base type: t.name, t.qualifiers
...
elif isinstance(t, Pointer):
# Pointer: t.pointee (recursive TypeExpr), t.qualifiers
inner = convert_type(t.pointee)
...
elif isinstance(t, Array):
# Array: t.element_type (TypeExpr), t.size (int | str | None)
elem = convert_type(t.element_type)
...
elif isinstance(t, FunctionPointer):
# Function pointer: t.return_type, t.parameters, t.is_variadic
...
def convert_declaration(decl):
"""Convert a Declaration to your target format."""
if isinstance(decl, Struct):
# decl.name, decl.fields, decl.is_union, decl.is_typedef
...
elif isinstance(decl, Enum):
# decl.name, decl.values (list of EnumValue)
...
elif isinstance(decl, Function):
# decl.name, decl.return_type, decl.parameters, decl.is_variadic
...
elif isinstance(decl, Typedef):
# decl.name, decl.underlying_type
...
elif isinstance(decl, Variable):
# decl.name, decl.type
...
elif isinstance(decl, Constant):
# decl.name, decl.value, decl.is_macro
...
Packaging as a Plugin¶
To distribute your writer as a separate package, register it in your package's __init__.py:
# mywriter/__init__.py
from headerkit.writers import register_writer
from mywriter.core import MarkdownWriter
register_writer("markdown", MarkdownWriter)
Users install your package and the writer becomes available: