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>
This commit is contained in:
Rian Hunter 2025-01-03 05:07:07 -08:00 committed by GitHub
parent f21af186bf
commit b4f799b1e7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 120 additions and 1 deletions

View file

@ -2182,6 +2182,28 @@ Utility functions
.. audit-event:: ctypes.wstring_at ptr,size ctypes.wstring_at
.. function:: memoryview_at(ptr, size, readonly=False)
Return a :class:`memoryview` object of length *size* that references memory
starting at *void \*ptr*.
If *readonly* is true, the returned :class:`!memoryview` object can
not be used to modify the underlying memory.
(Changes made by other means will still be reflected in the returned
object.)
This function is similar to :func:`string_at` with the key
difference of not making a copy of the specified memory.
It is a semantically equivalent (but more efficient) alternative to
``memoryview((c_byte * size).from_address(ptr))``.
(While :meth:`~_CData.from_address` only takes integers, *ptr* can also
be given as a :class:`ctypes.POINTER` or a :func:`~ctypes.byref` object.)
.. audit-event:: ctypes.memoryview_at address,size,readonly
.. versionadded:: next
.. _ctypes-data-types:
Data types

View file

@ -343,6 +343,14 @@ ctypes
* On Windows, the :func:`~ctypes.CopyComPointer` function is now public.
(Contributed by Jun Komoda in :gh:`127275`.)
* :func:`ctypes.memoryview_at` now exists to create a
:class:`memoryview` object that refers to the supplied pointer and
length. This works like :func:`ctypes.string_at` except it avoids a
buffer copy, and is typically useful when implementing pure Python
callback functions that are passed dynamically-sized buffers.
(Contributed by Rian Hunter in :gh:`112018`.)
datetime
--------

View file

@ -524,6 +524,7 @@ def WinError(code=None, descr=None):
# functions
from _ctypes import _memmove_addr, _memset_addr, _string_at_addr, _cast_addr
from _ctypes import _memoryview_at_addr
## void *memmove(void *, const void *, size_t);
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):
Return the byte string at void *ptr."""
return _string_at(ptr, size)
_memoryview_at = PYFUNCTYPE(
py_object, c_void_p, c_ssize_t, c_int)(_memoryview_at_addr)
def memoryview_at(ptr, size, readonly=False):
"""memoryview_at(ptr, size[, readonly]) -> memoryview
Return a memoryview representing the memory at void *ptr."""
return _memoryview_at(ptr, size, bool(readonly))
try:
from _ctypes import _wstring_at_addr
except ImportError:

View file

@ -5,7 +5,9 @@
create_string_buffer, string_at,
create_unicode_buffer, wstring_at,
memmove, memset,
c_char_p, c_byte, c_ubyte, c_wchar)
memoryview_at, c_void_p,
c_char_p, c_byte, c_ubyte, c_wchar,
addressof, byref)
class MemFunctionsTest(unittest.TestCase):
@ -77,6 +79,62 @@ def test_wstring_at(self):
self.assertEqual(wstring_at(a, 16), "Hello, World\0\0\0\0")
self.assertEqual(wstring_at(a, 0), "")
def test_memoryview_at(self):
b = (c_byte * 10)()
size = len(b)
for foreign_ptr in (
b,
cast(b, c_void_p),
byref(b),
addressof(b),
):
with self.subTest(foreign_ptr=type(foreign_ptr).__name__):
b[:] = b"initialval"
v = memoryview_at(foreign_ptr, size)
self.assertIsInstance(v, memoryview)
self.assertEqual(bytes(v), b"initialval")
# test that writes to source buffer get reflected in memoryview
b[:] = b"0123456789"
self.assertEqual(bytes(v), b"0123456789")
# test that writes to memoryview get reflected in source buffer
v[:] = b"9876543210"
self.assertEqual(bytes(b), b"9876543210")
with self.assertRaises(ValueError):
memoryview_at(foreign_ptr, -1)
with self.assertRaises(ValueError):
memoryview_at(foreign_ptr, sys.maxsize + 1)
v0 = memoryview_at(foreign_ptr, 0)
self.assertEqual(bytes(v0), b'')
def test_memoryview_at_readonly(self):
b = (c_byte * 10)()
size = len(b)
for foreign_ptr in (
b,
cast(b, c_void_p),
byref(b),
addressof(b),
):
with self.subTest(foreign_ptr=type(foreign_ptr).__name__):
b[:] = b"initialval"
v = memoryview_at(foreign_ptr, size, readonly=True)
self.assertIsInstance(v, memoryview)
self.assertEqual(bytes(v), b"initialval")
# test that writes to source buffer get reflected in memoryview
b[:] = b"0123456789"
self.assertEqual(bytes(v), b"0123456789")
# test that writes to the memoryview are blocked
with self.assertRaises(TypeError):
v[:] = b"9876543210"
if __name__ == "__main__":
unittest.main()

View file

@ -0,0 +1,5 @@
:func:`ctypes.memoryview_at` now exists to create a
:class:`memoryview` object that refers to the supplied pointer and
length. This works like :func:`ctypes.string_at` except it avoids a
buffer copy, and is typically useful when implementing pure Python
callback functions that are passed dynamically-sized buffers.

View file

@ -5791,6 +5791,22 @@ wstring_at(const wchar_t *ptr, int size)
return PyUnicode_FromWideChar(ptr, ssize);
}
static PyObject *
memoryview_at(void *ptr, Py_ssize_t size, int readonly)
{
if (PySys_Audit("ctypes.memoryview_at", "nni",
(Py_ssize_t)ptr, size, readonly) < 0) {
return NULL;
}
if (size < 0) {
PyErr_Format(PyExc_ValueError,
"memoryview_at: size is negative (or overflowed): %zd",
size);
return NULL;
}
return PyMemoryView_FromMemory(ptr, size,
readonly ? PyBUF_READ : PyBUF_WRITE);
}
static int
_ctypes_add_types(PyObject *mod)
@ -5919,6 +5935,7 @@ _ctypes_add_objects(PyObject *mod)
MOD_ADD("_string_at_addr", PyLong_FromVoidPtr(string_at));
MOD_ADD("_cast_addr", PyLong_FromVoidPtr(cast));
MOD_ADD("_wstring_at_addr", PyLong_FromVoidPtr(wstring_at));
MOD_ADD("_memoryview_at_addr", PyLong_FromVoidPtr(memoryview_at));
/* If RTLD_LOCAL is not defined (Windows!), set it to zero. */
#if !HAVE_DECL_RTLD_LOCAL