Skip to content

Commit 7caa629

Browse files
authored
[mypyc] Add buffer protocol support for vec (#21359)
Now `vec` with value item types can be used as 1-dimensional buffers. This allows other code access raw memory in the vec buffer. Currently only read-only buffer access is supported. I relied on coding agent assist quite a bit.
1 parent a464c9a commit 7caa629

9 files changed

Lines changed: 170 additions & 2 deletions

File tree

mypy/typeshed/stubs/librt/librt/vecs.pyi

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ class vec(Generic[T]):
1616
def __setitem__(self, i: i64, o: T, /) -> None: ...
1717
def __contains__(self, o: object, /) -> bool: ...
1818
def __iter__(self) -> Iterator[T]: ...
19+
def __buffer__(self, flags: int, /) -> memoryview: ...
1920

2021
def append(v: vec[T], o: T, /) -> vec[T]: ...
2122
def remove(v: vec[T], o: T, /) -> vec[T]: ...

mypyc/lib-rt/vecs/vec_bool.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
#define BOX_ITEM VecBool_BoxItem
1515
#define UNBOX_ITEM VecBool_UnboxItem
1616
#define IS_UNBOX_ERROR VecBool_IsUnboxError
17+
#define BUFFER_FORMAT "b"
1718

1819
#include "vec_template.c"
1920

mypyc/lib-rt/vecs/vec_float.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#define UNBOX_ITEM VecFloat_UnboxItem
1616
#define IS_UNBOX_ERROR VecFloat_IsUnboxError
1717
#define BUFFER_FORMAT_CHAR_OK(c) ((c) == 'd')
18+
#define BUFFER_FORMAT "d"
1819

1920
#include "vec_template.c"
2021

mypyc/lib-rt/vecs/vec_i16.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#define UNBOX_ITEM VecI16_UnboxItem
1616
#define IS_UNBOX_ERROR VecI16_IsUnboxError
1717
#define BUFFER_FORMAT_CHAR_OK(c) ((c) == 'h')
18+
#define BUFFER_FORMAT "h"
1819

1920
#include "vec_template.c"
2021

mypyc/lib-rt/vecs/vec_i32.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#define UNBOX_ITEM VecI32_UnboxItem
1616
#define IS_UNBOX_ERROR VecI32_IsUnboxError
1717
#define BUFFER_FORMAT_CHAR_OK(c) ((c) == 'i' || ((c) == 'l' && sizeof(long) == 4))
18+
#define BUFFER_FORMAT "i"
1819

1920
#include "vec_template.c"
2021

mypyc/lib-rt/vecs/vec_i64.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#define UNBOX_ITEM VecI64_UnboxItem
1616
#define IS_UNBOX_ERROR VecI64_IsUnboxError
1717
#define BUFFER_FORMAT_CHAR_OK(c) ((c) == 'q' || ((c) == 'l' && sizeof(long) == 8))
18+
#define BUFFER_FORMAT "q"
1819

1920
#include "vec_template.c"
2021

mypyc/lib-rt/vecs/vec_template.c

Lines changed: 61 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -409,6 +409,16 @@ VEC FUNC(Append)(VEC vec, ITEM_C_TYPE x) {
409409
}
410410
}
411411

