BPositionIO in Haiku: Offset-Based Reads Without Shared Seek State
Use Haiku's BPositionIO and BDataIO interfaces for sequential and offset-based I/O, including partial transfers and exact-read helpers.
Haiku’s BDataIO interface describes objects that transfer bytes through Read() and Write(). BPositionIO extends that stream-oriented contract with ReadAt() and WriteAt(), which take an explicit file offset, plus Seek() for callers that need a cursor. This distinction is useful for file formats, indexes, and storage code where multiple operations must address known ranges without first mutating one shared seek position.
The positioned methods still return a byte count or an error. A successful call is not automatically a complete transfer of the requested buffer length. Code that assumes a short read means a full structure was loaded can parse uninitialized bytes or accept a truncated record.
off_t offset = 0;
ssize_t bytes = file.ReadAt(offset, header, sizeof(header));
if (bytes < 0) {
return static_cast<status_t>(bytes);
}
if (static_cast<size_t>(bytes) != sizeof(header)) {
return B_PARTIAL_READ;
}
This sketch assumes file is a BPositionIO implementation and header is a valid buffer. It deliberately treats a short read as an incomplete header instead of parsing a prefix. For a current Haiku SDK, use the exact-transfer helper when the caller’s contract requires all requested bytes.
Current Haiku exposes ReadAtExactly() and WriteAtExactly() on BPositionIO. They are convenience wrappers for callers whose record contract requires all requested bytes: the implementation repeats the positioned operation until it completes or an operation fails or stops making progress. A short file is therefore a failure for an exact header read, rather than permission to parse a partial header. If the application also needs to distinguish how many bytes arrived before failure, pass the optional byte-count output and record it.
size_t received = 0;
status_t status = file.ReadAtExactly(0, header, sizeof(header), &received);
if (status != B_OK) {
// Do not parse header. Preserve status and received for diagnostics.
return status;
}
The exact helper does not validate the format, byte order, version, checksum, or fields. It only establishes the requested transfer condition. Parse into a temporary structure, validate every length and enum, and publish the parsed object only after the complete header passes. For a variable-length record, read a small fixed header first, validate its declared size against a configured maximum and the known file size, then read exactly that many bytes. Never allocate directly from an unchecked on-disk length.
An exact write helper does not make the update transactional. If it reports failure after some bytes were written, those bytes may already have changed the destination. Do not blindly retry a record write unless the operation is idempotent and the range is still valid. For durable replacement, write a complete new representation away from the live file and publish it only after checking all writes and the finalization path.
Choose cursor-based or positioned access deliberately
Read()/Write() advance an object’s current position, which is convenient for parsers that consume one stream from beginning to end. ReadAt()/WriteAt() name the offset for each operation, making code easier to reason about when reading a header and then an index region, or when separate tasks inspect known ranges. Explicit offsets avoid the application-level race where one component seeks and another component changes the same object’s position before the read.
They do not guarantee that an underlying device, file, or implementation supports arbitrary random access efficiently. A wrapper may emulate positioning or impose its own constraints, so check the concrete object’s contract and propagate short transfers. Avoid arithmetic overflow when calculating offset plus length, validate ranges against file size, and distinguish a missing/truncated file from a malformed one.
When implementing a custom BPositionIO subclass, keep the sequential interface consistent with the positioned operations and document how EOF, partial writes, and invalid offsets are reported. Tests should cover a short file, exact boundary reads, offsets beyond the end, a partial write, and repeated reads at the same offset.
The class is an interface contract, not a promise that the storage is a regular file. A memory-backed object, a compressed stream, a resource view, or a device wrapper may implement position operations with different costs and limits. GetSize() may fail or return a size that becomes stale if another writer changes the object. Treat size checks as a defensive preflight, not as an atomic reservation. A later read can still see truncation, and a later write can still fail because of capacity or device state.
Keep cursor state separate from shared data
Read() and Write() operate at the object’s current position. That is a useful model for a single parser, but two components sharing one object can interfere if one seeks between another component’s Seek() and Read(). Positioned operations make the desired offset an explicit argument, removing that particular shared-cursor race. They do not make the contents immutable or guarantee an atomic snapshot of multiple reads. If a producer rewrites the same range concurrently, protect the transaction with a lock, a generation/version field, a copy-on-write file, or an application-level protocol.
For parallel readers, give each task a bounded range and keep all offset arithmetic checked. For example, before computing recordOffset + recordLength, reject negative offsets, lengths that exceed the format’s maximum, and values that would overflow off_t. Verify that the resulting end is within the size observed for the file, then still handle an actual short read. A file-size observation and the subsequent transfer are separate operations; external mutation can occur between them.
Positioned writes also need an explicit overlap policy. Two writes to disjoint ranges may still interact through an underlying wrapper, while overlapping writes have application-defined last-writer behavior unless the implementation documents stronger coordination. Do not use WriteAt() as a substitute for a transaction or durable commit. A robust file update commonly writes a new temporary file, checks each result, flushes/closes according to the storage contract, and renames it only after the new representation is complete.
Validate custom implementations and callers
A subclass should implement the positioned primitives and cursor/size operations consistently. The base class’s sequential operations are defined in terms of the current position; if a subclass overrides those methods, its alternate behavior must remain coherent with Position(), Seek(), and the positioned methods. Document whether seeking beyond the current end is allowed, what setting the size does to the cursor, and whether size changes can fail. Keep error codes distinguishable from byte counts: ReadAt() and WriteAt() return a signed transfer count, while the exact wrappers return a status and can separately report bytes transferred.
An acceptance test should use both a real file and an in-memory implementation. Test a zero-byte request, a complete transfer, a short read at end-of-file, a partial write, a negative offset, a very large offset, repeated reads that leave the logical cursor unchanged, a seek followed by sequential I/O, and a size change. Inject failures after a partial transfer to ensure the caller neither repeats the wrong range nor mistakes a prefix for a complete record. Run the same parser against truncated and oversized fixtures and assert that no object is published on failure.
Choose the abstraction to match the invariant: BDataIO for sequential byte flow, BPositionIO when stable offsets are part of the protocol, and higher-level storage APIs when the task is really directory identity, metadata, or atomic replacement. The positioned interface makes addressing explicit; reliability still depends on validating sizes, handling partial results, and coordinating mutation.
Related:
- Haiku BResources: Packaging Typed Data Inside Executables
- Haiku Node Monitoring: Receiving Live File-System Changes Through the Storage Kit
Sources: