Skip to main content

Modules/_ctypes/ (part 6)

Source:

cpython 3.14 @ ab2d84fe1023/Modules/_ctypes/_ctypes.c

This annotation covers ctypes utility functions and function calling. See modules_ctypes5_detail for ctypes.Structure, Union, Array, and pointer types.

Map

LinesSymbolRole
1-80ctypes.castReinterpret a ctypes instance as another type
81-160ctypes.byrefLightweight pointer (cannot outlive referent)
161-240ctypes.addressofGet address of ctypes instance as integer
241-340CDLL.__getattr__Lazy function lookup by name
341-600FuncPtr.__call__Build argument list, invoke FFI function

Reading

ctypes.cast

// CPython: Modules/_ctypes/_ctypes.c:4240 cast_impl
static PyObject *
cast_impl(PyObject *module, PyObject *obj, PyObject *type)
{
/* Cast obj to type: reinterpret the memory as type */
StgInfo *info = PyStgInfo_FromType(module_state, (PyObject *)Py_TYPE(type));
Py_buffer buffer;
if (PyCArg_CheckExact(obj)) {
/* byref object */
...
}
if (!CDataObject_Check(module_state, obj)) {
PyErr_SetString(PyExc_TypeError, "argument 1 must be a ctypes instance");
return NULL;
}
/* Create new instance at same address */
CDataObject *result = (CDataObject *)type->tp_alloc((PyTypeObject *)type, 0);
result->b_ptr = ((CDataObject *)obj)->b_ptr;
result->b_needsfree = 0;
return (PyObject *)result;
}

cast(ptr, POINTER(c_int)) creates a new ctypes instance pointing to the same memory as ptr but interpreted as a POINTER(c_int). No data is copied. The new instance does not own the memory.

ctypes.byref

// CPython: Modules/_ctypes/_ctypes.c:4160 byref_impl
static PyObject *
byref_impl(PyObject *module, CDataObject *obj, Py_ssize_t offset)
{
/* Create a lightweight pointer to obj's memory + offset */
PyCArgObject *arg = PyCArgObject_new(module_state);
arg->tag = 'P';
arg->pffi_type = &ffi_type_pointer;
arg->obj = Py_NewRef(obj);
arg->value.p = (char *)obj->b_ptr + offset;
return (PyObject *)arg;
}

byref(x) is equivalent to pointer(x) for function arguments but doesn't create a real ctypes pointer object. It holds a reference to x to keep the memory alive. Used for passing c_int by reference: func(byref(result)).

FuncPtr.__call__

// CPython: Modules/_ctypes/_ctypes.c:3860 PyCFuncPtr_call
static PyObject *
PyCFuncPtr_call(PyCFuncPtrObject *self, PyObject *inargs, PyObject *kwds)
{
/* Convert Python args to ctypes args */
PyObject *argtypes = self->argtypes;
for (int i = 0; i < nargs; i++) {
PyObject *arg = inargs[i];
if (argtypes) arg = convertor(argtypes[i], arg);
/* Store in ffi_args array */
}
/* Set up libffi cif */
ffi_call(&self->cif, self->b_ptr, &result, ffi_args);
/* Convert result back to Python object */
return GetResult(restype, &result, ...);
}

CDLL.func(1, 2) converts Python objects to C types using argtypes, calls ffi_call via libffi, then converts the result using restype. ffi_call handles the ABI details (calling convention, stack frame, register passing) for each platform.

gopy notes

ctypes.cast is module/ctypes.Cast in module/ctypes/module.go. It creates a new objects.CData sharing the same unsafe.Pointer. byref creates a objects.CArgRef. FuncPtr.__call__ uses Go's syscall.Syscall or CGo for the actual FFI invocation.