Skip to content

Commit 76a41c8

Browse files
authored
refine documentation for PyCustomMessageBuilder (#395)
* refine document for py custom message builder * refine example * refine example
1 parent aa90ab6 commit 76a41c8

3 files changed

Lines changed: 111 additions & 26 deletions

File tree

capnp/lib/capnp.pyx

Lines changed: 47 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1314,10 +1314,11 @@ cdef class _DynamicStructReader:
13141314
:type num_first_segment_words: int
13151315
:param num_first_segment_words: Size of the first segment to allocate (in words ie. 8 byte increments)
13161316
1317-
:type allocate_seg_callable: Callable[[int], bytearray]
1317+
:type allocate_seg_callable: Callable[[int], Buffer]
13181318
:param allocate_seg_callable: A python callable object that takes the minimum number of 8-byte
1319-
words to allocate (as an `int`) and returns a `bytearray`. This is used to customize the memory
1320-
allocation strategy.
1319+
words to allocate (as an `int`) and returns any object supporting the writable buffer protocol
1320+
(e.g., `bytearray`, `memoryview`, `numpy.ndarray`). This enables custom memory allocation
1321+
strategies including shared memory.
13211322
13221323
:rtype: :class:`_DynamicStructBuilder`
13231324
"""
@@ -1700,10 +1701,11 @@ cdef class _DynamicStructBuilder:
17001701
:type num_first_segment_words: int
17011702
:param num_first_segment_words: Size of the first segment to allocate (in words ie. 8 byte increments)
17021703
1703-
:type allocate_seg_callable: Callable[[int], bytearray]
1704+
:type allocate_seg_callable: Callable[[int], Buffer]
17041705
:param allocate_seg_callable: A python callable object that takes the minimum number of 8-byte
1705-
words to allocate (as an `int`) and returns a `bytearray`. This is used to customize the memory
1706-
allocation strategy.
1706+
words to allocate (as an `int`) and returns any object supporting the writable buffer protocol
1707+
(e.g., `bytearray`, `memoryview`, `numpy.ndarray`). This enables custom memory allocation
1708+
strategies including shared memory.
17071709
17081710
:rtype: :class:`_DynamicStructBuilder`
17091711
"""
@@ -3891,15 +3893,27 @@ cdef class _PyCustomMessageBuilder(_MessageBuilder):
38913893
This callable object will be invoked in the allocateSegment method of the MessageBuilder
38923894
to allocate memory. The allocated memory will be managed within the MessageBuilder.
38933895
3894-
:type allocate_seg_callable: Callable[[int], bytearray]
3896+
:type allocate_seg_callable: Callable[[int], Buffer]
38953897
:param allocate_seg_callable: A python callable object that takes the minimum number of 8-byte
3896-
words to allocate (as an `int`) and returns a `bytearray`. This is used to customize the memory
3897-
allocation strategy.
3898+
words to allocate (as an `int`) and returns any object supporting the writable buffer protocol
3899+
(e.g., `bytearray`, `memoryview`, `numpy.ndarray`). This enables custom memory allocation
3900+
strategies including shared memory.
38983901
38993902
Required function signature is like this:
3900-
def __call__(self, minimum_size: int) -> bytearray:
3903+
def __call__(self, minimum_size: int) -> Buffer:
3904+
3905+
Where `Buffer` is any object that:
3906+
- Supports the Python buffer protocol (PyObject_GetBuffer)
3907+
- Is writable
39013908
Note that the unit of minimum_size is words, ie. 8 byte increments.
39023909
3910+
The underlying memory must remain valid for the lifetime of the MessageBuilder.
3911+
If returning a view (e.g., `memoryview`, `numpy.ndarray`) that wraps external memory,
3912+
the allocator is responsible for properly managing the memory lifecycle。
3913+
3914+
Examples:
3915+
3916+
# Example 1: Simple bytearray allocator
39033917
class Allocator:
39043918
def __init__(self):
39053919
self.cur_size = 0
@@ -3911,9 +3925,32 @@ cdef class _PyCustomMessageBuilder(_MessageBuilder):
39113925
return bytearray(byte_count)
39123926
39133927
addressbook = capnp.load('addressbook.capnp')
3928+
allocator = Allocator()
39143929
message = capnp._PyCustomMessageBuilder(allocator)
39153930
person = message.init_root(addressbook.Person)
39163931
3932+
# Example 2: Shared memory allocator (zero-copy)
3933+
import ctypes
3934+
3935+
class ShmAllocator:
3936+
def __init__(self, shm_pool):
3937+
self.shm = shm_pool
3938+
self.buffers = []
3939+
3940+
def __call__(self, minimum_size: int) -> memoryview:
3941+
size = minimum_size * 8
3942+
ptr = self.shm.allocate(size)
3943+
buffer = (ctypes.c_uint8 * size).from_address(ptr)
3944+
self.buffers.append(buffer)
3945+
return memoryview(buffer)
3946+
3947+
def release(self):
3948+
for buffer in self.buffers:
3949+
ptr = ctypes.addressof(buffer)
3950+
size = ctypes.sizeof(buffer)
3951+
self.shm.deallocate(ptr, size)
3952+
self.buffers.clear()
3953+
39173954
:type size: int
39183955
:param size: Size of the first segment to allocate (in words ie. 8 byte increments)
39193956
"""

examples/py_custom_message_builder.py

Lines changed: 24 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -23,26 +23,43 @@ def __call__(self, minimum_size: int) -> bytearray:
2323
return bytearray(byte_count)
2424

2525

26+
class MemoryViewAllocator:
27+
def __init__(self):
28+
self.buffers = []
29+
30+
def __call__(self, minimum_size: int) -> memoryview:
31+
WORD_SIZE = 8
32+
buffer = bytearray(minimum_size * WORD_SIZE)
33+
self.buffers.append(buffer)
34+
return memoryview(buffer)
35+
36+
2637
person = addressbook_capnp.Person.new_message(allocate_seg_callable=Allocator())
2738

2839
person.init("extraData", 5)
2940
print(person.extraData)
30-
print(bytes(person.extraData))
3141
print(type(person.extraData))
3242
print()
3343

34-
person.extraData[1] = 0xFF
44+
person.extraData = b"hello"
3545
print(person.extraData)
36-
print(bytes(person.extraData))
46+
print(type(person.extraData))
3747
print()
3848

39-
person.extraData = b"hello"
49+
person = person.as_reader()
4050
print(person.extraData)
41-
print(bytes(person.extraData))
4251
print(type(person.extraData))
4352
print()
4453

45-
person = person.as_reader()
54+
person = addressbook_capnp.Person.new_message(
55+
allocate_seg_callable=MemoryViewAllocator()
56+
)
57+
58+
person.init("extraData", 5)
59+
print(person.extraData)
60+
print(type(person.extraData))
61+
print()
62+
63+
person.extraData = b"world"
4664
print(person.extraData)
47-
print(bytes(person.extraData))
4865
print(type(person.extraData))

test/test_py_custom_message_builder.py

Lines changed: 40 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,14 @@ def all_types():
1212
return capnp.load(os.path.join(this_dir, "all_types.capnp"))
1313

1414

15-
def test_addressbook(all_types):
15+
def test_bytearray_allocator(all_types):
1616
class Allocator:
1717
def __init__(self):
1818
self.cur_size = 0
1919
self.last_size = 0
2020

2121
def __call__(self, minimum_size: int) -> bytearray:
2222
actual_size = max(minimum_size, self.cur_size)
23-
print(
24-
f"minimum_size: {minimum_size}, last_size: {self.last_size}, "
25-
f"actual_size: {actual_size}, cur_size: {self.cur_size}"
26-
)
2723
self.last_size = actual_size
2824
self.cur_size += actual_size
2925
WORD_SIZE = 8
@@ -39,10 +35,45 @@ def __call__(self, minimum_size: int) -> bytearray:
3935
assert allocator.last_size == 1024
4036

4137
struct_builder.init("dataField", 5)
42-
assert struct_builder._get("dataField") == b"\x00\x00\x00\x00\x00"
38+
assert bytes(struct_builder._get("dataField")) == b"\x00\x00\x00\x00\x00"
4339

4440
struct_builder.dataField = b"hello"
45-
assert struct_builder._get("dataField") == b"hello"
41+
assert bytes(struct_builder._get("dataField")) == b"hello"
4642

47-
struct_builder = struct_builder.as_reader()
48-
assert struct_builder._get("dataField") == b"hello"
43+
struct_reader = struct_builder.as_reader()
44+
assert bytes(struct_reader._get("dataField")) == b"hello"
45+
46+
47+
def test_memoryview_allocator(all_types):
48+
class MemoryViewAllocator:
49+
def __init__(self):
50+
self.cur_size = 0
51+
self.last_size = 0
52+
self.buffers = []
53+
54+
def __call__(self, minimum_size: int) -> memoryview:
55+
actual_size = max(minimum_size, self.cur_size)
56+
self.last_size = actual_size
57+
self.cur_size += actual_size
58+
WORD_SIZE = 8
59+
byte_count = actual_size * WORD_SIZE
60+
buffer = bytearray(byte_count)
61+
self.buffers.append(buffer)
62+
return memoryview(buffer)
63+
64+
allocator = MemoryViewAllocator()
65+
assert allocator.cur_size == 0
66+
assert allocator.last_size == 0
67+
msg_builder = capnp._PyCustomMessageBuilder(allocator, 1024)
68+
struct_builder = msg_builder.init_root(all_types.TestAllTypes)
69+
assert allocator.cur_size == 1024
70+
assert allocator.last_size == 1024
71+
72+
struct_builder.init("dataField", 5)
73+
assert bytes(struct_builder._get("dataField")) == b"\x00\x00\x00\x00\x00"
74+
75+
struct_builder.dataField = b"hello"
76+
assert bytes(struct_builder._get("dataField")) == b"hello"
77+
78+
struct_reader = struct_builder.as_reader()
79+
assert bytes(struct_reader._get("dataField")) == b"hello"

0 commit comments

Comments
 (0)