Skip to content

Commit b4f799b

Browse files
rianhunterserhiy-storchakaencukoupicnixz
authored
gh-112015: Implement ctypes.memoryview_at() (GH-112018)
Co-authored-by: Serhiy Storchaka <storchaka@gmail.com> Co-authored-by: Petr Viktorin <encukou@gmail.com> Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com>
1 parent f21af18 commit b4f799b

File tree

6 files changed

+120
-1
lines changed

6 files changed

+120
-1
lines changed

Doc/library/ctypes.rst

+22
Original file line numberDiff line numberDiff line change
@@ -2182,6 +2182,28 @@ Utility functions
21822182
.. audit-event:: ctypes.wstring_at ptr,size ctypes.wstring_at
21832183

21842184

2185+
.. function:: memoryview_at(ptr, size, readonly=False)
2186+
2187+
Return a :class:`memoryview` object of length *size* that references memory
2188+
starting at *void \*ptr*.
2189+
2190+
If *readonly* is true, the returned :class:`!memoryview` object can
2191+
not be used to modify the underlying memory.
2192+
(Changes made by other means will still be reflected in the returned
2193+
object.)
2194+
2195+
This function is similar to :func:`string_at` with the key
2196+
difference of not making a copy of the specified memory.
2197+
It is a semantically equivalent (but more efficient) alternative to
2198+
``memoryview((c_byte * size).from_address(ptr))``.
2199+
(While :meth:`~_CData.from_address` only takes integers, *ptr* can also
2200+
be given as a :class:`ctypes.POINTER` or a :func:`~ctypes.byref` object.)
2201+
2202+
.. audit-event:: ctypes.memoryview_at address,size,readonly
2203+
2204+
.. versionadded:: next
2205+
2206+
21852207
.. _ctypes-data-types:
21862208

21872209
Data types

Doc/whatsnew/3.14.rst

+8
Original file line numberDiff line numberDiff line change
@@ -343,6 +343,14 @@ ctypes
343343
* On Windows, the :func:`~ctypes.CopyComPointer` function is now public.
344344
(Contributed by Jun Komoda in :gh:`127275`.)
345345

346+
* :func:`ctypes.memoryview_at` now exists to create a
347+
:class:`memoryview` object that refers to the supplied pointer and
348+
length. This works like :func:`ctypes.string_at` except it avoids a
349+
buffer copy, and is typically useful when implementing pure Python
350+
callback functions that are passed dynamically-sized buffers.
351+
(Contributed by Rian Hunter in :gh:`112018`.)
352+
353+
346354
datetime
347355
--------
348356

Lib/ctypes/__init__.py

+9
Original file line numberDiff line numberDiff line change
@@ -524,6 +524,7 @@ def WinError(code=None, descr=None):
524524
# functions
525525

526526
from _ctypes import _memmove_addr, _memset_addr, _string_at_addr, _cast_addr
527+
from _ctypes import _memoryview_at_addr
527528

