Skip to content

Commit 3758955

Browse files
gh-71990: Call _as_parameter_ on ctypes.POINTER arguments (GH-159763)
1 parent f5d989e commit 3758955

6 files changed

Lines changed: 99 additions & 0 deletions

File tree

‎Doc/library/ctypes.rst‎

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1107,6 +1107,17 @@ In addition, if a function argument is explicitly declared to be a pointer type
11071107
type (``c_int`` in this case) can be passed to the function. ctypes will apply
11081108
the required :func:`byref` conversion in this case automatically.
11091109

1110+
These conversions are also applied to the :attr:`!_as_parameter_` attribute of
1111+
an object passed for a pointer argument (see
1112+
:ref:`ctypes-calling-functions-with-own-custom-data-types`). So, for a
1113+
``POINTER(c_int)`` argument, an object whose :attr:`!_as_parameter_` is a
1114+
``c_int`` instance is passed by reference, and one whose :attr:`!_as_parameter_`
1115+
is a ``c_int`` array is accepted as well.
1116+
1117+
.. versionchanged:: next
1118+
Objects with an :attr:`!_as_parameter_` attribute now receive the same
1119+
implicit conversions for pointer arguments as the object they wrap.
1120+
11101121
To set a POINTER type field to ``NULL``, you can assign ``None``::
11111122

11121123
>>> bar.values = None

‎Doc/whatsnew/3.16.rst‎

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -214,6 +214,10 @@ ctypes
214214
They are only available with libffi 3.5 or later.
215215
(Contributed by Serhiy Storchaka in :gh:`157487`.)
216216

217+
* Objects with an :attr:`!_as_parameter_` attribute are now
218+
implicitly converted when passed for a :func:`~ctypes.POINTER` argument.
219+
(Contributed by Peter Bierma in :gh:`71990`.)
220+
217221

218222
curses
219223
------

‎Lib/test/test_ctypes/test_as_parameter.py‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -208,6 +208,7 @@ class A:
208208
ctypes.c_void_p,
209209
ctypes.c_int, # PyCSimpleType
210210
POINT, # CDataType
211+
POINTER(c_int), # PyCPointerType
211212
):
212213
with self.subTest(c_type=c_type):
213214
with self.assertRaises(RecursionError):

‎Lib/test/test_ctypes/test_parameters.py‎

Lines changed: 66 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
c_long, c_ulong,
1616
c_longlong, c_ulonglong,
1717
c_float, c_double, c_longdouble)
18+
from ctypes.util import struct
1819
from test.support import import_helper
1920
_ctypes_test = import_helper.import_module("_ctypes_test")
2021

@@ -168,6 +169,71 @@ def test_array_pointers(self):
168169
self.assertRaises(TypeError, LPINT.from_param, c_long*3)
169170
self.assertRaises(TypeError, LPINT.from_param, c_uint*3)
170171

172+
def test_pointer_as_parameter(self):
173+
class Wrapper:
174+
def __init__(self, param):
175+
self._as_parameter_ = param
176+
177+
p_int = POINTER(c_int)
178+
179+
# Implicit byref() of a wrapped instance
180+
v = c_int(42)
181+
x = p_int.from_param(Wrapper(v))
182+
self.assertIs(x._obj, v)
183+
184+
# Wrapped pointer
185+
p = pointer(c_int(99))
186+
self.assertIs(p_int.from_param(Wrapper(p)), p)
187+
188+
# Wrapped byref()
189+
p_int.from_param(Wrapper(byref(c_int(7))))
190+
191+
# Wrapped array
192+
a = (c_int * 3)()
193+
self.assertIs(p_int.from_param(Wrapper(a)), a)
194+
195+
@struct
196+
class Base:
197+
x: c_int
198+
199+
class Derived(Base):
200+
pass
201+
202+
p_base = POINTER(Base)
203+
204+
# Pointer upcast of a wrapped pointer and a wrapped instance.
205+
d = Derived()
206+
pd = pointer(d)
207+
self.assertIs(p_base.from_param(Wrapper(pd)), pd)
208+
self.assertIs(p_base.from_param(Wrapper(d))._obj, d)
209+
210+
# Nested _as_parameter_.
211+
self.assertIs(p_int.from_param(Wrapper(Wrapper(Wrapper(v))))._obj, v)
212+
213+
# None is still accepted.
214+
self.assertIsNone(p_int.from_param(Wrapper(None)))
215+
216+
# Type mismatches are still rejected.
217+
with self.assertRaises(TypeError):
218+
p_int.from_param(Wrapper(c_short(1)))
219+
with self.assertRaises(TypeError):
220+
p_int.from_param(Wrapper(pointer(c_short(1))))
221+
with self.assertRaises(TypeError):
222+
p_int.from_param(Wrapper(byref(c_short(1))))
223+
with self.assertRaises(TypeError):
224+
p_int.from_param(Wrapper((c_short * 3)()))
225+
with self.assertRaises(TypeError):
226+
p_int.from_param(Wrapper(object()))
227+
228+
# End-to-end through a function call. The c_int must stay alive
229+
# while the returned pointer is used.
230+
func = CDLL(_ctypes_test.__file__)._testfunc_p_p
231+
func.restype = p_int
232+
func.argtypes = [p_int]
233+
v = c_int(123)
234+
result = func(Wrapper(v))
235+
self.assertEqual(result.contents.value, 123)
236+
171237
def test_noctypes_argtype(self):
172238
func = CDLL(_ctypes_test.__file__)._testfunc_p_p
173239
func.restype = c_void_p
Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
:mod:`ctypes`: objects with an :attr:`!_as_parameter_` attribute are now
2+
implicitly converted when passed for a :func:`~ctypes.POINTER` argument.

‎Modules/_ctypes/_ctypes.c‎

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1436,6 +1436,21 @@ PyCPointerType_from_param_impl(PyObject *type, PyTypeObject *cls,
14361436
return Py_NewRef(value);
14371437
}
14381438
}
1439+
1440+
PyObject *as_parameter;
1441+
if (PyObject_GetOptionalAttr(value, &_Py_ID(_as_parameter_), &as_parameter) < 0) {
1442+
return NULL;
1443+
}
1444+
if (as_parameter) {
1445+
if (_Py_EnterRecursiveCall(" while processing _as_parameter_")) {
1446+
Py_DECREF(as_parameter);
1447+
return NULL;
1448+
}
1449+
value = PyCPointerType_from_param_impl(type, cls, as_parameter);
1450+
Py_DECREF(as_parameter);
1451+
_Py_LeaveRecursiveCall();
1452+
return value;
1453+
}
14391454
return CDataType_from_param_impl(type, cls, value);
14401455
}
14411456

0 commit comments

Comments
 (0)