gh-112015: Implement `ctypes.memoryview_at()` (GH-112018)
https://github.com/python/cpython/commit/b4f799b1e78ede17b41de9a2bc51b437a7e... commit: b4f799b1e78ede17b41de9a2bc51b437a7e6dd74 branch: main author: Rian Hunter <rianhunter@users.noreply.github.com> committer: encukou <encukou@gmail.com> date: 2025-01-03T14:07:07+01:00 summary: 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> files: A Misc/NEWS.d/next/Library/2023-11-12-21-53-40.gh-issue-112015.2WPRxE.rst M Doc/library/ctypes.rst M Doc/whatsnew/3.14.rst M Lib/ctypes/__init__.py M Lib/test/test_ctypes/test_memfunctions.py M Modules/_ctypes/_ctypes.c diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 09692e56d29a39..398cb92bac809a 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -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 diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index cb9167300260cb..f365db37217e95 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -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 -------- diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 2f2b0ca9f38633..8e2a2926f7a853 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -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: diff --git a/Lib/test/test_ctypes/test_memfunctions.py b/Lib/test/test_ctypes/test_memfunctions.py index 112b27ba48e07e..325487618137f6 100644 --- a/Lib/test/test_ctypes/test_memfunctions.py +++ b/Lib/test/test_ctypes/test_memfunctions.py @@ -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() diff --git a/Misc/NEWS.d/next/Library/2023-11-12-21-53-40.gh-issue-112015.2WPRxE.rst b/Misc/NEWS.d/next/Library/2023-11-12-21-53-40.gh-issue-112015.2WPRxE.rst new file mode 100644 index 00000000000000..4b58ec9d219eff --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-11-12-21-53-40.gh-issue-112015.2WPRxE.rst @@ -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. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index ac520ffaad6c90..ede95bdf98bf76 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -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
participants (1)
-
encukou