Skip to content

Commit

Permalink
pythongh-112015: Implement ctypes.memoryview_at() (pythonGH-112018)
Browse files Browse the repository at this point in the history
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>
  • Loading branch information
4 people authored and srinivasreddy committed Jan 8, 2025
1 parent bf9975c commit 8f2cce6
Show file tree
Hide file tree
Showing 6 changed files with 120 additions and 1 deletion.
22 changes: 22 additions & 0 deletions Doc/library/ctypes.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
8 changes: 8 additions & 0 deletions Doc/whatsnew/3.14.rst
Original file line number Diff line number Diff line change
Expand Up @@ -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
--------

Expand Down
9 changes: 9 additions & 0 deletions Lib/ctypes/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand All @@ -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:
Expand Down
60 changes: 59 additions & 1 deletion Lib/test/test_ctypes/test_memfunctions.py
Original file line number Diff line number Diff line change
Expand Up @@ -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):
Expand Down Expand Up @@ -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()
Original file line number Diff line number Diff line change
@@ -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.
17 changes: 17 additions & 0 deletions Modules/_ctypes/_ctypes.c
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down Expand Up @@ -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
Expand Down

0 comments on commit 8f2cce6

Please sign in to comment.