# 58. `ctypes`

# 58. `ctypes`

The `ctypes` module is CPython’s built-in foreign function interface. It lets Python code load shared libraries, call C functions, define C-compatible data types, pass pointers, receive callbacks from native code, and inspect or mutate raw memory.

`ctypes` is part of the standard library, but it sits very close to the C ABI. It can cross the normal safety boundary of Python. Correct use requires understanding memory layout, calling conventions, object lifetime, pointer ownership, and platform-specific binary interfaces.

## 58.1 The Role of `ctypes`

`ctypes` answers one central question:

```text
How can Python call functions from a native shared library without writing a C extension module?
```

Example:

```python
import ctypes

libc = ctypes.CDLL(None)
```

On Unix-like systems, `CDLL(None)` loads the current process image, which usually exposes symbols from the C runtime and already-loaded shared libraries.

A more explicit example:

```python
import ctypes

libm = ctypes.CDLL("libm.so.6")
libm.cos.restype = ctypes.c_double
libm.cos.argtypes = [ctypes.c_double]

print(libm.cos(0.0))
```

`ctypes` is useful for:

```text
calling small C libraries
prototyping extension boundaries
binding system APIs
loading plugins
testing native ABI behavior
working with memory buffers
interacting with legacy shared libraries
```

It is less suitable for large, performance-critical bindings where a C extension, Cython, cffi, pybind11, or a generated binding layer may be clearer and safer.

## 58.2 Shared Libraries

A shared library is a binary file loaded into a process at runtime.

Common formats:

| Platform | Format | Typical suffix |
|---|---|---|
| Linux | ELF shared object | `.so` |
| macOS | Mach-O dynamic library | `.dylib` |
| Windows | Dynamic-link library | `.dll` |

`ctypes` loads libraries through loader objects:

| Loader | Calling convention |
|---|---|
| `ctypes.CDLL` | C calling convention |
| `ctypes.PyDLL` | C calling convention, keeps GIL and checks Python errors |
| `ctypes.WinDLL` | Windows stdcall convention |
| `ctypes.OleDLL` | Windows stdcall with HRESULT handling |

Example:

```python
import ctypes

lib = ctypes.CDLL("/usr/lib/libexample.so")
```

After loading, exported symbols can be accessed as attributes:

```python
func = lib.some_function
```

or by indexing:

```python
func = lib["some_function"]
```

The result is a callable foreign function object.

## 58.3 Function Signatures

A C function has a binary calling contract. Python must know how to convert arguments and return values.

`ctypes` uses two attributes:

```python
func.argtypes
func.restype
```

Example C function:

```c
double hypot(double x, double y);
```

Python binding:

```python
import ctypes

libm = ctypes.CDLL("libm.so.6")

libm.hypot.argtypes = [ctypes.c_double, ctypes.c_double]
libm.hypot.restype = ctypes.c_double

print(libm.hypot(3.0, 4.0))
```

Without `argtypes`, `ctypes` guesses conversions. That can work for simple integers, but it is unsafe for pointers, floating-point values, structs, and platform-sensitive types.

A disciplined binding sets both `argtypes` and `restype` for every function.

## 58.4 Scalar C Types

`ctypes` provides Python classes for C scalar types.

| C type | `ctypes` type |
|---|---|
| `char` | `c_char` |
| `signed char` | `c_byte` |
| `unsigned char` | `c_ubyte` |
| `short` | `c_short` |
| `unsigned short` | `c_ushort` |
| `int` | `c_int` |
| `unsigned int` | `c_uint` |
| `long` | `c_long` |
| `unsigned long` | `c_ulong` |
| `long long` | `c_longlong` |
| `unsigned long long` | `c_ulonglong` |
| `float` | `c_float` |
| `double` | `c_double` |
| `void *` | `c_void_p` |
| `char *` | `c_char_p` |
| `wchar_t *` | `c_wchar_p` |

Example:

```python
import ctypes

x = ctypes.c_int(42)

print(x)
print(x.value)
```

`ctypes` scalar objects are mutable boxes around C-compatible storage:

```python
x.value = 100
```

This differs from normal Python integers, which are immutable Python objects.

## 58.5 Python Values to C Values

When calling a foreign function, `ctypes` converts Python objects into C-compatible values.

Example:

```python
import ctypes

libc = ctypes.CDLL(None)

libc.abs.argtypes = [ctypes.c_int]
libc.abs.restype = ctypes.c_int

print(libc.abs(-10))
```

The Python integer `-10` is converted to a C `int`.

If the Python value does not fit the target type, behavior can be surprising. Always choose the C type that exactly matches the library header.

For example, `size_t` should usually be represented as:

```python
ctypes.c_size_t
```

not `c_int`.

Pointer-sized integer types should use:

```python
ctypes.c_void_p
ctypes.c_size_t
ctypes.c_ssize_t
```

depending on meaning.

## 58.6 Return Types

`restype` controls how native return values are converted back to Python.

Example:

```python
func.restype = ctypes.c_int
```

means the raw C return value is interpreted as `int`.

For a pointer-returning function:

```python
func.restype = ctypes.c_void_p
```

For a string pointer:

```python
func.restype = ctypes.c_char_p
```

Care is required with `c_char_p`. It converts a `char *` return value to Python `bytes` by reading until a null byte. That is correct for borrowed null-terminated strings, but wrong for arbitrary binary buffers.

For raw addresses, use `c_void_p`.

## 58.7 Pointers

`ctypes` supports pointer types.

```python
import ctypes

IntPtr = ctypes.POINTER(ctypes.c_int)

x = ctypes.c_int(42)
p = ctypes.pointer(x)

print(p.contents.value)
```

`pointer(x)` creates a pointer object pointing to `x`.

`byref(x)` passes a lightweight reference suitable for function calls:

```python
lib.some_func(ctypes.byref(x))
```

Use `byref()` for output parameters when the pointer does not need to live independently.

Conceptually:

```text
ctypes.c_int object
    owns C-sized storage

ctypes.byref(object)
    temporary pointer for call

ctypes.pointer(object)
    real pointer object keeping referent alive
```

## 58.8 Output Parameters

Many C APIs return data through pointer arguments.

C shape:

```c
int parse(const char *text, int *out_value);
```

Python binding:

```python
import ctypes

# lib.parse.argtypes = [ctypes.c_char_p, ctypes.POINTER(ctypes.c_int)]
# lib.parse.restype = ctypes.c_int

out = ctypes.c_int()

# rc = lib.parse(b"123", ctypes.byref(out))
# if rc == 0:
#     print(out.value)
```

The Python object owns storage for `out`. The C function writes into that storage through the pointer.

This pattern is common in system APIs.

## 58.9 Arrays

`ctypes` can create fixed-size C arrays.

```python
import ctypes

IntArray4 = ctypes.c_int * 4

arr = IntArray4(1, 2, 3, 4)

print(arr[0])
print(arr[3])
```

The array stores contiguous C values, not Python object references.

Memory shape:

```text
int[4]
+------+------+------+------+
|  1   |  2   |  3   |  4   |
+------+------+------+------+
```

Arrays can be passed to C functions expecting pointers to their first element.

## 58.10 Structures

C structs are modeled with `ctypes.Structure`.

C shape:

```c
struct Point {
    int x;
    int y;
};
```

Python shape:

```python
import ctypes

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

p = Point(3, 4)

print(p.x)
print(p.y)
```

The `_fields_` list defines memory layout.

Ordering matters. Field types matter. Platform ABI rules matter.

You can inspect layout:

```python
print(ctypes.sizeof(Point))
print(Point.x.offset)
print(Point.y.offset)
```

The offsets should match the C compiler’s struct layout.

## 58.11 Padding and Alignment

C structs may contain padding bytes for alignment.

Example:

```python
import ctypes

class Example(ctypes.Structure):
    _fields_ = [
        ("a", ctypes.c_char),
        ("b", ctypes.c_int),
    ]

print(ctypes.sizeof(Example))
print(Example.a.offset)
print(Example.b.offset)
```

Even though `char` is 1 byte and `int` is often 4 bytes, the struct may be 8 bytes or more because `b` must be aligned.

Conceptual layout on a common ABI:

```text
offset 0: char a
offset 1: padding
offset 2: padding
offset 3: padding
offset 4: int b
```

Never guess struct layout. Match the C header and verify with `sizeof` and offsets when binding native libraries.

## 58.12 Packed Structures

Some C APIs use packed structs.

`ctypes` supports `_pack_`.

