From 236657b0fe8f20e18343dbad1dec966e7a2524f2 Mon Sep 17 00:00:00 2001 From: Rian Hunter Date: Sun, 12 Nov 2023 21:24:00 -0500 Subject: [PATCH 01/19] Implement `ctypes.buffer_at()` ctypes currently has no way to easily create a buffer object from a pointer and a dynamic size. It is possible to create memoryview objects of array objects (e.g. memoryview((c_ubyte * 10)())) but this is excessively slow when implementing a callback function in Python that is passed a dynamic void * and a size_t. `ctypes.buffer_at()` fills that gap in the API. This is similar to `ffi.buffer()` in the cffi module. --- Doc/library/ctypes.rst | 13 +++++++++++++ Lib/ctypes/__init__.py | 8 ++++++++ .../2023-11-12-21-53-40.gh-issue-112015.2WPRxE.rst | 3 +++ Modules/_ctypes/_ctypes.c | 12 ++++++++++++ 4 files changed, 36 insertions(+) create mode 100644 Misc/NEWS.d/next/Library/2023-11-12-21-53-40.gh-issue-112015.2WPRxE.rst diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index ef3a9a0f5898af..c28a378d181b59 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -2109,6 +2109,19 @@ Utility functions .. audit-event:: ctypes.wstring_at address,size ctypes.wstring_at +.. function:: buffer_at(address, size, allow_write=False) + + This function returns a memoryview object that references the + memory starting at *address* up to (but not including) *address + + size*. If *allow_write* is set to a truthy value then the + memoryview object is mutable. + + This function is similar to :funct:`string_at` with the key difference + of not making a copy of the specified memory. + + .. audit-event:: ctypes.buffer_at address,size,allow_write ctypes.buffer_at + + .. _ctypes-data-types: Data types diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 141142a57dcb3e..674286550730d7 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -538,6 +538,14 @@ def wstring_at(ptr, size=-1): Return the string at addr.""" return _wstring_at(ptr, size) +from _ctypes import _buffer_at_addr + +_buffer_at = PYFUNCTYPE(py_object, c_void_p, c_int, c_int)(_buffer_at_addr) +def buffer_at(ptr, size, allow_write=False): + """buffer_at(addr, size[, allow_write]) -> memoryview + + Return the buffer at addr.""" + return _buffer_at(ptr, size, bool(allow_write)) if _os.name == "nt": # COM stuff def DllGetClassObject(rclsid, riid, ppv): 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..5be73325032c62 --- /dev/null +++ b/Misc/NEWS.d/next/Library/2023-11-12-21-53-40.gh-issue-112015.2WPRxE.rst @@ -0,0 +1,3 @@ +ctypes.buffer_at() now exists to create a memoryview object that refers to +the supplied pointer and length. Works just like ctypes.string_at() except +it avoids a buffer copy. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 6e0709cc1e4a4d..a77824b592d4a9 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5642,6 +5642,17 @@ wstring_at(const wchar_t *ptr, int size) return PyUnicode_FromWideChar(ptr, ssize); } +static PyObject * +buffer_at(char *ptr, int size, int allow_write) +{ + if (PySys_Audit("ctypes.buffer_at", "nii", (Py_ssize_t)ptr, size, + allow_write) < 0) { + return NULL; + } + + return PyMemoryView_FromMemory(ptr, size, + allow_write ? PyBUF_WRITE : PyBUF_READ); +} static struct PyModuleDef _ctypesmodule = { PyModuleDef_HEAD_INIT, @@ -5777,6 +5788,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("_buffer_at_addr", PyLong_FromVoidPtr(buffer_at)); /* If RTLD_LOCAL is not defined (Windows!), set it to zero. */ #if !HAVE_DECL_RTLD_LOCAL From 55d7690f5cac7674473ab714a2c33fb2cca389d7 Mon Sep 17 00:00:00 2001 From: Rian Hunter Date: Sun, 12 Nov 2023 22:03:53 -0500 Subject: [PATCH 02/19] Fix typo --- Doc/library/ctypes.rst | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index c28a378d181b59..90073eba5b0726 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -2116,7 +2116,7 @@ Utility functions size*. If *allow_write* is set to a truthy value then the memoryview object is mutable. - This function is similar to :funct:`string_at` with the key difference + This function is similar to :func:`string_at` with the key difference of not making a copy of the specified memory. .. audit-event:: ctypes.buffer_at address,size,allow_write ctypes.buffer_at From 00e3c234c82bcb2184d9afdca0bb8331918f9c06 Mon Sep 17 00:00:00 2001 From: Rian Hunter Date: Thu, 18 Jan 2024 14:14:19 -0800 Subject: [PATCH 03/19] Apply suggestions from code review Documentation improvements Co-authored-by: Serhiy Storchaka --- Doc/library/ctypes.rst | 6 ++++-- .../Library/2023-11-12-21-53-40.gh-issue-112015.2WPRxE.rst | 4 ++-- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 90073eba5b0726..e8def4009c0e47 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -2111,16 +2111,18 @@ Utility functions .. function:: buffer_at(address, size, allow_write=False) - This function returns a memoryview object that references the + This function returns a :class:`memoryview` object that references the memory starting at *address* up to (but not including) *address + size*. If *allow_write* is set to a truthy value then the - memoryview object is mutable. + :class:`!memoryview` object is mutable. This function is similar to :func:`string_at` with the key difference of not making a copy of the specified memory. .. audit-event:: ctypes.buffer_at address,size,allow_write ctypes.buffer_at + .. versionadded:: 3.13 + .. _ctypes-data-types: 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 index 5be73325032c62..aa3b6655eb2cbd 100644 --- 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 @@ -1,3 +1,3 @@ -ctypes.buffer_at() now exists to create a memoryview object that refers to -the supplied pointer and length. Works just like ctypes.string_at() except +:func:`ctypes.buffer_at` now exists to create a :class:`memoryview` object that refers to +the supplied pointer and length. Works just like :func:`ctypes.string_at` except it avoids a buffer copy. From df0f992fbbfd7911e7eb8eca97dc8abadc6ac441 Mon Sep 17 00:00:00 2001 From: Rian Hunter Date: Thu, 18 Jan 2024 20:56:08 -0500 Subject: [PATCH 04/19] Make size argument a Py_ssize_t, per @serhiy-storchaka suggestion --- Lib/ctypes/__init__.py | 2 +- Modules/_ctypes/_ctypes.c | 4 ++-- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 674286550730d7..c45527f5de5160 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -540,7 +540,7 @@ def wstring_at(ptr, size=-1): from _ctypes import _buffer_at_addr -_buffer_at = PYFUNCTYPE(py_object, c_void_p, c_int, c_int)(_buffer_at_addr) +_buffer_at = PYFUNCTYPE(py_object, c_void_p, c_ssize_t, c_int)(_buffer_at_addr) def buffer_at(ptr, size, allow_write=False): """buffer_at(addr, size[, allow_write]) -> memoryview diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index a77824b592d4a9..bbd6f0d65a9b75 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5643,9 +5643,9 @@ wstring_at(const wchar_t *ptr, int size) } static PyObject * -buffer_at(char *ptr, int size, int allow_write) +buffer_at(char *ptr, Py_ssize_t size, int allow_write) { - if (PySys_Audit("ctypes.buffer_at", "nii", (Py_ssize_t)ptr, size, + if (PySys_Audit("ctypes.buffer_at", "nni", (Py_ssize_t)ptr, size, allow_write) < 0) { return NULL; } From eaa2d136c4791bbc0e69b4f6cb5df728c8783cb8 Mon Sep 17 00:00:00 2001 From: Rian Hunter Date: Thu, 18 Jan 2024 21:04:30 -0500 Subject: [PATCH 05/19] Add what's new entry --- Doc/whatsnew/3.13.rst | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index ae3c88d28d73a0..fd130acf789b2d 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -168,6 +168,13 @@ copy any user classes which define the :meth:`!__replace__` method. (Contributed by Serhiy Storchaka in :gh:`108751`.) +ctypes +------ + +* :func:`ctypes.buffer_at` now exists to create a :class:`memoryview` object that refers to + the supplied pointer and length. Works just like :func:`ctypes.string_at` except + it avoids a buffer copy. + dbm --- From db3a26a05512e8fa03e571ffa29d5a58e5061aab Mon Sep 17 00:00:00 2001 From: Rian Hunter Date: Thu, 18 Jan 2024 22:32:12 -0500 Subject: [PATCH 06/19] Rename buffer_at to memoryview_at It's a more descriptive and precise name. --- Doc/library/ctypes.rst | 10 ++++++---- Doc/whatsnew/3.13.rst | 9 ++++++--- Lib/ctypes/__init__.py | 12 ++++++------ .../2023-11-12-21-53-40.gh-issue-112015.2WPRxE.rst | 8 +++++--- Modules/_ctypes/_ctypes.c | 6 +++--- 5 files changed, 26 insertions(+), 19 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index e8def4009c0e47..27fc4463fba933 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -2109,17 +2109,19 @@ Utility functions .. audit-event:: ctypes.wstring_at address,size ctypes.wstring_at -.. function:: buffer_at(address, size, allow_write=False) +.. function:: memoryview_at(address, size, allow_write=False) This function returns a :class:`memoryview` object that references the memory starting at *address* up to (but not including) *address + size*. If *allow_write* is set to a truthy value then the :class:`!memoryview` object is mutable. - This function is similar to :func:`string_at` with the key difference - of not making a copy of the specified memory. + 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(address))`` - .. audit-event:: ctypes.buffer_at address,size,allow_write ctypes.buffer_at + .. audit-event:: ctypes.memoryview_at address,size,allow_write ctypes.buffer_at .. versionadded:: 3.13 diff --git a/Doc/whatsnew/3.13.rst b/Doc/whatsnew/3.13.rst index fd130acf789b2d..814b645f959c07 100644 --- a/Doc/whatsnew/3.13.rst +++ b/Doc/whatsnew/3.13.rst @@ -171,9 +171,12 @@ copy ctypes ------ -* :func:`ctypes.buffer_at` now exists to create a :class:`memoryview` object that refers to - the supplied pointer and length. Works just like :func:`ctypes.string_at` except - it avoids a buffer copy. +* :func:`ctypes.memoryview_at` now exists to create a + :class:`memoryview` object that refers to the supplied pointer and + length. Works just like :func:`ctypes.string_at` except it avoids a + buffer copy. Useful when implementing callback functions in Python + that are passed dynamically-sized buffers. + (Contributed by Rian Hunter in :gh:`112018`.) dbm --- diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index c45527f5de5160..4f1ec618e1ce3b 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -538,14 +538,14 @@ def wstring_at(ptr, size=-1): Return the string at addr.""" return _wstring_at(ptr, size) -from _ctypes import _buffer_at_addr +from _ctypes import _memoryview_at_addr -_buffer_at = PYFUNCTYPE(py_object, c_void_p, c_ssize_t, c_int)(_buffer_at_addr) -def buffer_at(ptr, size, allow_write=False): - """buffer_at(addr, size[, allow_write]) -> memoryview +_memoryview_at = PYFUNCTYPE(py_object, c_void_p, c_ssize_t, c_int)(_memoryview_at_addr) +def memoryview_at(ptr, size, allow_write=False): + """memoryview_at(addr, size[, allow_write]) -> memoryview - Return the buffer at addr.""" - return _buffer_at(ptr, size, bool(allow_write)) + Return a memoryview representing the memory at addr.""" + return _memoryview_at(ptr, size, bool(allow_write)) if _os.name == "nt": # COM stuff def DllGetClassObject(rclsid, riid, ppv): 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 index aa3b6655eb2cbd..b22f54311c2833 100644 --- 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 @@ -1,3 +1,5 @@ -:func:`ctypes.buffer_at` now exists to create a :class:`memoryview` object that refers to -the supplied pointer and length. Works just like :func:`ctypes.string_at` except -it avoids a buffer copy. +:func:`ctypes.memoryview_at` now exists to create a +:class:`memoryview` object that refers to the supplied pointer and +length. Works just like :func:`ctypes.string_at` except it avoids a +buffer copy. Useful when implementing callback functions in Python +that are passed dynamically-sized buffers. diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index bbd6f0d65a9b75..c57fce3384c2df 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5643,9 +5643,9 @@ wstring_at(const wchar_t *ptr, int size) } static PyObject * -buffer_at(char *ptr, Py_ssize_t size, int allow_write) +memoryview_at(char *ptr, Py_ssize_t size, int allow_write) { - if (PySys_Audit("ctypes.buffer_at", "nni", (Py_ssize_t)ptr, size, + if (PySys_Audit("ctypes.memoryview_at", "nni", (Py_ssize_t)ptr, size, allow_write) < 0) { return NULL; } @@ -5788,7 +5788,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("_buffer_at_addr", PyLong_FromVoidPtr(buffer_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 From d7e2f2516c35830eb4a67788083630d6dc25ee28 Mon Sep 17 00:00:00 2001 From: Rian Hunter Date: Thu, 18 Jan 2024 22:33:29 -0500 Subject: [PATCH 07/19] Make mutable objects the default It's more common that the user will want a memoryview object that can be mutated. An immutable object is more rare, so make the default case return a mutable object. --- Doc/library/ctypes.rst | 8 ++++---- Lib/ctypes/__init__.py | 6 +++--- Modules/_ctypes/_ctypes.c | 6 +++--- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 27fc4463fba933..c6b0915e8f90cb 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -2109,19 +2109,19 @@ Utility functions .. audit-event:: ctypes.wstring_at address,size ctypes.wstring_at -.. function:: memoryview_at(address, size, allow_write=False) +.. function:: memoryview_at(address, size, readonly=False) This function returns a :class:`memoryview` object that references the memory starting at *address* up to (but not including) *address + - size*. If *allow_write* is set to a truthy value then the - :class:`!memoryview` object is mutable. + size*. If *readonly* is set to a truthy value then the + :class:`!memoryview` object is immutable. 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(address))`` - .. audit-event:: ctypes.memoryview_at address,size,allow_write ctypes.buffer_at + .. audit-event:: ctypes.memoryview_at address,size,readonly ctypes.buffer_at .. versionadded:: 3.13 diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 4f1ec618e1ce3b..a82b790b9d6e53 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -541,11 +541,11 @@ def wstring_at(ptr, size=-1): from _ctypes import _memoryview_at_addr _memoryview_at = PYFUNCTYPE(py_object, c_void_p, c_ssize_t, c_int)(_memoryview_at_addr) -def memoryview_at(ptr, size, allow_write=False): - """memoryview_at(addr, size[, allow_write]) -> memoryview +def memoryview_at(ptr, size, readonly=False): + """memoryview_at(addr, size[, readonly]) -> memoryview Return a memoryview representing the memory at addr.""" - return _memoryview_at(ptr, size, bool(allow_write)) + return _memoryview_at(ptr, size, bool(readonly)) if _os.name == "nt": # COM stuff def DllGetClassObject(rclsid, riid, ppv): diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index c57fce3384c2df..225fc4d8dc6b14 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5643,15 +5643,15 @@ wstring_at(const wchar_t *ptr, int size) } static PyObject * -memoryview_at(char *ptr, Py_ssize_t size, int allow_write) +memoryview_at(char *ptr, Py_ssize_t size, int readonly) { if (PySys_Audit("ctypes.memoryview_at", "nni", (Py_ssize_t)ptr, size, - allow_write) < 0) { + readonly) < 0) { return NULL; } return PyMemoryView_FromMemory(ptr, size, - allow_write ? PyBUF_WRITE : PyBUF_READ); + readonly ? PyBUF_READ : PyBUF_WRITE); } static struct PyModuleDef _ctypesmodule = { From 097a41cae6c4a910d4686e5fd474df4f81e1186b Mon Sep 17 00:00:00 2001 From: Rian Hunter Date: Thu, 18 Jan 2024 22:46:52 -0500 Subject: [PATCH 08/19] Add test for ctypes.memoryview_at --- Lib/test/test_ctypes/test_memfunctions.py | 20 ++++++++++++++++++++ 1 file changed, 20 insertions(+) diff --git a/Lib/test/test_ctypes/test_memfunctions.py b/Lib/test/test_ctypes/test_memfunctions.py index 112b27ba48e07e..eb5ba59eb627de 100644 --- a/Lib/test/test_ctypes/test_memfunctions.py +++ b/Lib/test/test_ctypes/test_memfunctions.py @@ -5,6 +5,7 @@ create_string_buffer, string_at, create_unicode_buffer, wstring_at, memmove, memset, + memoryview_at, c_void_p, c_char_p, c_byte, c_ubyte, c_wchar) @@ -77,6 +78,25 @@ 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)() + + foreign_ptr = cast(b, c_void_p) + foreign_ptr_size = len(b) + + # memoryview_at() is normally used with pointers given to us + # by C APIs. It's an efficient way to get a buffer + # representing a dynamically-sized memory region without having + # to create an array type first. + v = memoryview_at(foreign_ptr, foreign_ptr_size) + + # 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") if __name__ == "__main__": unittest.main() From e2c260999bb4b297d9ebdcc86d2715edfde8f8fe Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Wed, 18 Sep 2024 15:07:04 +0200 Subject: [PATCH 09/19] TMP --- Lib/ctypes/__init__.py | 10 +--- Modules/_ctypes/_ctypes.c | 33 +++++++++++- Modules/_ctypes/clinic/_ctypes.c.h | 82 +++++++++++++++++++++++++++++- 3 files changed, 113 insertions(+), 12 deletions(-) diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 0be138678c8fec..10e75ad23aadce 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -12,6 +12,7 @@ from _ctypes import RTLD_LOCAL, RTLD_GLOBAL from _ctypes import ArgumentError from _ctypes import SIZEOF_TIME_T +from _ctypes import memoryview_at from struct import calcsize as _calcsize @@ -559,15 +560,6 @@ def wstring_at(ptr, size=-1): Return the wide-character string at void *ptr.""" return _wstring_at(ptr, size) -from _ctypes import _memoryview_at_addr - -_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(addr, size[, readonly]) -> memoryview - - Return a memoryview representing the memory at addr.""" - return _memoryview_at(ptr, size, bool(readonly)) - if _os.name == "nt": # COM stuff def DllGetClassObject(rclsid, riid, ppv): try: diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 8bebb03503892f..65633575f8d2a1 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5704,9 +5704,29 @@ wstring_at(const wchar_t *ptr, int size) return PyUnicode_FromWideChar(ptr, ssize); } +/*[clinic input] +_ctypes.memoryview_at + + obj: object + / + size: Py_ssize_t + readonly: bool = False + +Return a memoryview representing the memory at addr. +[clinic start generated code]*/ + static PyObject * -memoryview_at(char *ptr, Py_ssize_t size, int readonly) +_ctypes_memoryview_at_impl(PyObject *module, PyObject *obj, Py_ssize_t size, + int readonly) +/*[clinic end generated code: output=c89fdda64bd9901d input=c960c5a2b3ccb9fb]*/ { + ctypes_state *st = get_module_state(module); + if (!CDataObject_Check(st, obj)) { + PyErr_SetString(PyExc_TypeError, "invalid type"); + return NULL; + } + void *ptr = ((CDataObject *)obj)->b_ptr; + if (PySys_Audit("ctypes.memoryview_at", "nni", (Py_ssize_t)ptr, size, readonly) < 0) { return NULL; @@ -5843,7 +5863,6 @@ _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 @@ -5864,6 +5883,12 @@ _ctypes_add_objects(PyObject *mod) #undef MOD_ADD } +// Most ctypes methods are defined in callproc.c. +// Here is the rest. +static PyMethodDef module_methods[] = { + _CTYPES_MEMORYVIEW_AT_METHODDEF + {NULL, NULL} /* Sentinel */ +}; static int _ctypes_mod_exec(PyObject *mod) @@ -5891,6 +5916,10 @@ _ctypes_mod_exec(PyObject *mod) if (_ctypes_add_objects(mod) < 0) { return -1; } + if (PyModule_AddFunctions(mod, module_methods) < 0) { + return -1; + } + return 0; } diff --git a/Modules/_ctypes/clinic/_ctypes.c.h b/Modules/_ctypes/clinic/_ctypes.c.h index e1d5a17cbe7d68..a5515e4b079869 100644 --- a/Modules/_ctypes/clinic/_ctypes.c.h +++ b/Modules/_ctypes/clinic/_ctypes.c.h @@ -3,6 +3,7 @@ preserve [clinic start generated code]*/ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) +# include "pycore_gc.h" // PyGC_Head # include "pycore_runtime.h" // _Py_SINGLETON() #endif #include "pycore_abstract.h" // _PyNumber_Index() @@ -610,4 +611,83 @@ Simple_from_outparm(PyObject *self, PyTypeObject *cls, PyObject *const *args, Py } return Simple_from_outparm_impl(self, cls); } -/*[clinic end generated code: output=a90886be2a294ee6 input=a9049054013a1b77]*/ + +PyDoc_STRVAR(_ctypes_memoryview_at__doc__, +"memoryview_at($module, obj, /, size, readonly=False)\n" +"--\n" +"\n" +"Return a memoryview representing the memory at addr."); + +#define _CTYPES_MEMORYVIEW_AT_METHODDEF \ + {"memoryview_at", _PyCFunction_CAST(_ctypes_memoryview_at), METH_FASTCALL|METH_KEYWORDS, _ctypes_memoryview_at__doc__}, + +static PyObject * +_ctypes_memoryview_at_impl(PyObject *module, PyObject *obj, Py_ssize_t size, + int readonly); + +static PyObject * +_ctypes_memoryview_at(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) +{ + PyObject *return_value = NULL; + #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) + + #define NUM_KEYWORDS 2 + static struct { + PyGC_Head _this_is_not_used; + PyObject_VAR_HEAD + PyObject *ob_item[NUM_KEYWORDS]; + } _kwtuple = { + .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) + .ob_item = { &_Py_ID(size), &_Py_ID(readonly), }, + }; + #undef NUM_KEYWORDS + #define KWTUPLE (&_kwtuple.ob_base.ob_base) + + #else // !Py_BUILD_CORE + # define KWTUPLE NULL + #endif // !Py_BUILD_CORE + + static const char * const _keywords[] = {"", "size", "readonly", NULL}; + static _PyArg_Parser _parser = { + .keywords = _keywords, + .fname = "memoryview_at", + .kwtuple = KWTUPLE, + }; + #undef KWTUPLE + PyObject *argsbuf[3]; + Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; + PyObject *obj; + Py_ssize_t size; + int readonly = 0; + + args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, 2, 3, 0, argsbuf); + if (!args) { + goto exit; + } + obj = args[0]; + { + Py_ssize_t ival = -1; + PyObject *iobj = _PyNumber_Index(args[1]); + if (iobj != NULL) { + ival = PyLong_AsSsize_t(iobj); + Py_DECREF(iobj); + } + if (ival == -1 && PyErr_Occurred()) { + goto exit; + } + size = ival; + } + if (!noptargs) { + goto skip_optional_pos; + } + readonly = PyObject_IsTrue(args[2]); + if (readonly < 0) { + goto exit; + } +skip_optional_pos: + return_value = _ctypes_memoryview_at_impl(module, obj, size, readonly); + +exit: + return return_value; +} +/*[clinic end generated code: output=295c1a614c99d945 input=a9049054013a1b77]*/ From 3a6b559e08a2d0c53b93d5fda58dd5ff76e3a0d3 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 29 Nov 2024 15:48:03 +0100 Subject: [PATCH 10/19] Revert to calling through `ctypes` to get `c_void_p` conversion semantics. Improve tests and docs. --- Doc/library/ctypes.rst | 19 +++--- Lib/ctypes/__init__.py | 9 ++- Lib/test/test_ctypes/test_memfunctions.py | 40 +++++++---- Modules/_ctypes/_ctypes.c | 38 ++--------- Modules/_ctypes/clinic/_ctypes.c.h | 83 +---------------------- 5 files changed, 51 insertions(+), 138 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 62926c07a45667..4f2e0c8d8d0c1c 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -2164,21 +2164,22 @@ Utility functions .. audit-event:: ctypes.wstring_at ptr,size ctypes.wstring_at -.. function:: memoryview_at(address, size, readonly=False) +.. function:: memoryview_at(ptr, size, readonly=False) - This function returns a :class:`memoryview` object that references the - memory starting at *address* up to (but not including) *address + - size*. If *readonly* is set to a truthy value then the - :class:`!memoryview` object is immutable. + This function returns a :class:`memoryview` object of length *size* that + references the memory starting at *void \*ptr*, . + If *readonly* is true then the :class:`!memoryview` object is immutable. 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(address))`` + 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 integer, *ptr* can also + be given as a :class:`ctypes.POINTER` or a :func:`~ctypes.byref` object.) .. audit-event:: ctypes.memoryview_at address,size,readonly ctypes.buffer_at - .. versionadded:: 3.13 + .. versionadded:: next .. _ctypes-data-types: diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 5c6ebaeca19114..9102dd5f5536aa 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -12,7 +12,6 @@ from _ctypes import RTLD_LOCAL, RTLD_GLOBAL from _ctypes import ArgumentError from _ctypes import SIZEOF_TIME_T -from _ctypes import memoryview_at from struct import calcsize as _calcsize @@ -525,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) @@ -550,6 +550,13 @@ 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_int, c_int)(_memoryview_at_addr) +def memoryview_at(ptr, size, readonly=False): + """memoryview_at(ptr[, size]) -> memoryview + + Return a memoryview representing the memory at addr.""" + 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 eb5ba59eb627de..6ee1381241410c 100644 --- a/Lib/test/test_ctypes/test_memfunctions.py +++ b/Lib/test/test_ctypes/test_memfunctions.py @@ -6,7 +6,8 @@ create_unicode_buffer, wstring_at, memmove, memset, memoryview_at, c_void_p, - c_char_p, c_byte, c_ubyte, c_wchar) + c_char_p, c_byte, c_ubyte, c_wchar, + addressof, byref) class MemFunctionsTest(unittest.TestCase): @@ -83,20 +84,29 @@ def test_memoryview_at(self): foreign_ptr = cast(b, c_void_p) foreign_ptr_size = len(b) - - # memoryview_at() is normally used with pointers given to us - # by C APIs. It's an efficient way to get a buffer - # representing a dynamically-sized memory region without having - # to create an array type first. - v = memoryview_at(foreign_ptr, foreign_ptr_size) - - # 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") + for foreign_ptr in ( + b, + cast(b, c_void_p), + byref(b), + addressof(b) + ): + with self.subTest(foreign_ptr=type(foreign_ptr).__name__): + v = memoryview_at(foreign_ptr, foreign_ptr_size) + self.assertIsInstance(v, memoryview) + + # 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) + + v0 = memoryview_at(foreign_ptr, 0) + self.assertEqual(bytes(v0), b'') if __name__ == "__main__": unittest.main() diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index a116d69923cd7a..29465cb5de90e7 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5741,34 +5741,18 @@ wstring_at(const wchar_t *ptr, int size) return PyUnicode_FromWideChar(ptr, ssize); } -/*[clinic input] -_ctypes.memoryview_at - - obj: object - / - size: Py_ssize_t - readonly: bool = False - -Return a memoryview representing the memory at addr. -[clinic start generated code]*/ - static PyObject * -_ctypes_memoryview_at_impl(PyObject *module, PyObject *obj, Py_ssize_t size, - int readonly) -/*[clinic end generated code: output=c89fdda64bd9901d input=c960c5a2b3ccb9fb]*/ +memoryview_at(void *ptr, int size, int readonly) { - ctypes_state *st = get_module_state(module); - if (!CDataObject_Check(st, obj)) { - PyErr_SetString(PyExc_TypeError, "invalid type"); + Py_ssize_t ssize = size; + if (PySys_Audit("ctypes.memoryview_at", "nni", + (Py_ssize_t)ptr, ssize, readonly) < 0) { return NULL; } - void *ptr = ((CDataObject *)obj)->b_ptr; - - if (PySys_Audit("ctypes.memoryview_at", "nni", (Py_ssize_t)ptr, size, - readonly) < 0) { + if (ssize < 0) { + PyErr_SetString(PyExc_ValueError, "size must not be negative"); return NULL; } - return PyMemoryView_FromMemory(ptr, size, readonly ? PyBUF_READ : PyBUF_WRITE); } @@ -5900,6 +5884,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 @@ -5920,12 +5905,6 @@ _ctypes_add_objects(PyObject *mod) #undef MOD_ADD } -// Most ctypes methods are defined in callproc.c. -// Here is the rest. -static PyMethodDef module_methods[] = { - _CTYPES_MEMORYVIEW_AT_METHODDEF - {NULL, NULL} /* Sentinel */ -}; static int _ctypes_mod_exec(PyObject *mod) @@ -5953,9 +5932,6 @@ _ctypes_mod_exec(PyObject *mod) if (_ctypes_add_objects(mod) < 0) { return -1; } - if (PyModule_AddFunctions(mod, module_methods) < 0) { - return -1; - } return 0; } diff --git a/Modules/_ctypes/clinic/_ctypes.c.h b/Modules/_ctypes/clinic/_ctypes.c.h index df3a79e8ead1c2..1332ba04cdfecd 100644 --- a/Modules/_ctypes/clinic/_ctypes.c.h +++ b/Modules/_ctypes/clinic/_ctypes.c.h @@ -3,7 +3,6 @@ preserve [clinic start generated code]*/ #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) -# include "pycore_gc.h" // PyGC_Head # include "pycore_runtime.h" // _Py_SINGLETON() #endif #include "pycore_abstract.h" // _PyNumber_Index() @@ -622,84 +621,4 @@ Simple_from_outparm(PyObject *self, PyTypeObject *cls, PyObject *const *args, Py } return Simple_from_outparm_impl(self, cls); } - -PyDoc_STRVAR(_ctypes_memoryview_at__doc__, -"memoryview_at($module, obj, /, size, readonly=False)\n" -"--\n" -"\n" -"Return a memoryview representing the memory at addr."); - -#define _CTYPES_MEMORYVIEW_AT_METHODDEF \ - {"memoryview_at", _PyCFunction_CAST(_ctypes_memoryview_at), METH_FASTCALL|METH_KEYWORDS, _ctypes_memoryview_at__doc__}, - -static PyObject * -_ctypes_memoryview_at_impl(PyObject *module, PyObject *obj, Py_ssize_t size, - int readonly); - -static PyObject * -_ctypes_memoryview_at(PyObject *module, PyObject *const *args, Py_ssize_t nargs, PyObject *kwnames) -{ - PyObject *return_value = NULL; - #if defined(Py_BUILD_CORE) && !defined(Py_BUILD_CORE_MODULE) - - #define NUM_KEYWORDS 2 - static struct { - PyGC_Head _this_is_not_used; - PyObject_VAR_HEAD - PyObject *ob_item[NUM_KEYWORDS]; - } _kwtuple = { - .ob_base = PyVarObject_HEAD_INIT(&PyTuple_Type, NUM_KEYWORDS) - .ob_item = { &_Py_ID(size), &_Py_ID(readonly), }, - }; - #undef NUM_KEYWORDS - #define KWTUPLE (&_kwtuple.ob_base.ob_base) - - #else // !Py_BUILD_CORE - # define KWTUPLE NULL - #endif // !Py_BUILD_CORE - - static const char * const _keywords[] = {"", "size", "readonly", NULL}; - static _PyArg_Parser _parser = { - .keywords = _keywords, - .fname = "memoryview_at", - .kwtuple = KWTUPLE, - }; - #undef KWTUPLE - PyObject *argsbuf[3]; - Py_ssize_t noptargs = nargs + (kwnames ? PyTuple_GET_SIZE(kwnames) : 0) - 2; - PyObject *obj; - Py_ssize_t size; - int readonly = 0; - - args = _PyArg_UnpackKeywords(args, nargs, NULL, kwnames, &_parser, - /*minpos*/ 2, /*maxpos*/ 3, /*minkw*/ 0, /*varpos*/ 0, argsbuf); - if (!args) { - goto exit; - } - obj = args[0]; - { - Py_ssize_t ival = -1; - PyObject *iobj = _PyNumber_Index(args[1]); - if (iobj != NULL) { - ival = PyLong_AsSsize_t(iobj); - Py_DECREF(iobj); - } - if (ival == -1 && PyErr_Occurred()) { - goto exit; - } - size = ival; - } - if (!noptargs) { - goto skip_optional_pos; - } - readonly = PyObject_IsTrue(args[2]); - if (readonly < 0) { - goto exit; - } -skip_optional_pos: - return_value = _ctypes_memoryview_at_impl(module, obj, size, readonly); - -exit: - return return_value; -} -/*[clinic end generated code: output=b99e39c432185eca input=a9049054013a1b77]*/ +/*[clinic end generated code: output=52724c091e3a8b8d input=a9049054013a1b77]*/ From 375081a7f9783d9e1a81a2cd516cba8cf388823b Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 29 Nov 2024 15:53:22 +0100 Subject: [PATCH 11/19] Test read-only memoryview --- Lib/test/test_ctypes/test_memfunctions.py | 31 +++++++++++++++++++++-- 1 file changed, 29 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_ctypes/test_memfunctions.py b/Lib/test/test_ctypes/test_memfunctions.py index 6ee1381241410c..206e0dab91694f 100644 --- a/Lib/test/test_ctypes/test_memfunctions.py +++ b/Lib/test/test_ctypes/test_memfunctions.py @@ -83,7 +83,7 @@ def test_memoryview_at(self): b = (c_byte * 10)() foreign_ptr = cast(b, c_void_p) - foreign_ptr_size = len(b) + size = len(b) for foreign_ptr in ( b, cast(b, c_void_p), @@ -91,8 +91,10 @@ def test_memoryview_at(self): addressof(b) ): with self.subTest(foreign_ptr=type(foreign_ptr).__name__): - v = memoryview_at(foreign_ptr, foreign_ptr_size) + 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" @@ -108,5 +110,30 @@ def test_memoryview_at(self): v0 = memoryview_at(foreign_ptr, 0) self.assertEqual(bytes(v0), b'') + def test_memoryview_at_readonly(self): + b = (c_byte * 10)() + + foreign_ptr = cast(b, c_void_p) + 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() From b9e857266b3fa7f6238ec0ccfc7f5a5bf8caba7c Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 29 Nov 2024 15:55:04 +0100 Subject: [PATCH 12/19] Use c_ssize_t for the size --- Lib/ctypes/__init__.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 9102dd5f5536aa..526623816efd1d 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -550,7 +550,7 @@ 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_int, c_int)(_memoryview_at_addr) +_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]) -> memoryview From 1a33fe373a0b3b2f541396521ef5e95654d5e017 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 29 Nov 2024 16:00:02 +0100 Subject: [PATCH 13/19] Test size overflow --- Lib/test/test_ctypes/test_memfunctions.py | 3 +++ Modules/_ctypes/_ctypes.c | 6 ++++-- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/Lib/test/test_ctypes/test_memfunctions.py b/Lib/test/test_ctypes/test_memfunctions.py index 206e0dab91694f..d5dd79bf4c1654 100644 --- a/Lib/test/test_ctypes/test_memfunctions.py +++ b/Lib/test/test_ctypes/test_memfunctions.py @@ -107,6 +107,9 @@ def test_memoryview_at(self): 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'') diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 29465cb5de90e7..a4f04481c3937d 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5742,7 +5742,7 @@ wstring_at(const wchar_t *ptr, int size) } static PyObject * -memoryview_at(void *ptr, int size, int readonly) +memoryview_at(void *ptr, Py_ssize_t size, int readonly) { Py_ssize_t ssize = size; if (PySys_Audit("ctypes.memoryview_at", "nni", @@ -5750,7 +5750,9 @@ memoryview_at(void *ptr, int size, int readonly) return NULL; } if (ssize < 0) { - PyErr_SetString(PyExc_ValueError, "size must not be negative"); + PyErr_Format(PyExc_ValueError, + "memoryview_at: size is negative (or overflowed): %zd", + size); return NULL; } return PyMemoryView_FromMemory(ptr, size, From 0198c89b04639beca150be2797e4417968b4bbb9 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 29 Nov 2024 16:01:49 +0100 Subject: [PATCH 14/19] Doc fixups. Don't imply that *readonly* makes the memory immutable. --- Doc/library/ctypes.rst | 12 ++++++++---- 1 file changed, 8 insertions(+), 4 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index 4f2e0c8d8d0c1c..ffae496829da1a 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -2166,15 +2166,19 @@ Utility functions .. function:: memoryview_at(ptr, size, readonly=False) - This function returns a :class:`memoryview` object of length *size* that - references the memory starting at *void \*ptr*, . - If *readonly* is true then the :class:`!memoryview` object is immutable. + 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 integer, *ptr* can also + (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 ctypes.buffer_at From 6b0a8ae6429dbe2f41985dfc04e79e29494ab0e1 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 29 Nov 2024 16:13:28 +0100 Subject: [PATCH 15/19] Fixups --- Doc/library/ctypes.rst | 2 +- Lib/ctypes/__init__.py | 4 +++- Lib/test/test_ctypes/test_memfunctions.py | 4 ++-- Modules/_ctypes/_ctypes.c | 1 - 4 files changed, 6 insertions(+), 5 deletions(-) diff --git a/Doc/library/ctypes.rst b/Doc/library/ctypes.rst index ffae496829da1a..cdd8948c5cf332 100644 --- a/Doc/library/ctypes.rst +++ b/Doc/library/ctypes.rst @@ -2181,7 +2181,7 @@ Utility functions (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 ctypes.buffer_at + .. audit-event:: ctypes.memoryview_at address,size,readonly .. versionadded:: next diff --git a/Lib/ctypes/__init__.py b/Lib/ctypes/__init__.py index 526623816efd1d..ec39ec1f6cca55 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -550,7 +550,8 @@ 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) +_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]) -> memoryview @@ -569,6 +570,7 @@ def wstring_at(ptr, size=-1): Return the wide-character string at void *ptr.""" return _wstring_at(ptr, size) + if _os.name == "nt": # COM stuff def DllGetClassObject(rclsid, riid, ppv): try: diff --git a/Lib/test/test_ctypes/test_memfunctions.py b/Lib/test/test_ctypes/test_memfunctions.py index d5dd79bf4c1654..9408789e987598 100644 --- a/Lib/test/test_ctypes/test_memfunctions.py +++ b/Lib/test/test_ctypes/test_memfunctions.py @@ -88,7 +88,7 @@ def test_memoryview_at(self): b, cast(b, c_void_p), byref(b), - addressof(b) + addressof(b), ): with self.subTest(foreign_ptr=type(foreign_ptr).__name__): b[:] = b"initialval" @@ -122,7 +122,7 @@ def test_memoryview_at_readonly(self): b, cast(b, c_void_p), byref(b), - addressof(b) + addressof(b), ): with self.subTest(foreign_ptr=type(foreign_ptr).__name__): b[:] = b"initialval" diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index a4f04481c3937d..1b02cbaf6208d3 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5934,7 +5934,6 @@ _ctypes_mod_exec(PyObject *mod) if (_ctypes_add_objects(mod) < 0) { return -1; } - return 0; } From 5600cef2f4a5fefec8a742865b47f4565fa75829 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 19 Dec 2024 14:33:10 +0100 Subject: [PATCH 16/19] Apply suggestions from code review MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: Bénédikt Tran <10796600+picnixz@users.noreply.github.com> --- Doc/whatsnew/3.14.rst | 7 ++++--- Lib/ctypes/__init__.py | 4 ++-- .../Library/2023-11-12-21-53-40.gh-issue-112015.2WPRxE.rst | 6 +++--- 3 files changed, 9 insertions(+), 8 deletions(-) diff --git a/Doc/whatsnew/3.14.rst b/Doc/whatsnew/3.14.rst index 2ae25ce868750a..8071d64f022e3c 100644 --- a/Doc/whatsnew/3.14.rst +++ b/Doc/whatsnew/3.14.rst @@ -328,11 +328,12 @@ ctypes * :func:`ctypes.memoryview_at` now exists to create a :class:`memoryview` object that refers to the supplied pointer and - length. Works just like :func:`ctypes.string_at` except it avoids a - buffer copy. Useful when implementing callback functions in Python - that are passed dynamically-sized buffers. + 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 881307bec2d950..8e2a2926f7a853 100644 --- a/Lib/ctypes/__init__.py +++ b/Lib/ctypes/__init__.py @@ -553,9 +553,9 @@ def string_at(ptr, size=-1): _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]) -> memoryview + """memoryview_at(ptr, size[, readonly]) -> memoryview - Return a memoryview representing the memory at addr.""" + Return a memoryview representing the memory at void *ptr.""" return _memoryview_at(ptr, size, bool(readonly)) try: 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 index b22f54311c2833..4b58ec9d219eff 100644 --- 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 @@ -1,5 +1,5 @@ :func:`ctypes.memoryview_at` now exists to create a :class:`memoryview` object that refers to the supplied pointer and -length. Works just like :func:`ctypes.string_at` except it avoids a -buffer copy. Useful when implementing callback functions in Python -that are passed dynamically-sized buffers. +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. From c3c9e175a2c2ed5abbb8413880435ece17e8cdcf Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 19 Dec 2024 14:32:15 +0100 Subject: [PATCH 17/19] Avoid extra variable --- Modules/_ctypes/_ctypes.c | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/Modules/_ctypes/_ctypes.c b/Modules/_ctypes/_ctypes.c index 1b02cbaf6208d3..131de4490fd13e 100644 --- a/Modules/_ctypes/_ctypes.c +++ b/Modules/_ctypes/_ctypes.c @@ -5744,12 +5744,11 @@ wstring_at(const wchar_t *ptr, int size) static PyObject * memoryview_at(void *ptr, Py_ssize_t size, int readonly) { - Py_ssize_t ssize = size; if (PySys_Audit("ctypes.memoryview_at", "nni", - (Py_ssize_t)ptr, ssize, readonly) < 0) { + (Py_ssize_t)ptr, size, readonly) < 0) { return NULL; } - if (ssize < 0) { + if (size < 0) { PyErr_Format(PyExc_ValueError, "memoryview_at: size is negative (or overflowed): %zd", size); From d41a2011c1317a0ea294cbd71bd4f1839ea3cdc5 Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Fri, 20 Dec 2024 14:13:24 +0100 Subject: [PATCH 18/19] Remove unneeded line --- Lib/test/test_ctypes/test_memfunctions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_ctypes/test_memfunctions.py b/Lib/test/test_ctypes/test_memfunctions.py index 9408789e987598..ce3508e8f6e9e6 100644 --- a/Lib/test/test_ctypes/test_memfunctions.py +++ b/Lib/test/test_ctypes/test_memfunctions.py @@ -116,7 +116,6 @@ def test_memoryview_at(self): def test_memoryview_at_readonly(self): b = (c_byte * 10)() - foreign_ptr = cast(b, c_void_p) size = len(b) for foreign_ptr in ( b, From 400a62decf8e641cb0a7c6f23bf59bb02918a78c Mon Sep 17 00:00:00 2001 From: Petr Viktorin Date: Thu, 2 Jan 2025 17:45:04 +0100 Subject: [PATCH 19/19] Remove unneeded line --- Lib/test/test_ctypes/test_memfunctions.py | 1 - 1 file changed, 1 deletion(-) diff --git a/Lib/test/test_ctypes/test_memfunctions.py b/Lib/test/test_ctypes/test_memfunctions.py index ce3508e8f6e9e6..325487618137f6 100644 --- a/Lib/test/test_ctypes/test_memfunctions.py +++ b/Lib/test/test_ctypes/test_memfunctions.py @@ -82,7 +82,6 @@ def test_wstring_at(self): def test_memoryview_at(self): b = (c_byte * 10)() - foreign_ptr = cast(b, c_void_p) size = len(b) for foreign_ptr in ( b,