Memory Buffers¶
The raw memory layer of the SimpleArray family consists of two untyped byte
buffers. ConcreteBuffer owns a fixed-size block of contiguous memory and is
the storage that every typed array sits on. BufferExpander is a growable
staging buffer for code that must accumulate bytes before the final size is
known, and can hand its content over as a ConcreteBuffer.
ConcreteBuffer¶
ConcreteBuffer is an untyped byte buffer: no dtype, no shape, and no stride.
Once constructed, its size never changes.
Construction¶
A buffer is constructed with a byte count and an optional alignment:
buf = solvcon.ConcreteBuffer(1024)
buf = solvcon.ConcreteBuffer(1024, alignment=64)
The memory is allocated immediately and is not initialized. The nbytes
property returns the byte count and alignment returns the requested
alignment. Valid alignment values are 0 (the default, no specific alignment),
16, 32, and 64 bytes; any other value raises ValueError. When a non-zero
alignment is requested, the byte count must be a multiple of the alignment, or
the allocation raises ValueError. A zero-byte buffer is permitted and
records the requested alignment without allocating.
Size and Byte Access¶
The buffer sizes and indexes like a Python sequence of bytes. len(buf)
equals buf.nbytes. Indexing with buf[i] reads one byte and assignment
buf[i] = value writes one byte; the element type is a signed 8-bit integer.
Indexing at or beyond the size raises IndexError:
buf = solvcon.ConcreteBuffer(10)
for it in range(len(buf)):
buf[it] = it
buf[10] # IndexError: ConcreteBuffer: index 10 is out of bounds ...
Iteration follows from indexing, so list(buf) returns the byte values.
Cloning¶
clone() returns a deep copy: a new buffer with the same byte count, the same
alignment, and a copy of the content. Writes to either buffer are not visible
in the other.
Wrapping a Numpy Array¶
The second constructor form wraps the memory of an existing numpy array instead of allocating:
ndarr = np.arange(24, dtype='float64').reshape((2, 3, 4))
buf = solvcon.ConcreteBuffer(array=ndarr)
assert buf.nbytes == ndarr.nbytes
The dtype and shape of the source array do not matter, but the array must be C-
or F-contiguous; other layouts raise ValueError. The buffer covers its
nbytes bytes and shares them zero-copy, so writing through one side is
visible on the other. The constructor also accepts the optional alignment
keyword, validated against the same set (0, 16, 32, or 64); no size-multiple
check applies because nothing is allocated. The buffer holds a reference to the
source array, tying the lifetime of the memory to the Python object. The
is_from_python property reports the provenance: it is True for a buffer
wrapping a numpy array and False otherwise, including a buffer over memory
that is owned by something other than a numpy array.
BufferExpander¶
BufferExpander is an untyped byte buffer that grows. It distinguishes its
size (the bytes currently in use) from its capacity (the bytes allocated). Its
role is a staging area: accumulate bytes while the final length is unknown,
then hand the result over as a ConcreteBuffer for fixed-size access. The
internal expandable memory is never exposed to other components.
Construction¶
Three forms are supported:
ep = solvcon.BufferExpander() # empty: size 0, capacity 0
ep = solvcon.BufferExpander(10) # size 10, capacity 10
ep = solvcon.BufferExpander(buf) # copy of a ConcreteBuffer
The third form initializes size and capacity to the byte count of the given
ConcreteBuffer and copies its content; the expander does not alias the
source buffer, so later writes to the expander leave the source unchanged. The
sized and buffer-copying forms accept an optional alignment keyword with the
same valid values as ConcreteBuffer (0, 16, 32, or 64); the empty form fixes
the alignment at 0. The read-only alignment property returns the value given
at construction.
Growing¶
reserve(cap) grows the capacity to at least cap bytes while keeping the
size and the existing content; when cap does not exceed the current capacity
it does nothing. Capacity never shrinks. expand(length) reserves length
bytes and then sets the size to length:
ep = solvcon.BufferExpander()
ep.reserve(10) # capacity 10, size still 0
ep.expand(10) # capacity 10, size 10
The capacity property returns the allocated byte count and len(ep) returns
the size.
Byte Access¶
Indexing works as on ConcreteBuffer: ep[i] reads and ep[i] = value
writes single signed 8-bit bytes. Bounds are checked against the size, not the
capacity, so a reserved-but-unexpanded region is not addressable and raises
IndexError.
Producing a ConcreteBuffer¶
Two methods convert the staged bytes into a ConcreteBuffer, differing in
whether the result shares memory with the expander:
copy_concrete(cap=0)returns an independent copy. The new buffer holdsmax(cap, len(ep))bytes with the staged content copied in; later writes on either side do not affect the other.as_concrete(cap=0)converts the expander in place. The expander adopts aConcreteBufferas its storage and returns it; from then on the two objects share memory, so a write through one is visible through the other. When the expander is not yet concrete, the adopted buffer holdsmax(cap, len(ep))bytes; when it already is,capis ignored and the existing buffer is returned.
clone() returns a new independent BufferExpander with a copy of the
content.
The is_concrete property reports whether the expander is currently backed by
a ConcreteBuffer. It is False for a freshly expanded buffer, True after
as_concrete(), and True for an expander constructed from a
ConcreteBuffer:
ep = solvcon.BufferExpander(10)
assert not ep.is_concrete
cbuf = ep.as_concrete()
assert ep.is_concrete
cbuf[0] = 42
assert ep[0] == 42 # memory is shared
Growing the capacity after as_concrete() reallocates the storage and
detaches the expander from the concrete buffer: is_concrete drops back to
False and the previously returned buffer keeps the old memory.