412+
inline static int vec_memory_overlaps(const void *p1, Py_ssize_t len1,
413+
const void *p2, Py_ssize_t len2) {
414+
if (len1 <= 0 || len2 <= 0)
415+
return 0;
416+
uintptr_t a = (uintptr_t)p1, b = (uintptr_t)p2;
417+
if (a <= b)
418+
return b - a < (uintptr_t)len1;
419+
return a - b < (uintptr_t)len2;
420+
}
421+
412422
// Extend 'dst' by appending 'n' items from 'items', stealing 'dst'.
413423
// Caller guarantees n > 0 and that 'items' remains valid for the call.
414424
// If force_alloc is true, always allocate a new buffer even when dst has capacity.
@@ -471,8 +481,14 @@ VEC FUNC(Extend)(VEC vec, PyObject *iterable) {
471481
}
472482
if (buf_ok) {
473483
Py_ssize_t n = view.len / (Py_ssize_t)sizeof(ITEM_C_TYPE);
474-
if (n > 0)
475-
vec = vec_extend_items(vec, (const ITEM_C_TYPE *)view.buf, n, 0);
484+
if (n > 0) {
485+
Py_ssize_t dst_bytes = n * (Py_ssize_t)sizeof(ITEM_C_TYPE);
486+
int force_alloc = vec.buf != NULL
487+
&& n <= VEC_CAP(vec) - vec.len
488+
&& vec_memory_overlaps(view.buf, view.len,
489+
vec.buf->items + vec.len, dst_bytes);
490+
vec = vec_extend_items(vec, (const ITEM_C_TYPE *)view.buf, n, force_alloc);
491+
}
476492
PyBuffer_Release(&view);
477493
return vec;
478494
}
@@ -566,6 +582,46 @@ static PyMappingMethods vec_mapping_methods = {
566582
.mp_subscript = vec_subscript,
567583
};
568584

