From 03f2e20ff1c6d70619c0cf91dc644a7cf693c811 Mon Sep 17 00:00:00 2001 From: Alba Mendez Date: Tue, 24 Feb 2026 06:57:29 +0100 Subject: [PATCH] _cffi_backend introspection APIs + docs --- doc/source/ref.rst | 75 +++++++++-- doc/source/whatsnew.rst | 6 + src/c/_cffi_backend.c | 2 + src/c/ffi_obj.c | 284 ++++++++++++++++++++++++++++++++++++++++ 4 files changed, 359 insertions(+), 8 deletions(-) diff --git a/doc/source/ref.rst b/doc/source/ref.rst index efb63c16..24b880f3 100644 --- a/doc/source/ref.rst +++ b/doc/source/ref.rst @@ -406,11 +406,32 @@ ffi.CData, ffi.CType as ```` and ```` in the rest of this document. Note that some cdata objects may be actually of a subclass of ``ffi.CData``, and similarly with ctype, so you should check with -``if isinstance(x, ffi.CData)``. Also, ```` objects have -a number of attributes for introspection: ``kind`` and ``cname`` are -always present, and depending on the kind they may also have -``item``, ``length``, ``fields``, ``args``, ``result``, ``ellipsis``, -``abi``, ``elements`` and ``relements``. +``if isinstance(x, ffi.CData)``. + +```` objects have a number of attributes for introspection. +``kind`` (``enum``, ``primitive``, ``pointer``, ``array``, ``void``, +``function``, ``struct``, ``union``) and ``cname`` (normalized C type +string) are always present, and depending on the kind they may have +additional attributes: + +* ``enum``: attribute ``relements`` is a dictionary of enumerator names + to their numerical values, and ``elements`` maps numerical values to + the name of the first defined enumerator with that value. + +* ``pointer``, ``array``: attribute ``item`` is the base (dereferenced) + ````, and for arrays, attribute ``length`` is the array length + in elements (``None`` if dynamic, e.g. ``T[]``). + +* ``function`` describes a function pointer: attribute ``result`` is the + return ````, ``args`` is a tuple of argument ```` objects, + ``ellipsis`` is a boolean indicating whether the function is variadic, + i.e. ends with an ellipsis (``...``), and ``abi`` is a numerical value + indicating the calling convention. + +* ``struct``, ``union`` describe a composite type: attribute ``fields`` + is a list of ``(name, )`` pairs, or ``None`` if the type is + incomplete. CField objects have attributes ``type`` (contained + ````), ``offset``, ``bitshift``, ``bitsize`` and ``flags``. *New in version 1.10:* ``ffi.buffer`` is now `a type`__ as well. @@ -745,11 +766,45 @@ example:: .. __: https://foss.heptapod.net/pypy/cffi/-/issues/233 +.. _ffi-includes: +.. _ffi-list-globals: + +ffi.includes, ffi.list_globals() +++++++++++++++++++++++++++++++++ + +**ffi.includes**: Returns a tuple of directly included FFI instances. +*New in version 2.0.0dev.* + +**ffi.list_globals()**: Returns a list of globals known to this FFI +instance, sorted by name. This allows introspecting the API that a +loaded library object would have (assuming the library provides all +symbols) without needing one. *New in version 2.0.0dev.* + +Note that only the globals directly defined in this FFI instance are +returned; to get globals inherited from included FFI instances, call +``list_globals()`` on them. Note that when resolving symbols cffi currently +applies a 100-level recursion limit for safety, but this may change in +the future. Each item in the list is a ``CGlobal`` object, with at least +fields ``name`` (Python attribute name) and ``kind``. Depending on the +value of ``kind``, additional attributes may be available: + +* ``enum``, ``int_constant``: an extra ``value`` attribute returns the + numerical value of the attribute. These attributes are also available + on the FFI instance, as their value does not depend on the loaded + library. + +* ``constant``, ``variable``, ``function``, ``python_function``: an extra + ``type`` attribute is available, which is the ```` of the global. + For ``function`` and ``python_function``, ``type`` is guaranteed to be + of kind ``function``, i.e. a function pointer. + + .. _ffi-getctype: .. _ffi-list-types: +.. _ffi-list-enums: -ffi.getctype(), ffi.list_types() -++++++++++++++++++++++++++++++++ +ffi.getctype(), ffi.list_types(), ffi.list_enums() +++++++++++++++++++++++++++++++++++++++++++++++++++ **ffi.getctype("C type" or , extra="")**: return the string representation of the given C type. If non-empty, the "extra" string is @@ -762,9 +817,13 @@ of the C type "pointer to the same type than x"; and **ffi.list_types()**: Returns the user type names known to this FFI instance. This returns a tuple containing three lists of names: -``(typedef_names, names_of_structs, names_of_unions)``. *New in +``(typedef_names, names_of_structs, names_of_unions)``. For historical +reasons enums are not included, see ``ffi.list_enums()``. *New in version 1.6.* +**ffi.list_enums()**: Returns the list of enum type names known to this +FFI instance. *New in version 2.0.0dev.* + .. _`Preparing and Distributing modules`: cdef.html#loading-libraries diff --git a/doc/source/whatsnew.rst b/doc/source/whatsnew.rst index db9352d0..51ed3b74 100644 --- a/doc/source/whatsnew.rst +++ b/doc/source/whatsnew.rst @@ -10,10 +10,16 @@ v2.0.0.dev0 with the limited API, so you must set py_limited_api=False when building extensions for the free-threaded build. * Added support for Python 3.14. (`#177`_) +* `ffi.includes`_ +* `ffi.list_globals()`_ +* `ffi.list_enums()`_ * WIP .. _`#177`: https://github.com/python-cffi/cffi/pull/177 .. _`#178`: https://github.com/python-cffi/cffi/pull/178 +.. _`ffi.includes`: ref.html#ffi-includes +.. _`ffi.list_globals()`: ref.html#ffi-list-globals +.. _`ffi.list_enums()`: ref.html#ffi-list-enums v1.17.1 ======= diff --git a/src/c/_cffi_backend.c b/src/c/_cffi_backend.c index 6087e635..37cabf57 100644 --- a/src/c/_cffi_backend.c +++ b/src/c/_cffi_backend.c @@ -287,6 +287,7 @@ typedef struct cfieldobject_s { #define BS_EMPTY_ARRAY (-2) /* a field declared 'type[0]' or 'type[]' */ #define BF_IGNORE_IN_CTOR 0x01 /* union field not in the first place */ +static PyTypeObject CGlobalDescr_Type; static PyTypeObject CTypeDescr_Type; static PyTypeObject CField_Type; static PyTypeObject CData_Type; @@ -7908,6 +7909,7 @@ PyInit__cffi_backend(void) &dl_type, &CTypeDescr_Type, &CField_Type, + &CGlobalDescr_Type, &CData_Type, &CDataOwning_Type, &CDataOwningGC_Type, diff --git a/src/c/ffi_obj.c b/src/c/ffi_obj.c index fbba2894..82aa9e4b 100644 --- a/src/c/ffi_obj.c +++ b/src/c/ffi_obj.c @@ -843,6 +843,17 @@ static PyObject *ffi_get_errno(PyObject *self, void *closure) return b_get_errno(NULL, NULL); } +PyDoc_STRVAR(ffi_includes_doc, "tuple of directly included FFI objects, if any"); + +static PyObject *ffi_get_includes(FFIObject *self, void *closure) +{ + PyObject *result = self->types_builder.included_ffis; + if (result == NULL) + return PyTuple_New(0); + Py_INCREF(result); + return result; +} + static int ffi_set_errno(PyObject *self, PyObject *newval, void *closure) { PyObject *x = b_set_errno(NULL, newval); @@ -894,6 +905,36 @@ static PyObject *ffi_int_const(FFIObject *self, PyObject *args, PyObject *kwds) return x; } +static PyObject *cglobaldescr_new(FFIObject *ffi, int index); /* forward */ + +PyDoc_STRVAR(ffi_list_globals_doc, +"Returns a list of globals known to this FFI instance, sorted by name.\n" +"This allows introspecting the API that a loaded library object would have\n" +"(assuming the library provides all symbols) without needing one."); + +static PyObject *ffi_list_globals(FFIObject *self, PyObject *noargs) +{ + Py_ssize_t i, n = self->types_builder.ctx.num_globals; + PyObject *o, *lst, *result = NULL; + + lst = PyList_New(n); + if (lst == NULL) + goto error; + + for (i = 0; i < n; i++) { + o = cglobaldescr_new(self, i); + if (o == NULL) + goto error; + PyList_SET_ITEM(lst, i, o); + } + result = lst; + lst = NULL; + /* fall-through */ + error: + Py_XDECREF(lst); + return result; +} + PyDoc_STRVAR(ffi_list_types_doc, "Returns the user type names known to this FFI instance.\n" "This returns a tuple containing three lists of names:\n" @@ -948,6 +989,33 @@ static PyObject *ffi_list_types(FFIObject *self, PyObject *noargs) return result; } +PyDoc_STRVAR(ffi_list_enums_doc, +"Returns the list of enum type names known to this FFI instance."); + +static PyObject *ffi_list_enums(FFIObject *self, PyObject *noargs) +{ + Py_ssize_t i, n = self->types_builder.ctx.num_enums; + PyObject *o, *lst, *result = NULL; + + lst = PyList_New(n); + if (lst == NULL) + goto error; + + for (i = 0; i < n; i++) { + o = PyUnicode_FromString(self->types_builder.ctx.enums[i].name); + if (o == NULL) + goto error; + PyList_SET_ITEM(lst, i, o); + } + + result = lst; + lst = NULL; + /* fall-through */ + error: + Py_XDECREF(lst); + return result; +} + PyDoc_STRVAR(ffi_memmove_doc, "ffi.memmove(dest, src, n) copies n bytes of memory from src to dest.\n" "\n" @@ -1116,7 +1184,9 @@ static PyMethodDef ffi_methods[] = { #endif {"init_once", (PyCFunction)ffi_init_once, METH_VKW, ffi_init_once_doc}, {"integer_const",(PyCFunction)ffi_int_const,METH_VKW, ffi_int_const_doc}, + {"list_globals",(PyCFunction)ffi_list_globals,METH_NOARGS,ffi_list_globals_doc}, {"list_types", (PyCFunction)ffi_list_types, METH_NOARGS, ffi_list_types_doc}, + {"list_enums", (PyCFunction)ffi_list_enums, METH_NOARGS, ffi_list_enums_doc}, {"memmove", (PyCFunction)ffi_memmove, METH_VKW, ffi_memmove_doc}, {"new", (PyCFunction)ffi_new, METH_VKW, ffi_new_doc}, {"new_allocator",(PyCFunction)ffi_new_allocator,METH_VKW,ffi_new_allocator_doc}, @@ -1132,6 +1202,7 @@ static PyMethodDef ffi_methods[] = { static PyGetSetDef ffi_getsets[] = { {"errno", ffi_get_errno, ffi_set_errno, ffi_errno_doc}, + {"includes", (getter)ffi_get_includes, NULL, ffi_includes_doc}, {NULL} }; @@ -1219,3 +1290,216 @@ _fetch_external_struct_or_union(const struct _cffi_struct_union_s *s, } return NULL; /* not found at all, leave without an error */ } + +/************************************************************/ + +typedef struct cglobaldescrobject_s { + PyObject_HEAD + FFIObject *ffi; + int index; + builder_c_t *types_builder; + const struct _cffi_global_s *global; +} CGlobalDescrObject; + +static PyObject * +cglobaldescr_new(FFIObject *ffi, int index) +{ + CGlobalDescrObject *cg = PyObject_GC_New(CGlobalDescrObject, + &CGlobalDescr_Type); + if (cg == NULL) + return NULL; + + Py_INCREF(ffi); + cg->ffi = ffi; + cg->types_builder = &ffi->types_builder; + cg->index = index; + cg->global = &ffi->types_builder.ctx.globals[index]; + PyObject_GC_Track(cg); + return (PyObject *) cg; +} + +static void +cglobaldescr_dealloc(CGlobalDescrObject *cg) +{ + PyObject_GC_UnTrack(cg); + Py_XDECREF(cg->ffi); + Py_TYPE(cg)->tp_free((PyObject *)cg); +} + +static int +cglobaldescr_traverse(CGlobalDescrObject *cg, visitproc visit, void *arg) +{ + Py_VISIT(cg->ffi); + return 0; +} + +static int +cglobaldescr_clear(CGlobalDescrObject *cg) +{ + Py_CLEAR(cg->ffi); + return 0; +} + +static PyObject *cglobalget_name(CGlobalDescrObject *cg, void *context) +{ + return PyUnicode_FromString(cg->global->name); +} + +static PyObject *cglobalget_kind(CGlobalDescrObject *cg, void *context) +{ + char *result; + + switch (_CFFI_GETOP(cg->global->type_op)) { + + case _CFFI_OP_CONSTANT_INT: + result = "int_constant"; + break; + case _CFFI_OP_ENUM: + result = "enum"; + break; + + case _CFFI_OP_CONSTANT: + case _CFFI_OP_DLOPEN_CONST: + result = "constant"; + break; + + case _CFFI_OP_GLOBAL_VAR: + case _CFFI_OP_GLOBAL_VAR_F: + result = "variable"; + break; + + case _CFFI_OP_DLOPEN_FUNC: + case _CFFI_OP_EXTERN_PYTHON: + result = "function"; + break; + + case _CFFI_OP_CPYTHON_BLTN_V: + case _CFFI_OP_CPYTHON_BLTN_N: + case _CFFI_OP_CPYTHON_BLTN_O: + result = "python_function"; + break; + + default: + result = "?"; + } + + return PyUnicode_FromString(result); +} + +static PyObject *cglobalget_type(CGlobalDescrObject *cg, void *context) +{ + PyObject *ft, *ct; + _cffi_opcode_t type_op = cg->global->type_op; + + switch (_CFFI_GETOP(type_op)) { + + case _CFFI_OP_CONSTANT: + case _CFFI_OP_DLOPEN_CONST: + case _CFFI_OP_GLOBAL_VAR: + case _CFFI_OP_GLOBAL_VAR_F: + return (PyObject *)realize_c_type(cg->types_builder, + cg->types_builder->ctx.types, + _CFFI_GETARG(type_op)); + + case _CFFI_OP_DLOPEN_FUNC: + case _CFFI_OP_EXTERN_PYTHON: + case _CFFI_OP_CPYTHON_BLTN_V: + case _CFFI_OP_CPYTHON_BLTN_N: + case _CFFI_OP_CPYTHON_BLTN_O: + ft = realize_c_type_or_func(cg->types_builder, + cg->types_builder->ctx.types, + _CFFI_GETARG(type_op)); + if (ft == NULL) + return NULL; + + ct = (PyObject *)unwrap_fn_as_fnptr(ft); + Py_INCREF(ct); + Py_DECREF(ft); + return ct; + } + return nosuchattr("type"); +} + +static PyObject *cglobalget_value(CGlobalDescrObject *cg, void *context) +{ + switch (_CFFI_GETOP(cg->global->type_op)) { + case _CFFI_OP_CONSTANT_INT: + case _CFFI_OP_ENUM: + return realize_global_int(cg->types_builder, cg->index); + } + return nosuchattr("value"); +} + +static PyGetSetDef cglobaldescr_getsets[] = { + {"name", (getter)cglobalget_name, NULL, "attribute name"}, + {"kind", (getter)cglobalget_kind, NULL, "kind"}, + {"type", (getter)cglobalget_type, NULL, "attribute ctype"}, + {"value", (getter)cglobalget_value, NULL, "integer constant value"}, + {NULL} /* sentinel */ +}; + +static PyObject * +cglobaldescr_dir(PyObject *cg, PyObject *noarg) +{ + int err; + struct PyGetSetDef *gsdef; + PyObject *res = PyList_New(0); + if (res == NULL) + return NULL; + + for (gsdef = cglobaldescr_getsets; gsdef->name; gsdef++) { + PyObject *x = PyObject_GetAttrString(cg, gsdef->name); + if (x == NULL) { + PyErr_Clear(); + } + else { + Py_DECREF(x); + x = PyUnicode_FromString(gsdef->name); + err = (x != NULL) ? PyList_Append(res, x) : -1; + Py_XDECREF(x); + if (err < 0) { + Py_DECREF(res); + return NULL; + } + } + } + return res; +} + +static PyMethodDef cglobaldescr_methods[] = { + {"__dir__", cglobaldescr_dir, METH_NOARGS}, + {NULL, NULL} /* sentinel */ +}; + +static PyTypeObject CGlobalDescr_Type = { + PyVarObject_HEAD_INIT(NULL, 0) + "_cffi_backend.CGlobal", + sizeof(CGlobalDescrObject), + 0, + (destructor)cglobaldescr_dealloc, /* tp_dealloc */ + 0, /* tp_print */ + 0, /* tp_getattr */ + 0, /* tp_setattr */ + 0, /* tp_compare */ + 0, /* tp_repr */ + 0, /* tp_as_number */ + 0, /* tp_as_sequence */ + 0, /* tp_as_mapping */ + 0, /* tp_hash */ + 0, /* tp_call */ + 0, /* tp_str */ + PyObject_GenericGetAttr, /* tp_getattro */ + 0, /* tp_setattro */ + 0, /* tp_as_buffer */ + Py_TPFLAGS_DEFAULT | Py_TPFLAGS_HAVE_GC, /* tp_flags */ + 0, /* tp_doc */ + (traverseproc)cglobaldescr_traverse, /* tp_traverse */ + (inquiry)cglobaldescr_clear, /* tp_clear */ + 0, /* tp_richcompare */ + 0, /* tp_weaklistoffset */ + 0, /* tp_iter */ + 0, /* tp_iternext */ + cglobaldescr_methods, /* tp_methods */ + 0, /* tp_members */ + cglobaldescr_getsets, /* tp_getset */ +};