From 0e81ef93ebd87607c8d946e7dfb6940c5fe9f083 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Sat, 18 Jul 2026 08:30:50 -0400 Subject: [PATCH] gh-153903: Add a `ctypes` decorator for generating function pointers using annotations (GH-153904) MIME-Version: 1.0 Content-Type: text/plain; charset=utf8 Content-Transfer-Encoding: 8bit Co-authored-by: Tomas R. Co-authored-by: Bartosz Sławecki --- Doc/library/ctypes.rst | 40 +++++++++++++++++++ Doc/whatsnew/3.16.rst | 3 ++ Lib/ctypes/util.py | 18 +++++++++ Lib/test/test_ctypes/test_funcptr.py | 21 ++++++++++ ...-07-18-06-16-55.gh-issue-153903.mJFrs8.rst | 2 + 5 files changed, 84 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-07-18-06-16-55.gh-issue-153903.mJFrs8.rst diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 80868a8b2418..34c8636abaa9 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -694,6 +694,46 @@ through the :attr:`~_CFuncPtr.errcheck` attribute; see the reference manual for details. +Specifying function pointers using type annotations +^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ + +.. decorator:: wrap_dll_function(dll) + :module: ctypes.util + + A :term:`decorator` that generates :attr:`~ctypes._CFuncPtr.argtypes` and + :attr:`~ctypes._CFuncPtr.restype` from a function signature, using the + :attr:`~function.__name__` of the function and its :term:`type annotations `. + + The decorated function should look like this:: + + @wrap_dll_function(dll_to_wrap) + def function_ptr_name(arg_name: ctypes_type, ...) -> ctypes_type: + # There should be no body + pass + + The body of the decorated function is ignored, and any parameters that are + missing type annotations are skipped. The names of the parameters are ignored + and do not have to match the underlying C implementation. + + If the decorated function does not have a return type annotation, a + :exc:`ValueError` is raised. If the name of the function does not exist + in *dll*, an :exc:`AttributeError` is raised. + + For example:: + + import ctypes + from ctypes.util import wrap_dll_function + + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttrString(op: ctypes.py_object, attr: ctypes.c_char_p) -> ctypes.py_object: + pass + + PyObject_GetAttrString(42, b"real") + + + .. versionadded:: next + + .. _ctypes-passing-pointers: Passing pointers (or: passing parameters by reference) diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index 5cb1a86dac46..e6565b186a59 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -237,6 +237,9 @@ ctypes from an annotation-based syntax, similar to how the :mod:`dataclasses` module is used. (Contributed by Peter Bierma in :gh:`104533`.) +* Add :func:`ctypes.util.wrap_dll_function` for generating function pointers + through a function signature. + (Contributed by Peter Bierma in :gh:`153903`.) encodings diff --git a/Lib/ctypes/util.py b/Lib/ctypes/util.py index 51c0d441bc8d..b035bfb99fee 100644 --- a/Lib/ctypes/util.py +++ b/Lib/ctypes/util.py @@ -574,6 +574,24 @@ def struct(class_or_none=None, /, *, align=None, layout=None, endian='native', p return process_the_struct(class_or_none) + +def wrap_dll_function(dll): + def decorator(func): + name = func.__name__ + ptr = getattr(dll, name) + annotations = annotationlib.get_annotations(func) + + try: + restype = annotations.pop("return") + except KeyError as error: + raise ValueError(f"{name!r} missing return type annotation") from error + + ptr.restype = restype + ptr.argtypes = tuple(annotations.values()) + return ptr + + return decorator + ################################################################ # test code diff --git a/Lib/test/test_ctypes/test_funcptr.py b/Lib/test/test_ctypes/test_funcptr.py index be641da30ead..94d03ad553d2 100644 --- a/Lib/test/test_ctypes/test_funcptr.py +++ b/Lib/test/test_ctypes/test_funcptr.py @@ -2,6 +2,7 @@ import ctypes import unittest from ctypes import (CDLL, Structure, CFUNCTYPE, sizeof, _CFuncPtr, c_void_p, c_char_p, c_char, c_int, c_uint, c_long) +from ctypes.util import wrap_dll_function from test.support import import_helper _ctypes_test = import_helper.import_module("_ctypes_test") from ._support import (_CData, PyCFuncPtrType, Py_TPFLAGS_DISALLOW_INSTANTIATION, @@ -130,6 +131,26 @@ class CFuncPtrTestCase(unittest.TestCase, StructCheckMixin): def test_abstract(self): self.assertRaises(TypeError, _CFuncPtr, 13, "name", 42, "iid") + def test_wrap_dll_function(self): + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttr(op: ctypes.py_object, attr: ctypes.py_object) -> ctypes.py_object: + pass + + class Foo: + a = "abc" + + self.assertEqual(PyObject_GetAttr(Foo, "a"), "abc") + + with self.assertRaises(AttributeError): + @wrap_dll_function(ctypes.pythonapi) + def noexist(): + pass + + with self.assertRaisesRegex(ValueError, "'PyObject_GetAttrString' missing return type annotation"): + @wrap_dll_function(ctypes.pythonapi) + def PyObject_GetAttrString(op: ctypes.py_object, attr: ctypes.c_char_p): + pass + if __name__ == '__main__': unittest.main() diff --git a/Misc/NEWS.d/next/Library/2026-07-18-06-16-55.gh-issue-153903.mJFrs8.rst b/Misc/NEWS.d/next/Library/2026-07-18-06-16-55.gh-issue-153903.mJFrs8.rst new file mode 100644 index 000000000000..00f89b471175 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-07-18-06-16-55.gh-issue-153903.mJFrs8.rst @@ -0,0 +1,2 @@ +Add :func:`ctypes.util.wrap_dll_function` for creating +:class:`~ctypes._CFuncPtr` objects from a function signature. -- 2.47.3