How to Buffer a Pascal Stream Without Breaking Its Cursor

Reading a binary record one field at a time can turn a small logical operation into many calls to the underlying stream. Read-ahead can reduce those calls, but it creates a second cursor because the source has already supplied bytes that the application has not yet consumed. Confusing those positions causes subtle corruption when a reader later writes, seeks or hands the stream back.

Sphere 10 Pascal Framework's TBufferedStream makes that distinction explicit. The example below reads two single-byte fields through a four-byte buffer. We count physical reads, compare the two positions and explicitly return unread bytes before changing mode. We also show that length caching is an independent choice, not a synonym for buffering data.

A buffered stream with distinct logical and underlying cursor positions.
A buffered stream with distinct logical and underlying cursor positions.

Contents

Follow the Bytes

The source contains ten ASCII bytes. The first one-byte read fills a four-byte buffer. The second consumes the next buffered byte. The caller has consumed two bytes, so the decorator's logical position is two. The wrapped source has supplied four bytes, so its physical position is four. One physical read has served both logical reads.

That difference is normal. Do not access the wrapped source independently while the decorator is active. Doing so bypasses its cursor bookkeeping. Use the decorator for the complete operation, or finish an explicit handover before another owner continues with the raw stream.

Count Calls Instead of Inventing a Speedup

Our TCountedStream records read and size-query calls. The example asserts one physical read for the first two bytes. It then calls FlushUnreadData, which rewinds the unread region on this seekable memory stream, before disabling read buffering.

The source remains caller-owned because AOwnsStream is false. The surrounding try/finally frees the decorator first and its input afterwards. Construction with ownership enabled needs the corresponding successful-construction handover so an allocation failure cannot leak the original stream.


program ArticleSample;
{$IFDEF FPC}{$mode Delphi}{$H+}{$ENDIF}
uses Classes, SysUtils, UStreams;
type
  TCountedStream = class(TMemoryStream)
  public
    ReadCalls: Integer;
    SizeCalls: Integer;
    function Read(var Buffer; Count: LongInt): LongInt; override;
  protected
    function GetSize: Int64; override;
  end;

function TCountedStream.Read(var Buffer; Count: LongInt): LongInt;
begin
  Inc(ReadCalls);
  Result := inherited Read(Buffer, Count);
end;

function TCountedStream.GetSize: Int64;
begin
  Inc(SizeCalls);
  Result := inherited GetSize;
end;

var
  Source: TCountedStream;
  Buffered: TBufferedStream;
  Data: AnsiString;
  Value: Byte;
  LengthValue: Int64;
begin
  Source := TCountedStream.Create;
  try
    Data := '0123456789';
    Source.WriteBuffer(Data[1], Length(Data));
    Source.Position := 0;
    Buffered := TBufferedStream.Create(Source, 4, False, True, True);
    try
      Buffered.ReadBuffer(Value, 1);
      Buffered.ReadBuffer(Value, 1);
      if (Buffered.Position <> 2) or (Source.Position <> 4) then
        raise Exception.Create('Read-ahead cursor mismatch');
      if Source.ReadCalls <> 1 then raise Exception.Create('Expected one physical read');
      WriteLn('Logical position=2; physical position=4; reads=1');
      Buffered.FlushUnreadData;
      if Source.Position <> 2 then raise Exception.Create('Rewind failed');
      Buffered.EnableReadBuffering := False;
      Source.SizeCalls := 0;
      LengthValue := Buffered.Size;
      LengthValue := Buffered.Size;
      if Source.SizeCalls <> 1 then raise Exception.Create('Independent size cache failed');
      Buffered.EnableStreamLengthCaching := False;
      LengthValue := Buffered.Size;
      LengthValue := Buffered.Size;
      if Source.SizeCalls <> 3 then raise Exception.Create('Uncached size checks failed');
      if LengthValue <> 10 then raise Exception.Create('Length changed');
      WriteLn('Read buffering=False; length caching independently selectable');
    finally
      Buffered.Free;
    end;
  finally
    Source.Free;
  end;
end.

Check the Cursor and Length Policies


Logical position=2; physical position=4; reads=1
Read buffering=False; length caching independently selectable

After the rewind, both readers agree that the next source byte is at position two. With read buffering disabled and length caching still enabled, two Size queries call the source once. Disabling only length caching makes the next two Size queries delegate separately. These flags can be selected independently.

The call counts are a diagnostic demonstration, not a throughput benchmark. Files, memory streams and network adapters have different costs. Measure the actual access pattern, include output validation and distinguish byte-read calls from metadata queries when comparing buffer settings.

Respect Forward-Only Sources

The decorator accepts a TStream whose sequential reads work even if seeking, Size or Position are unavailable. Construction does not probe those capabilities. A read can return a positive short result after at most one wrapped read. It returns control without waiting to fill a request while a peer may be waiting for the application's response.

Rewinding unread data is impossible when the source cannot seek. In that case an explicit flush or a transition that requires synchronisation fails and retains the unread bytes. Do not silently discard those bytes or pretend that disabling the buffer makes them reappear in the source.

ℹ️ Disabling read buffering while unread bytes remain is rejected before changing the mode. Consume them or explicitly flush on a source that supports rewinding. Destruction itself does not seek.

Choose a Policy for the Workload

Small serialized fields benefit from bounded read-ahead. An application already reading large blocks may prefer direct reads to avoid another copy. Immutable inputs can use lazy length caching, while independently growing sources need an appropriate uncached policy. Writes and resizes invalidate cached length before attempted mutation, including failure paths.

The exact companion stream unit supplies the implementation used by this example. Its original MIT license and version provenance are retained in the download. Start with a small counted example like this one, establish the cursor and ownership rules, then tune buffer size with the real workload. Faster I/O is useful only when the bytes and lifetime remain correct.

sample.zip
Download the complete worked example and build instructions (ZIP).

Get the Framework and Run the Example

Get Sphere 10 Pascal Framework and follow its package instructions . Use the units directly, or build the optional Lazarus .lpk or Delphi runtime package. Pascal uses these source/package workflows. The C# Framework is distributed separately through NuGet.

The attached sample was compiled and executed with Free Pascal 3.2.0 on Windows using the pinned public base plus the included, versioned UStreams.pas companion. Range and overflow checks were enabled. Its Build.ps1 accepts the Framework, compiler, Lazarus and output paths so the example does not need machine-specific paths. Lazarus UI examples require the LCL Win32 widgetset. A successful FPC run does not claim a new Delphi build. Follow the repository's separate Delphi/VCL project and resource instructions there.

ℹ️ This stream article includes an exact, unchanged UStreams.pas companion from source revision cb02a74. Keep it beside Sample.lpr. The companion unit is newer than the pinned public base revision and is supplied explicitly with this example. Its inclusion does not mean the older snapshot contains it or that a new Framework release has been published. The complete sample was also checked against the public base plus this companion.

Download the complete runnable sample.

sample.zip
Download the complete worked example and build instructions (ZIP).

Continue the Series

How to Give Pascal Resources Automatic Cleanup

How to Page and Sort Data Without Fetching the Same Snapshot Again

For the broader Sphere 10 development toolkit, see the Sphere10 Framework overview .