Skip to content

ctypes Writer

The ctypes writer generates Python source code that uses the ctypes standard library to define C type bindings. The output is a runnable Python module containing struct/union classes, enum constants, type aliases, callback types, and function prototype annotations.

Writer Class

CtypesWriter

CtypesWriter(lib_name='_lib')

Bases: BaseWriter

Writer that generates Python ctypes binding modules from headerkit IR.

Options

lib_name : str Variable name for the loaded library object. Defaults to "_lib". Controls the variable name used in function prototype annotations (e.g., _lib.func.argtypes = [...]). library : str Base name of the native library a scaffolded package loads, without a lib prefix or a platform suffix -- what ctypes.util.find_library is given. This is not lib_name, which names a Python variable. Defaults to the package name, which is right only when the two happen to coincide: scaffolding probe_bindings around libprobe needs library="probe" or the generated module cannot find its binary.

Example

::

from headerkit.writers import get_writer

writer = get_writer("ctypes", lib_name="mylib")
source = writer.write(header)

# Or directly:
from headerkit.writers.ctypes import CtypesWriter
writer = CtypesWriter(lib_name="_lib")
source = writer.write(header)

write

write(header)

Convert header IR to Python ctypes binding source code.

hash_comment_format

hash_comment_format()

Return format string for wrapping TOML cache metadata in Python comments.

Convenience Function

header_to_ctypes

header_to_ctypes(header, lib_name='_lib', *, library=None)

Convert all declarations in a Header to a Python ctypes module string.

Parameters:

Name Type Description Default
header Header

Parsed header IR from headerkit.

required
lib_name str

Variable name for the loaded library object. Used in function prototype annotations (e.g., _lib.func.argtypes = [...]).

'_lib'
library str | None

Base name of the native library to load. When given, the module defines lib_name itself and binds each function to a module-level name, so the result is importable and callable on its own. When omitted the output stays a fragment that expects the caller to supply lib_name -- which is what the single-file writer emits.

None

Returns:

Type Description
str

A string of Python source code defining ctypes bindings.

Low-Level Functions

These functions are used internally by header_to_ctypes and can be useful when working with individual type expressions.

type_to_ctypes

type_to_ctypes(t, types=_EMPTY_TYPES)

Convert a type expression to its ctypes string representation.

Handles special cases like const char * -> ctypes.c_char_p, void * -> ctypes.c_void_p, and pointer/array composition.

Parameters:

Name Type Description Default
types _TypeTable

Every spelling this header uses for an enum type, from :func:_enum_type_names. An enum binds no ctypes class, so a use of one has to resolve to :data:ENUM_CTYPE here rather than to the C name. The tag spelling enum E is not valid Python at all, and the alias spelling E names something the module may not have assigned yet -- a Typedef renders into a section emitted after the records that use it. Resolving at the use site is what makes the result independent of both the spelling the backend chose and the order of the sections.

_EMPTY_TYPES

Example

from headerkit.backends import get_backend
from headerkit.writers import get_writer

backend = get_backend()
header = backend.parse("""
typedef struct {
    int x;
    int y;
} Point;

int distance(Point* a, Point* b);
""", "geometry.h")

writer = get_writer("ctypes", lib_name="_geometry")
print(writer.write(header))

Output:

"""ctypes bindings generated from geometry.h."""

import ctypes
import ctypes.util
import sys

# ============================================================
# Structures and Unions
# ============================================================

class Point(ctypes.Structure):
    _fields_ = [
        ("x", ctypes.c_int),
        ("y", ctypes.c_int),
    ]

# ============================================================
# Function Prototypes
# ============================================================

_geometry.distance.argtypes = [ctypes.POINTER(Point), ctypes.POINTER(Point)]
_geometry.distance.restype = ctypes.c_int

Packed records the writer could not reproduce

A packed record's C layout is not always expressible in ctypes. The writer records what C says each packed record looks like and emits a check that runs when the module is imported, comparing that against where ctypes actually put each field.

The check runs on import rather than at generation because ctypes has changed its bit-field layout across versions -- _pack_ selects the MSVC rules from CPython 3.14, where earlier versions used the System V ones -- so a verdict computed when the module was written is about the wrong interpreter as soon as somebody else imports it.

Two things mark an affected record. Its class carries a # HEADERKIT: comment if the generating interpreter already saw the problem, and the module defines:

#: Packed records whose layout this interpreter does not reproduce. Empty
#: when every one of them checks out; absent when the header had none.
HEADERKIT_UNVERIFIED_RECORDS = _hk_unverified_records()

The name is defined whenever the header held at least one packed record. An empty tuple means every one of them was verified on the importing interpreter; the name is absent only when there were no packed records at all. Read it with a default:

import mybindings

unverified = getattr(mybindings, "HEADERKIT_UNVERIFIED_RECORDS", ())
if unverified:
    raise SystemExit(f"layout not reproduced for: {', '.join(unverified)}")

from mybindings import HEADERKIT_UNVERIFIED_RECORDS raises ImportError on a module with no packed records. Absent means none.

The check also binds two private helpers, _HK_PACKED_EXPECTED and _hk_unverified_records. Both are renamed out of the way if the header declares something of the same name, so a declaration always wins. The public name cannot move -- consumers read it to decide whether the bindings are trustworthy -- so a header declaring HEADERKIT_UNVERIFIED_RECORDS is refused with an error rather than generated with the check under a different name.