How to Buffer a Pascal Stream Without Breaking Its Cursor
Binary decoders usually read a stream one small field at a time. Each field becomes a separate Read call on the underlying stream, which is costly when that stream is a file or a socket. Read-ahead buffering removes those calls, but it also moves the underlying stream's cursor ahead of the position the decoder has actually reached.
TBufferedStream in Sphere 10 Pascal Framework is a TStream decorator that adds bounded read-ahead to any stream and keeps the two cursors coherent. This article covers how to wrap a stream, what each constructor option controls, and how to hand the underlying stream to another consumer at the correct position.
1. Wrap the input and reuse the decoder
To buffer the input, construct the decorator over the source and pass the decorator to the decoder that already reads the source.
Buffered := TBufferedStream.Create(Source, 16, False, True, True);
try
BufferedSum := SumItems(Buffered);
BufferedCalls := Source.ReadCalls;
finally
Buffered.Free;
end;
The constructor takes the wrapped stream and four options, all of which have defaults.
-
ABufferSize(default 65536) is the size in bytes of the single read-ahead buffer. -
AOwnsStream(defaultFalse) makes freeing the decorator also free the wrapped stream. -
AEnableStreamLengthCaching(defaultTrue) retains one successfulSizequery. Disable it when the input can grow independently. -
AEnableReadBuffering(defaultTrue) turns read-ahead on. When it is off, reads are forwarded directly.
Both flags are also writable properties, EnableReadBuffering and EnableStreamLengthCaching, and EnableBuffering and DisableBuffering set both at once. The sample uses a deliberately small 16-byte buffer so that its five-byte records straddle buffer boundaries. Reading 100 bytes through it takes 7 reads on the source instead of 100.
Read calls on the source: direct=100; buffered=7
Those counts describe this in-memory example. They show the mechanism and are not a throughput measurement for files or sockets.
2. Keep the decoder independent of buffering
TBufferedStream descends from TStream, so existing code that reads from a TStream needs no change. The sample's decoder reads a five-byte record with five single-byte reads and knows nothing about buffering.
function ReadItem(ASource: TStream): TItem;
var
J: Integer;
Field: Byte;
begin
ASource.ReadBuffer(Result.Tag, 1);
Result.Value := 0;
for J := 0 to 3 do begin
ASource.ReadBuffer(Field, 1);
Result.Value := Result.Value or (LongWord(Field) shl (J * 8));
end;
end;
3. The two cursors
After any buffered read there are two positions. The decorator's Position is the logical cursor, which is where the caller's next read will come from. The wrapped stream's Position is the physical cursor, which is wherever the last read-ahead finished.
The first row is the 16-byte buffer after the decoder has taken one five-byte record. The logical cursor sits at byte 5, where the unread part begins, and the physical cursor sits at byte 16, where the buffer ends. The sample observes exactly this after decoding one record.
Diagram source
---
config:
packet:
bitsPerRow: 16
bitWidth: 22
showBits: false
---
packet-beta
0-4: "Read, 0 to 4"
5-15: "In buffer, unread, 5 to 15"
16-31: "Still in the wrapped stream, 16 onward"
After one record: logical=5; underlying=16
While the decorator is active, all access should go through it. Reading or seeking the wrapped stream directly would use the physical cursor and skip the bytes that are still waiting in the buffer.
4. Hand over at the logical position
When another consumer needs the raw stream, call FlushUnreadData first. It seeks the wrapped stream back by the number of unread bytes, so the physical cursor matches the logical one, and then empties the buffer.
Diagram source
---
config:
sequence:
actorMargin: 8
width: 96
mirrorActors: false
messageMargin: 28
noteMargin: 8
---
sequenceDiagram
participant D as Decoder
participant B as TBufferedStream
participant S as Source
D->>B: Read 1 byte
B->>S: Read 16 bytes
D->>B: Read 4 bytes
Note over B: served from buffer
D->>B: FlushUnreadData
B->>S: Seek back 11
Note over S: Position = 5
First := ReadItem(Buffered);
if (First.Tag <> 1) or (Buffered.Position <> 5) or (Source.Position <> 16) then
raise Exception.Create('Logical and underlying cursors differ from expected positions');
WriteLn('After one record: logical=', Buffered.Position, '; underlying=', Source.Position);
Buffered.FlushUnreadData;
if Source.Position <> 5 then
raise Exception.Create('FlushUnreadData did not rewind the unread bytes');
WriteLn('After FlushUnreadData: underlying=', Source.Position);
finally
Buffered.Free;
end;
Second := ReadItem(Source);
The destructor never seeks. Freeing a decorator without flushing leaves a borrowed stream at its physical cursor, which is why the handover is an explicit call. SynchronizePosition is a compatibility alias for FlushUnreadData.
5. Choose the input capabilities deliberately
The decorator accepts any TStream and asks only for the capabilities an operation needs.
-
Sequential reads need nothing beyond
Read. Construction does not query the size or position, so forward-only streams work. -
FlushUnreadDataneeds a seekable input. If the seek is rejected, the call raises and the unread bytes stay in the buffer. -
Disabling read buffering while unread bytes remain raises before anything changes. Consume the bytes or flush them first.
-
Writes are forwarded immediately and are not buffered. A write first returns unread bytes to the input and invalidates the cached length.
TBufferedStream adds no synchronization. Use it from one thread at a time, or protect it with your own lock.
The complete contract is documented in the Framework's stream unit .
6. Get the Framework and run the example
Get Sphere 10 Pascal Framework and follow its package instructions . You can use the units directly or build the optional Lazarus or Delphi runtime package.
The sample reads the same twenty records directly and through the decorator, compares the results and the number of Read calls, and then performs the handover shown above. It was compiled and run on Windows with Free Pascal 3.2.0 against
source revision
2cc6558
. Its Build.ps1 accepts the Framework, compiler and output paths.
Download the complete runnable sample.
For the broader Sphere 10 development toolkit, see the Sphere10 Framework overview .
Author: Herman Schoenfeld