```python
import ctypes

class Packed(ctypes.Structure):
    _pack_ = 1
    _fields_ = [
        ("a", ctypes.c_char),
        ("b", ctypes.c_int),
    ]
```

Packing changes alignment and field offsets.

Use `_pack_` only when the C header explicitly specifies packed layout, such as with compiler pragmas or attributes.

Incorrect packing is a common cause of memory corruption.

## 58.13 Unions

C unions are modeled with `ctypes.Union`.

```python
import ctypes

class Number(ctypes.Union):
    _fields_ = [
        ("i", ctypes.c_int),
        ("d", ctypes.c_double),
    ]

n = Number()
n.i = 10
print(n.i)
```

All fields share the same memory.

Conceptually:

```text
same bytes interpreted as int or double
```

Unions are common in system APIs, binary formats, and tagged data structures. They must be paired with correct tag logic at the Python layer.

## 58.14 String Pointers

`c_char_p` represents `char *` interpreted as a null-terminated byte string.

```python
import ctypes

s = ctypes.c_char_p(b"hello")

print(s.value)
```

For a function expecting `const char *`, `c_char_p` is often correct.

For writable buffers, use `create_string_buffer()`:

```python
import ctypes

buf = ctypes.create_string_buffer(100)

print(ctypes.sizeof(buf))
```

This allocates mutable C memory.

Example:

```python
buf.value = b"hello"
print(buf.raw)
```

Distinguish carefully:

| Type | Meaning |
|---|---|
| `c_char_p` | Pointer to null-terminated bytes |
| `create_string_buffer(n)` | Owned mutable char array |
| `c_void_p` | Raw pointer address |
| `POINTER(c_char)` | Pointer to char storage |

## 58.15 Buffers

`ctypes` can work with raw memory buffers.

```python
import ctypes

buf = ctypes.create_string_buffer(b"abc")

print(buf.value)
print(buf.raw)
```

`.value` reads until the first null byte.

`.raw` returns the entire buffer.

For binary data, prefer `.raw` because null bytes may appear inside the data.

```python
buf = ctypes.create_string_buffer(b"a\x00b", 3)

print(buf.value)
print(buf.raw)
```

Output shape:

```text
b'a'
b'a\x00b'
```

## 58.16 Memory Addresses

`ctypes.addressof()` returns the memory address of a ctypes object.

```python
import ctypes

x = ctypes.c_int(42)

addr = ctypes.addressof(x)
print(hex(addr))
```

You can cast addresses to pointer types:

```python
p = ctypes.cast(addr, ctypes.POINTER(ctypes.c_int))
print(p.contents.value)
```

This is powerful and unsafe.

The address is only valid while the underlying object remains alive and unmoved. CPython does not move objects, but the `ctypes` storage object must still be kept alive by Python references.

## 58.17 Casting

`ctypes.cast()` reinterprets a pointer as another pointer type.

```python
import ctypes

x = ctypes.c_int(0x41424344)

p = ctypes.pointer(x)
q = ctypes.cast(p, ctypes.POINTER(ctypes.c_ubyte))

print(q[0])
```

This is equivalent to C-style pointer reinterpretation.

It does not convert data. It changes how the same bytes are interpreted.

Incorrect casts can violate alignment, size, lifetime, or aliasing expectations of the native code.

## 58.18 Callbacks

C libraries sometimes call function pointers supplied by the caller. `ctypes` can create C-callable callbacks from Python functions.

Example shape:

```python
import ctypes

CALLBACK = ctypes.CFUNCTYPE(ctypes.c_int, ctypes.c_int)

@CALLBACK
def py_callback(x):
    return x + 1
```

The callback object must be kept alive while C may call it.

Bad:

```python
lib.register_callback(CALLBACK(lambda x: x + 1))
```

If Python garbage collects the callback object, C may later call a dangling function pointer.

Good:

```python
callback = CALLBACK(py_callback)
lib.register_callback(callback)

# Keep callback referenced for as long as native code may use it.
```

Callbacks cross from C into Python, so they must reacquire interpreter state and interact with the GIL.

## 58.19 Error Handling

C APIs often signal errors through return values and global error state.

Unix-style APIs may set `errno`.

Windows APIs may set `GetLastError()`.

`ctypes` supports this with loader options:

```python
lib = ctypes.CDLL("libc.so.6", use_errno=True)
```