528529
## void *memmove(void *, const void *, size_t);
529530
memmove = CFUNCTYPE(c_void_p, c_void_p, c_void_p, c_size_t)(_memmove_addr)
@@ -549,6 +550,14 @@ def string_at(ptr, size=-1):
549550
Return the byte string at void *ptr."""
550551
return _string_at(ptr, size)
551552

553+
_memoryview_at = PYFUNCTYPE(
554+
py_object, c_void_p, c_ssize_t, c_int)(_memoryview_at_addr)
555+
def memoryview_at(ptr, size, readonly=False):
556+
"""memoryview_at(ptr, size[, readonly]) -> memoryview
557+
558+
Return a memoryview representing the memory at void *ptr."""
559+
return _memoryview_at(ptr, size, bool(readonly))
560+
552561
try:
553562
from _ctypes import _wstring_at_addr
554563
except ImportError:

Lib/test/test_ctypes/test_memfunctions.py

+59-1
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,9 @@
55
create_string_buffer, string_at,
66
create_unicode_buffer, wstring_at,
77
memmove, memset,
8-
c_char_p, c_byte, c_ubyte, c_wchar)
8+
memoryview_at, c_void_p,
9+
c_char_p, c_byte, c_ubyte, c_wchar,
10+
addressof, byref)
911

1012

1113
class MemFunctionsTest(unittest.TestCase):
@@ -77,6 +79,62 @@ def test_wstring_at(self):
7779
self.assertEqual(wstring_at(a, 16), "Hello, World\0\0\0\0")
7880
self.assertEqual(wstring_at(a, 0), "")
7981

82+
def test_memoryview_at(self):
83+
b = (c_byte * 10)()
84+
85+
size = len(b)
86+
for foreign_ptr in (
87+
b,
88+
cast(b, c_void_p),
89+
byref(b),
90+
addressof(b),
91+
):
92+
with self.subTest(foreign_ptr=type(foreign_ptr).__name__):
93+
b[:] = b"initialval"
94+
v = memoryview_at(foreign_ptr, size)
95+
self.assertIsInstance(v, memoryview)
96+
self.assertEqual(bytes(v), b"initialval")
97+
98+
# test that writes to source buffer get reflected in memoryview
99+
b[:] = b"0123456789"
100+
self.assertEqual(bytes(v), b"0123456789")
101+
102+
# test that writes to memoryview get reflected in source buffer
103+
v[:] = b"9876543210"
104+
self.assertEqual(bytes(b), b"9876543210")
105+
106+
with self.assertRaises(ValueError):
107+
memoryview_at(foreign_ptr, -1)
108+
109+
with self.assertRaises(ValueError):
110+
memoryview_at(foreign_ptr, sys.maxsize + 1)
111+
112+
v0 = memoryview_at(foreign_ptr, 0)
113+
self.assertEqual(bytes(v0), b'')
114+
115+
def test_memoryview_at_readonly(self):
116+
b = (c_byte * 10)()
117+
118+
size = len(b)
119+
for foreign_ptr in (
120+
b,
121+
cast(b, c_void_p),
122+
byref(b),
123+
addressof(b),
124+
):
125+
with self.subTest(foreign_ptr=type(foreign_ptr).__name__):
126+
b[:] = b"initialval"
127+
v = memoryview_at(foreign_ptr, size, readonly=True)
128+
self.assertIsInstance(v, memoryview)
129+
self.assertEqual(bytes(v), b"initialval")
130+
131+
# test that writes to source buffer get reflected in memoryview
132+
b[:] = b"0123456789"
133+
self.assertEqual(bytes(v), b"0123456789")
134+
135+
# test that writes to the memoryview are blocked
136+
with self.assertRaises(TypeError):
137+
v[:] = b"9876543210"
80138

81139
if __name__ == "__main__":
82140
unittest.main()
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
:func:`ctypes.memoryview_at` now exists to create a
2+
:class:`memoryview` object that refers to the supplied pointer and
3+
length. This works like :func:`ctypes.string_at` except it avoids a
4+
buffer copy, and is typically useful when implementing pure Python
5+
callback functions that are passed dynamically-sized buffers.

Modules/_ctypes/_ctypes.c

+17
Original file line numberDiff line numberDiff line change
@@ -5791,6 +5791,22 @@ wstring_at(const wchar_t *ptr, int size)
57915791
return PyUnicode_FromWideChar(ptr, ssize);
57925792
}
57935793

5794+
static PyObject *
5795+
memoryview_at(void *ptr, Py_ssize_t size, int readonly)
5796+
{
5797+
if (PySys_Audit("ctypes.memoryview_at", "nni",
5798+
(Py_ssize_t)ptr, size, readonly) < 0) {
5799+
return NULL;
5800+
}
5801+
if (size < 0) {
5802+
PyErr_Format(PyExc_ValueError,
5803+
"memoryview_at: size is negative (or overflowed): %zd",
5804+
size);
5805+
return NULL;
5806+
}
5807+
return PyMemoryView_FromMemory(ptr, size,
5808+
readonly ? PyBUF_READ : PyBUF_WRITE);
5809+
}
57945810

57955811
static int
57965812
_ctypes_add_types(PyObject *mod)
@@ -5919,6 +5935,7 @@ _ctypes_add_objects(PyObject *mod)
59195935
MOD_ADD("_string_at_addr", PyLong_FromVoidPtr(string_at));
59205936
MOD_ADD("_cast_addr", PyLong_FromVoidPtr(cast));
59215937
MOD_ADD("_wstring_at_addr", PyLong_FromVoidPtr(wstring_at));
5938+
MOD_ADD("_memoryview_at_addr", PyLong_FromVoidPtr(memoryview_at));
59225939

59235940
/* If RTLD_LOCAL is not defined (Windows!), set it to zero. */
59245941
#if !HAVE_DECL_RTLD_LOCAL

0 commit comments

Comments
 (0)