The C++ standard library and the Unix syscall interface meet at a byte
boundary. read and write return a signed count: positive progress, zero, or
minus one with errno. Check that result before converting it to size_t.
A successful syscall can transfer fewer bytes than requested.
State a loop can actually observe
After a positive result, keep that count and advance exactly that far. After EINTR, retry the remaining operation. An ordinary read returning zero is EOF. On a nonblocking stream, EAGAIN means no progress is available now; it is not EOF, and spinning on it wastes a core. Permanent errors stay explicit.
The build accepts injected syscall callbacks so short transfers and interrupted calls can be tested without a timing race. Its default callbacks still use real pread/pwrite on real files. This is a test seam around the operation, not a replacement for understanding its contract.
Positioned operations and shared cursors
lseek followed by read is two operations on shared cursor state. Another
user of a duplicated or inherited descriptor can change that state in between.
pread(fd, buffer, count, offset) supplies a position for this operation and
leaves the open file description's ordinary cursor unchanged.
#include <cassert>
#include <cstdio>
#include <cstdlib>
#include <fcntl.h>
#include <unistd.h>
int main() {
char name[]="/tmp/unix-check-XXXXXX"; int fd=::mkstemp(name); assert(fd>=0); ::unlink(name);
assert(::write(fd,"abcdef",6)==6); assert(::lseek(fd,0,SEEK_SET)==0);
assert(::lseek(fd,4,SEEK_SET)==4); char c=0; assert(::pread(fd,&c,1,1)==1);
std::printf("%c %ld\n",c,long(::lseek(fd,0,SEEK_CUR))); ::close(fd);
}Use the same idea for pwrite, but read platform exceptions: on Linux an O_APPEND descriptor makes pwrite append despite its explicit offset. The build rejects append mode rather than silently violating its positioned-write promise. It does not clear the flag behind another owner's back.
Range checks and zero progress
Validate a nonempty range before computing its end: size <= maximum - offset
after checking offset is in range. Otherwise arithmetic can wrap before an
apparently reasonable syscall argument is constructed. The teaching helpers
cap each window at one MiB; this bounds allocation and keeps exercises small.
#include <array>
#include <cstdio>
int main() {
int done=0,calls=0;
for(int n:std::array{2,1,0,8}) { ++calls; if(n==0)break; done+=n; }
std::printf("%d %d\n",done,calls);
}An empty request is a no-op in this API. A nonempty write returning zero must
not spin forever; the wrapper reports EIO. A read that reaches EOF can still
return a useful partial window. A different application might need an
exact_read result distinguishing a truncated record from clean EOF; state that
policy in its interface rather than hiding it in a loop.
Buffering is not durability
C stdio may buffer in user space. The kernel also caches file data. A successful write makes progress under its syscall contract, but is not a promise that a power failure cannot lose the data. This build does not add fsync or claim to measure storage speed. Error-injection tests verify the loop's accounting; ordinary fixtures verify real file contents and an unchanged shared cursor.
Before the fill, walk a sequence of EINTR, two bytes, one byte and EOF. Keep both the requested offset and the accumulated result explicit. Continue with filesystem operations, then processes.