Then:

```python
err = ctypes.get_errno()
```

For Windows:

```python
lib = ctypes.WinDLL("kernel32", use_last_error=True)
err = ctypes.get_last_error()
```

A robust binding wraps native calls and converts errors into Python exceptions.

Example pattern:

```python
def check_zero(result, func, args):
    if result == 0:
        err = ctypes.get_errno()
        raise OSError(err)
    return result

# func.errcheck = check_zero
```

`errcheck` lets you attach result validation to a foreign function.

## 58.20 `errcheck`

`errcheck` is called after a foreign function returns.

```python
def check_negative(result, func, args):
    if result < 0:
        raise OSError("native call failed")
    return result

# lib.some_call.errcheck = check_negative
```

It receives:

```text
result
function object
arguments
```

It may return a transformed result or raise an exception.

This is useful for centralizing error conversion and output parameter handling.

## 58.21 Ownership

Ownership is the hardest part of `ctypes`.

A C function may return:

```text
borrowed pointer
newly allocated pointer
pointer to static storage
pointer owned by caller-provided buffer
pointer valid only until next call
```

The Python binding must know which case applies.

Example C API:

```c
char *make_message(void);
void free_message(char *);
```

Python binding must call the matching free function:

```python
ptr = lib.make_message()
try:
    ...
finally:
    lib.free_message(ptr)
```

Do not free memory unless the library contract says you own it. Do not ignore ownership when the contract says you must free it.

Mismatched allocation and freeing across runtimes can crash the process.

## 58.22 Lifetime

Python references must keep `ctypes` objects alive while native code uses their memory.

Example:

```python
import ctypes

def make_pointer():
    x = ctypes.c_int(42)
    return ctypes.pointer(x)

p = make_pointer()
print(p.contents.value)
```

This works because the pointer object keeps the referent alive.

But raw addresses do not:

```python
def make_address():
    x = ctypes.c_int(42)
    return ctypes.addressof(x)

addr = make_address()
```

After the function returns, `x` may be gone. The address is invalid.

The same applies to buffers, arrays, structs, and callbacks. Keep Python owner objects alive for as long as C might access their storage.

## 58.23 The GIL

Foreign calls through `ctypes.CDLL` normally release the GIL during the call. This lets other Python threads run while native code executes.

`ctypes.PyDLL` does not release the GIL and checks for Python exceptions after the call. It is intended for calling Python C API functions.

Use `PyDLL` when calling functions that require the GIL, especially CPython API functions.

Example concept:

```python
pythonapi = ctypes.PyDLL(None)
```

Many CPython C API functions are exposed through:

```python
ctypes.pythonapi
```

Calling CPython C API functions through `ctypes` is possible, but dangerous. Incorrect signatures or reference ownership mistakes can crash the interpreter.

## 58.24 `ctypes.pythonapi`

`ctypes.pythonapi` exposes the Python C API from the running process.

Example:

```python
import ctypes

ctypes.pythonapi.Py_GetVersion.restype = ctypes.c_char_p

print(ctypes.pythonapi.Py_GetVersion())
```

This calls a CPython C API function.

This is useful for experiments and diagnostics, but should not replace proper extension modules for serious C API integration.

Problems include:

```text
reference ownership mistakes
wrong signatures
GIL assumptions
ABI changes
borrowed references
object lifetime bugs
process crashes
```

## 58.25 Calling Conventions

A calling convention defines how arguments are passed, who cleans the stack, and how return values are delivered.

On many Unix-like platforms, ordinary C calls use one dominant ABI per architecture.

On Windows, different APIs may use different conventions.

`ctypes` separates these with loaders:

```python
ctypes.CDLL
ctypes.WinDLL
ctypes.OleDLL
```

Using the wrong calling convention can corrupt the stack or crash the process.

## 58.26 Platform Sensitivity

`ctypes` code is often platform-sensitive.

Differences include:

```text
library names
symbol names
integer sizes
long size
pointer size
struct alignment
calling convention
errno behavior
wide character representation
dynamic loader rules
```

Example:

```python
ctypes.c_long
```

has different size on common 64-bit Unix and 64-bit Windows ABIs.

Use fixed-width or semantic types where possible:

```python
ctypes.c_int32
ctypes.c_uint64
ctypes.c_size_t
ctypes.c_void_p
```