585+
#ifdef BUFFER_FORMAT
586+
static int vec_getbuffer(VEC_OBJECT *self, Py_buffer *view, int flags) {
587+
if (view == NULL) {
588+
PyErr_SetString(PyExc_BufferError,
589+
"vec_getbuffer: view==NULL argument is obsolete");
590+
return -1;
591+
}
592+
if ((flags & PyBUF_WRITABLE) == PyBUF_WRITABLE) {
593+
PyErr_SetString(PyExc_BufferError, "Object is not writable");
594+
view->obj = NULL;
595+
return -1;
596+
}
597+
598+
view->obj = (PyObject *)self;
599+
Py_INCREF(self);
600+
view->buf = (self->vec.buf != NULL) ? (void *)self->vec.buf->items : NULL;
601+
view->len = self->vec.len * (Py_ssize_t)sizeof(ITEM_C_TYPE);
602+
view->readonly = 1;
603+
view->itemsize = sizeof(ITEM_C_TYPE);
604+
view->format = NULL;
605+
if ((flags & PyBUF_FORMAT) == PyBUF_FORMAT)
606+
view->format = BUFFER_FORMAT;
607+
view->ndim = 1;
608+
view->shape = NULL;
609+
if ((flags & PyBUF_ND) == PyBUF_ND)
610+
view->shape = &self->vec.len;
611+
view->strides = NULL;
612+
if ((flags & PyBUF_STRIDES) == PyBUF_STRIDES)
613+
view->strides = &view->itemsize;
614+
view->suboffsets = NULL;
615+
view->internal = NULL;
616+
617+
return 0;
618+
}
619+
620+
static PyBufferProcs vec_buffer_procs = {
621+
.bf_getbuffer = (getbufferproc)vec_getbuffer,
622+
};
623+
#endif
624+
569625
static PySequenceMethods vec_sequence_methods = {
570626
.sq_item = vec_get_item,
571627
.sq_ass_item = vec_ass_item,
@@ -667,6 +723,9 @@ PyTypeObject VEC_TYPE = {
667723
.tp_iter = vec_iter,
668724
.tp_as_sequence = &vec_sequence_methods,
669725
.tp_as_mapping = &vec_mapping_methods,
726+
#ifdef BUFFER_FORMAT
727+
.tp_as_buffer = &vec_buffer_procs,
728+
#endif
670729
.tp_richcompare = vec_richcompare,
671730
.tp_methods = vec_methods,
672731
};

mypyc/lib-rt/vecs/vec_u8.c

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
#define UNBOX_ITEM VecU8_UnboxItem
1616
#define IS_UNBOX_ERROR VecU8_IsUnboxError
1717
#define BUFFER_FORMAT_CHAR_OK(c) ((c) == 'B' || (c) == 'c')
18+
#define BUFFER_FORMAT "B"
1819

1920
#include "vec_template.c"
2021

mypyc/test-data/run-vecs-misc-interp.test

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -531,6 +531,108 @@ def test_extend_bool() -> None:
531531
v = extend(v, [False, True])
532532
assert v == vec[bool]([True, False, True])
533533

534+
[case testLibrtVecsBufferProtocol_librt_experimental]
535+
import struct
536+
from typing import Any
537+
538+
import librt.vecs
539+
import mypy_extensions
540+
541+
from testutil import assertRaises
542+
543+
vec: Any = librt.vecs.vec
544+
append: Any = librt.vecs.append
545+
extend: Any = librt.vecs.extend
546+
i64: Any = mypy_extensions.i64
547+
i32: Any = mypy_extensions.i32
548+
i16: Any = mypy_extensions.i16
549+
u8: Any = mypy_extensions.u8
550+
memoryview_: Any = eval('memoryview')
551+
552+
def test_buffer_u8() -> None:
553+
v = vec[u8]([1, 2, 3])
554+
mv = memoryview_(v)
555+
assert mv.format == "B"
556+
assert mv.itemsize == 1
557+
assert mv.readonly
558+
assert mv.ndim == 1
559+
assert mv.shape == (3,)
560+
assert list(mv) == [1, 2, 3]
561+
562+
def test_buffer_i64() -> None:
563+
v = vec[i64]([10, 20, -30])
564+
mv = memoryview_(v)
565+
assert mv.format == "q"
566+
assert mv.itemsize == 8
567+
assert mv.readonly
568+
assert mv.ndim == 1
569+
assert mv.shape == (3,)
570+
assert list(mv) == [10, 20, -30]
571+
572+
def test_buffer_i32() -> None:
573+
v = vec[i32]([5, -12])
574+
mv = memoryview_(v)
575+
assert mv.format == "i"
576+
assert mv.itemsize == 4
577+
assert mv.readonly
578+
assert list(mv) == [5, -12]
579+
580+
def test_buffer_i16() -> None:
581+
v = vec[i16]([100, -200])
582+
mv = memoryview_(v)
583+
assert mv.format == "h"
584+
assert mv.itemsize == 2
585+
assert mv.readonly
586+
assert list(mv) == [100, -200]
587+
588+
def test_buffer_float() -> None:
589+
v = vec[float]([1.5, -2.5])
590+
mv = memoryview_(v)
591+
assert mv.format == "d"
592+
assert mv.itemsize == 8
593+
assert mv.readonly
594+
assert list(mv) == [1.5, -2.5]
595+
596+
def test_buffer_bool() -> None:
597+
v = vec[bool]([True, False, True])
598+
mv = memoryview_(v)
599+
assert mv.format == "b"
600+
assert mv.itemsize == 1
601+
assert mv.readonly
602+
assert list(mv) == [1, 0, 1]
603+
604+
def test_buffer_empty() -> None:
605+
v = vec[i64]([])
606+
mv = memoryview_(v)
607+
assert mv.format == "q"
608+
assert len(mv) == 0
609+
assert mv.shape == (0,)
610+
assert list(mv) == []
611+
612+
def test_buffer_readonly() -> None:
613+
v = vec[u8]([1, 2, 3])
614+
mv = memoryview_(v)
615+
with assertRaises(TypeError):
616+
mv[0] = 99
617+
618+
def test_buffer_bytes_conversion() -> None:
619+
v = vec[u8]([72, 101, 108, 108, 111])
620+
assert bytes(v) == b"Hello"
621+
622+
def test_buffer_tobytes() -> None:
623+
v = vec[i32]([1, 2])
624+
mv = memoryview_(v)
625+
b = mv.tobytes()
626+
values = struct.unpack("ii", b)
627+
assert values == (1, 2)
628+
629+
def test_buffer_extend_from_shared_buffer() -> None:
630+
old = vec[u8]([1, 2], capacity=5)
631+
new = append(old, 3)
632+
v = extend(old, memoryview_(new))
633+
assert v == vec[u8]([1, 2, 1, 2, 3])
634+
assert new == vec[u8]([1, 2, 3])
635+
534636
[case testLibrtVecsFeaturesNotAvailableInNonExperimentalBuild_librt]
535637
# This also ensures librt.vecs can be built without experimental features
536638
import librt.vecs

0 commit comments

Comments
 (0)