From 90ac4d438d9a05dfc924bf7f6a92c4a05ade6575 Mon Sep 17 00:00:00 2001 From: Peter Bierma Date: Mon, 19 Oct 2026 16:41:56 -0400 Subject: [PATCH] Call _as_parameter_ on ctypes.POINTER() arguments. --- Doc/library/ctypes.rst | 11 ++++ Doc/whatsnew/3.16.rst | 4 ++ Lib/test/test_ctypes/test_as_parameter.py | 1 + Lib/test/test_ctypes/test_parameters.py | 66 +++++++++++++++++++ ...6-10-19-16-26-57.gh-issue-71990.K3FlJ9.rst | 2 + Modules/_ctypes/_ctypes.c | 15 +++++ 6 files changed, 99 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2026-10-19-16-26-57.gh-issue-71990.K3FlJ9.rst diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index c2c98863f7ccbd1..2cae840a97ad8ab 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -1107,6 +1107,17 @@ In addition, if a function argument is explicitly declared to be a pointer type type (``c_int`` in this case) can be passed to the function. ctypes will apply the required :func:`byref` conversion in this case automatically. +These conversions are also applied to the :attr:`!_as_parameter_` attribute of +an object passed for a pointer argument (see +:ref:`ctypes-calling-functions-with-own-custom-data-types`). So, for a +``POINTER(c_int)`` argument, an object whose :attr:`!_as_parameter_` is a +``c_int`` instance is passed by reference, and one whose :attr:`!_as_parameter_` +is a ``c_int`` array is accepted as well. + +.. versionchanged:: next + Objects with an :attr:`!_as_parameter_` attribute now receive the same + implicit conversions for pointer arguments as the object they wrap. + To set a POINTER type field to ``NULL``, you can assign ``None``:: >>> bar.values = None diff --git a/Doc/whatsnew/3.16.rst b/Doc/whatsnew/3.16.rst index a1d7d1eaae7d526..716a5cdcbf4facd 100644 --- a/Doc/whatsnew/3.16.rst +++ b/Doc/whatsnew/3.16.rst @@ -198,6 +198,10 @@ ctypes They are only available with libffi 3.5 or later. (Contributed by Serhiy Storchaka in :gh:`157487`.) +* Objects with an :attr:`!_as_parameter_` attribute are now + implicitly converted when passed for a :func:`~ctypes.POINTER` argument. + (Contributed by Peter Bierma in :gh:`71990`.) + curses ------ diff --git a/Lib/test/test_ctypes/test_as_parameter.py b/Lib/test/test_ctypes/test_as_parameter.py index abbfb6efdcea55b..8634c3f03a02ec1 100644 --- a/Lib/test/test_ctypes/test_as_parameter.py +++ b/Lib/test/test_ctypes/test_as_parameter.py @@ -208,6 +208,7 @@ class A: ctypes.c_void_p, ctypes.c_int, # PyCSimpleType POINT, # CDataType + POINTER(c_int), # PyCPointerType ): with self.subTest(c_type=c_type): with self.assertRaises(RecursionError): diff --git a/Lib/test/test_ctypes/test_parameters.py b/Lib/test/test_ctypes/test_parameters.py index 6dadb7b410d7034..ba3de0e5908494c 100644 --- a/Lib/test/test_ctypes/test_parameters.py +++ b/Lib/test/test_ctypes/test_parameters.py @@ -15,6 +15,7 @@ c_long, c_ulong, c_longlong, c_ulonglong, c_float, c_double, c_longdouble) +from ctypes.util import struct from test.support import import_helper _ctypes_test = import_helper.import_module("_ctypes_test") @@ -168,6 +169,71 @@ def test_array_pointers(self): self.assertRaises(TypeError, LPINT.from_param, c_long*3) self.assertRaises(TypeError, LPINT.from_param, c_uint*3) + def test_pointer_as_parameter(self): + class Wrapper: + def __init__(self, param): + self._as_parameter_ = param + + p_int = POINTER(c_int) + + # Implicit byref() of a wrapped instance + v = c_int(42) + x = p_int.from_param(Wrapper(v)) + self.assertIs(x._obj, v) + + # Wrapped pointer + p = pointer(c_int(99)) + self.assertIs(p_int.from_param(Wrapper(p)), p) + + # Wrapped byref() + p_int.from_param(Wrapper(byref(c_int(7)))) + + # Wrapped array + a = (c_int * 3)() + self.assertIs(p_int.from_param(Wrapper(a)), a) + + @struct + class Base: + x: c_int + + class Derived(Base): + pass + + p_base = POINTER(Base) + + # Pointer upcast of a wrapped pointer and a wrapped instance. + d = Derived() + pd = pointer(d) + self.assertIs(p_base.from_param(Wrapper(pd)), pd) + self.assertIs(p_base.from_param(Wrapper(d))._obj, d) + + # Nested _as_parameter_. + self.assertIs(p_int.from_param(Wrapper(Wrapper(Wrapper(v))))._obj, v) + + # None is still accepted. + self.assertIsNone(p_int.from_param(Wrapper(None))) + + # Type mismatches are still rejected. + with self.assertRaises(TypeError): + p_int.from_param(Wrapper(c_short(1))) + with self.assertRaises(TypeError): + p_int.from_param(Wrapper(pointer(c_short(1)))) + with self.assertRaises(TypeError): + p_int.from_param(Wrapper(byref(c_short(1)))) + with self.assertRaises(TypeError): + p_int.from_param(Wrapper((c_short * 3)())) + with self.assertRaises(TypeError): + p_int.from_param(Wrapper(object())) + + # End-to-end through a function call. The c_int must stay alive + # while the returned pointer is used. + func = CDLL(_ctypes_test.__file__)._testfunc_p_p + func.restype = p_int + func.argtypes = [p_int] + v = c_int(123) + result = func(Wrapper(v)) + self.assertEqual(result.contents.value, 123) + def test_noctypes_argtype(self): func = CDLL(_ctypes_test.__file__)._testfunc_p_p func.restype = c_void_p diff --git a/Misc/NEWS.d/next/Library/2026-10-19-16-26-57.gh-issue-71990.K3FlJ9.rst b/Misc/NEWS.d/next/Library/2026-10-19-16-26-57.gh-issue-71990.K3FlJ9.rst new file mode 100644 index 000000000000000..87276db65054a03 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2026-10-19-16-26-57.gh-issue-71990.K3FlJ9.rst @@ -0,0 +1,2 @@ +:mod:`ctypes`: objects with an :attr:`!_as_parameter_` attribute are now +implicitly converted when passed for a :func:`~ctypes.POINTER` argument. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 43f416c141fd8ad..6f49af3eb5d10ab 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -1436,6 +1436,21 @@ PyCPointerType_from_param_impl(PyObject *type, PyTypeObject *cls, return Py_NewRef(value); } } + + PyObject *as_parameter; + if (PyObject_GetOptionalAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) { + return NULL; + } + if (as_parameter) { + if (_Py_EnterRecursiveCall(" while processing _as_parameter_")) { + Py_DECREF(as_parameter); + return NULL; + } + value = PyCPointerType_from_param_impl(type, cls, as_parameter); + Py_DECREF(as_parameter); + _Py_LeaveRecursiveCall(); + return value; + } return CDataType_from_param_impl(type, cls, value); }