## 58.27 `ctypes` and the Buffer Protocol

`ctypes` arrays and buffers can often interoperate with Python’s buffer protocol.

Example:

```python
import ctypes

arr = (ctypes.c_ubyte * 4)(1, 2, 3, 4)

view = memoryview(arr)
print(view.tolist())
```

This is useful for binary data, I/O buffers, and integration with libraries that accept buffer-like objects.

The buffer protocol provides a safer alternative to raw pointer manipulation when possible.

## 58.28 Relationship to C Extensions

`ctypes` and C extensions solve related but different problems.

| Aspect | `ctypes` | C extension |
|---|---|---|
| Build step | Usually none | Required |
| Performance | Function call overhead | Lower overhead possible |
| Safety | Runtime signature mistakes | Compile-time checking possible |
| API shape | Foreign C ABI | Native Python C API |
| Best for | Small bindings, system calls, prototypes | Large integrations, performance-sensitive code |

`ctypes` calls existing shared libraries. A C extension creates a Python module compiled specifically for CPython.

## 58.29 Relationship to `struct`

`struct` packs and unpacks bytes according to C-like layouts.

`ctypes` creates live C-compatible memory objects.

Use `struct` for binary serialization:

```python
import struct

data = struct.pack("<I", 42)
value = struct.unpack("<I", data)[0]
```

Use `ctypes` for ABI interaction:

```python
import ctypes

x = ctypes.c_uint32(42)
```

They overlap conceptually, but the intended uses differ.

## 58.30 Common Failure Modes

`ctypes` failures can be severe.

| Mistake | Result |
|---|---|
| Wrong `argtypes` | Corrupted arguments |
| Wrong `restype` | Corrupted return value |
| Wrong struct layout | Native code reads wrong memory |
| Missing callback reference | Dangling function pointer |
| Freeing borrowed memory | Crash |
| Not freeing owned memory | Leak |
| Using invalid address | Crash or data corruption |
| Calling convention mismatch | Stack corruption |
| GIL misuse with Python C API | Undefined interpreter behavior |

Unlike normal Python errors, many `ctypes` mistakes can terminate the process.

## 58.31 Defensive Binding Pattern

A safer `ctypes` binding follows a strict pattern.

```python
import ctypes

class NativeError(RuntimeError):
    pass

lib = ctypes.CDLL("libexample.so", use_errno=True)

lib.example_open.argtypes = [ctypes.c_char_p]
lib.example_open.restype = ctypes.c_void_p

lib.example_close.argtypes = [ctypes.c_void_p]
lib.example_close.restype = None

def _check_ptr(ptr):
    if not ptr:
        err = ctypes.get_errno()
        raise OSError(err, "example_open failed")
    return ptr

class Handle:
    def __init__(self, path: str):
        raw = lib.example_open(path.encode("utf-8"))
        self._ptr = _check_ptr(raw)

    def close(self):
        if self._ptr:
            lib.example_close(self._ptr)
            self._ptr = None

    def __enter__(self):
        return self

    def __exit__(self, exc_type, exc, tb):
        self.close()

    def __del__(self):
        self.close()
```

The important ideas:

```text
declare signatures once
wrap raw pointers
centralize error checking
model ownership explicitly
provide close()
support context managers
avoid exposing raw addresses
```

## 58.32 Why `ctypes` Matters for CPython Internals

`ctypes` matters because it exposes the boundary between Python objects and native memory.

It shows how CPython can:

```text
load shared libraries
convert Python values to C ABI values
represent C structs and arrays
pass pointers
call native functions
receive native callbacks
interact with platform loaders
call parts of the Python C API
```

It is also a reminder that CPython’s safety guarantees end at the foreign function boundary. Once Python code passes raw addresses to native code, memory safety depends on the external ABI contract.

## 58.33 Chapter Summary

The `ctypes` module is CPython’s standard foreign function interface. It can load shared libraries, call C functions, define C-compatible types, pass pointers, create callbacks, and manipulate raw memory.

For CPython internals, `ctypes` is important because it exposes the relationship between Python objects, native ABI values, dynamic libraries, C memory layout, callbacks, the GIL, and the Python C API. It is powerful, but incorrect signatures, ownership mistakes, and lifetime bugs can crash the interpreter.
