Haiku BDataIO: Partial Transfers, Buffered Streams, and Memory I/O
Handle Haiku BDataIO short reads and writes, choose exact-transfer helpers, and use BBufferedDataIO, BMemoryIO, and BMallocIO with explicit ownership.
BDataIO is Haiku’s byte-stream interface for objects that read or write data without requiring callers to manage a shared seek position. It is the base for stream-like sources and sinks, and it also defines helpers for callers that require an exact byte count. The critical rule is that a Read() or Write() call returns a byte count, not a promise that the full request completed.
This is not a Haiku quirk. Files, sockets, pipes, and buffered wrappers can all have different transfer behavior. A parser that assumes one read fills a structure can misinterpret truncated data; a writer that assumes one call flushes a complete record can silently produce corrupt output. Use the actual return value and define how the caller distinguishes EOF, temporary unavailability, and a real error.
Interpret byte counts and status codes correctly
Read() and Write() return ssize_t, which can represent a positive byte count or a negative error result. A positive value smaller than the requested size is a short transfer, not automatically a failure. Preserve the number of bytes received and continue only when the stream contract permits it. Do not cast a negative result to size_t before checking it; that turns an error into an enormous apparent length.
For a fixed-size record, ReadExactly() and WriteExactly() express the higher-level requirement. They return status_t and can optionally report the number of bytes transferred. Check both the status and the byte count. “Exactly” describes the helper’s goal, not a guarantee that the stream contains the requested amount or will accept it. If the input ends early, the caller must reject the incomplete record or handle it as a format-specific partial state.
For a streaming parser, keep a bounded accumulation buffer and make progress explicit. If a call returns zero, do not spin in a tight loop hoping for new data. The meaning of zero depends on the underlying stream and its contract; for file-like input it commonly indicates end of available data, while a nonblocking device may require a different readiness mechanism. Consult the concrete implementation and wait through its documented mechanism.
Layer streams without losing ownership
BBufferedDataIO wraps another BDataIO to buffer operations. Its constructor accepts the underlying stream, buffer size, an ownsStream flag, and a partialReads policy. The header’s default ownership is true, so pass the flag explicitly whenever another object also owns the stream. Double ownership can cause a double delete; incorrect non-ownership can leave a dangling pointer.
Buffering changes when bytes are forwarded to the underlying stream. Call Flush() at deliberate durability or protocol boundaries and check its status. A successful Write() to the wrapper can mean bytes entered a user-space buffer, not that they reached a device or peer. Destruction and flush semantics differ from a transactional commit, so do not rely on object teardown as the only evidence that a record was delivered.
The partialReads option changes how the wrapper responds to read requests. Do not enable it without understanding how the parser expects data to arrive. A line-oriented consumer may accept partial data and accumulate it; a fixed-record consumer may need exact counts. Keep the policy close to the protocol adapter and test it with a stream that intentionally fragments reads.
Avoid layering several buffers without a reason. Each wrapper adds memory, delayed flush points, and possible latency. If a protocol already batches records, a second large buffer can make interactive output appear stuck. Measure throughput and tail latency, not only average bytes per second.
Use BMemoryIO for a bounded external buffer
BMemoryIO implements BPositionIO on top of a caller-supplied memory region. It can be constructed with a mutable pointer and length or a const pointer and length. The object does not turn the external memory into independently owned storage. Keep the backing memory alive for the entire BMemoryIO lifetime, do not reallocate it while the object uses it, and treat the const form as read-only.
BMallocIO is different: it manages an allocated in-memory stream and exposes its buffer and length for later use. It is useful for constructing a serialized message or response before writing it elsewhere, but it can grow and therefore needs a size budget. Check SetSize() results, bound untrusted input, and copy or consume its buffer before destroying the BMallocIO object.
Both classes implement position-based access in addition to the sequential BDataIO methods. If the same stream object is used from multiple threads, coordinate access to its position. For independent concurrent reads, use separate BMemoryIO views or explicit ReadAt() operations only when their ownership and synchronization rules are satisfied. A single seek position is shared mutable state.
Example: bounded exact record input
The following pattern demonstrates the status boundary for a fixed header:
uint8 header[kHeaderSize];
size_t bytesRead = 0;
status_t status = stream.ReadExactly(header, sizeof(header), &bytesRead);
if (status != B_OK) {
// Distinguish a truncated stream from an I/O error using status and count.
return status;
}
if (bytesRead != sizeof(header))
return B_BAD_DATA;
The application still has to decode fields with correct byte order, validate declared lengths before allocating, and ensure that the underlying stream’s exact-read behavior matches the intended timeout or nonblocking policy. Do not trust a length from the header until it is checked against a configured maximum and arithmetic overflow has been ruled out.
Design a reliable write loop
For output, distinguish “accepted by this wrapper” from “fully committed by the destination.” Use WriteExactly() for a fixed protocol frame when its semantics fit. If writing manually, advance the source pointer only by the returned positive count, stop on error, and bound retries. A stream that repeatedly makes no progress must not create an infinite loop.
For file output, use BPositionIO when offsets or truncation are part of the format contract. BDataIO alone intentionally does not promise random access. For sockets, use the networking APIs and their connection, timeout, and readiness contracts rather than assuming a successful stream write means a peer application processed the bytes.
When data must be durable, flush the appropriate layer and then use the concrete file or device durability operation supported by that type. A buffer flush is not necessarily an fsync. For a network protocol, use an acknowledgment if the application needs to know that a remote service accepted the record.
Compose parsers with bounded memory
Do not repeatedly concatenate attacker-controlled chunks into an unbounded BString or BMallocIO. Define a maximum message size, check each addition for overflow, and fail before memory grows beyond the budget. For streaming media, parse incrementally where possible and release consumed prefixes rather than retaining the whole file.
Keep ownership of each layer visible in code. If the outer wrapper owns the inner stream, the inner object must not also be deleted separately. If a caller supplies a fixed array to BMemoryIO, the caller owns and bounds it. If an object receives a pointer from BMallocIO::Buffer(), that pointer should not outlive a mutation or the BMallocIO instance.
Verification matrix
Use test streams that return one byte at a time, a short positive count, zero, an error after partial progress, and a write failure after several chunks. Verify exact helpers report incomplete data, parsers reject malformed lengths, and writers do not duplicate bytes after retries. Test BBufferedDataIO with both ownership values and with partial-read behavior enabled and disabled.
For BMemoryIO, test mutable and const backing buffers, exact end-of-buffer access, seek beyond supported bounds, and backing-memory destruction order. For BMallocIO, test empty streams, growth to the configured limit, retrieval of the buffer, and attempts to grow past the maximum. Run thread-safety tests if a stream is intentionally shared.
Record the concrete stream class, requested and actual byte counts, offset where applicable, buffer size, and status code in diagnostics. This makes “truncated input” distinguishable from “peer closed,” “storage full,” and “wrapper never flushed.”
BDataIO provides a common byte-transfer vocabulary, not a common delivery guarantee. Correct callers respect partial counts, check status, bound memory, and document who owns each stream. Those habits let the same parser logic work across files, memory, and network adapters without pretending they have identical semantics.
Related:
- BPositionIO in Haiku: Offset-Based Reads Without Shared Seek State
- Haiku BFile: Open Modes, Truncation Safety, and Reliable I/O
Sources: