PDFlibPas는 제한된 읽기 전용 memory-mapped view를 통해 로컬 PDF를 열 수 있습니다. LoadFromMappedFile과 DAOpenMappedFile은 파일 위에 정확히 하나의 sliding window를 유지하고 필요할 때 다시 매핑하며 모든 object 조각을 absolute-offset read로 제공합니다. Delphi PDF library는 source 전체를 메모리에 보관하지 않으므로 파일이 커져도 address-space 사용량은 일정하게 유지됩니다. 이 설계가 겨냥하는 workload는 하나입니다. parser가 로딩을 끝냈는데도 object 단위, stream fragment 단위로 디스크에 계속 돌아가야 하는 기가바이트급 PDF입니다
PDF를 로드한 뒤에도 드문드문 읽기가 비싼 이유
PDF를 로드했다고 읽기가 끝나는 것은 아니며 멀티 기가바이트 파일에서는 바로 그 간극에 시간이 들어갑니다. cross-reference table 또는 cross-reference stream (ISO 32000-1 §7.5.4 및 §7.5.8)은 각 indirect object가 시작하는 위치만 기록합니다. 실제 byte는 페이지를 render하거나 font program을 decode하거나 embedded file stream (ISO 32000-1 §7.11.4)을 추출할 때 나중에 도착합니다. 수만 개의 object가 있는 2 GB archive는 순서가 뒤섞인 작은 read 수만 개가 되고, 로드 시점에는 그 어느 것도 미리 알 수 없습니다
예전에는 하나의 positional stream에서 공유된 Seek 뒤에 Read를 수행하는 경로를 사용했는데, 이 방식은 두 방향에서 동시에 실패합니다. 페이지가 이미 operating-system cache에 상주해 있어도 모든 fragment가 file read 비용을 내고, cursor가 공유되는 mutable state이므로 로컬 파일과 prefetch를 사용하는 progressive PDF range loading 뒤의 byte-range source가 position을 두고 다투지 않고 같은 parser code를 실행할 수 없었습니다. PDFlibPas는 absolute-offset read를 최적화가 아니라 contract로 승격해 두 문제를 함께 해결합니다
TPDFReadAtStream이 보장하는 것
TPDFReadAtStream은 logical stream cursor에 의존하지도, 이를 방해하지도 않는 absolute offset read를 보장합니다. 정확히 하나의 virtual method를 가진 abstract TStream descendant이며 library의 cursor-independent source는 둘 다 여기서 파생됩니다. 로컬 파일용 TReadOnlyMappedFileStream과 range로 제공되는 remote source용 TByteRangeStream입니다. object-slice reader는 source가 TPDFReadAtStream인지 한 번 확인하고, 아니라면 예전의 seek-then-read sequence로 fallback하므로 일반 file stream이나 memory stream도 변경 없이 계속 동작합니다
type
// 공유된 Seek와 Read를 피하는 absolute read 전용 stream
TPDFReadAtStream = class(TStream)
public
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; virtual; abstract;
end;
// 하나의 로컬 파일에 대한 windowed read-only access
TReadOnlyMappedFileStream = class(TPDFReadAtStream)
private
FMemoryMapped: Boolean;
public
constructor Create(const FileName: WideString; WindowSize: Int64 = 0);
function GetStats: TPDFMappedFileStats;
function ReadAt(Offset: Int64; var Buffer;
Count: LongInt): LongInt; override;
property MemoryMapped: Boolean read FMemoryMapped;
end;
이 차이는 signature가 암시하는 것보다 중요합니다. ReadAt은 전달받은 offset을 사용하고 Position을 정확히 원래 위치에 남겨 두므로, 중첩된 parser level이 모든 호출마다 save-and-restore dance를 하지 않고 read를 발행할 수 있습니다. TReadOnlyMappedFileStream은 다른 TStream처럼 Read, Seek와 Size를 계속 구현하고 Seek은 logical position을 파일 안으로 clamp하며 source가 read-only로 열리므로 Write는 항상 0을 반환합니다
Delphi에서 mapped view로 PDF 열기
명시적인 entry point 두 개가 mapped source를 열며, 이미 사용 중인 entry point의 동작은 어느 것도 바꾸지 않습니다. LoadFromMappedFile은 문서를 로드하고 선택하며 DAOpenMappedFile은 같은 파일 위의 Direct Access handle을 반환합니다. 이는 Direct Access로 기가바이트 PDF를 병합하고 분할할 때 원하는 mode입니다. LoadFromFile과 DAOpenFile은 file sharing, error와 compatibility semantics를 그대로 유지하므로 opt in하지 않은 caller의 동작은 바뀌지 않습니다. 두 mapped entry point 모두 byte 단위의 요청 WindowSize와 Options bitmask를 받고 둘 다 어느 값에든 0을 허용합니다
var
Pdf: TPDFlib;
Payload: AnsiString;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
// WindowSize 0은 64 MiB 기본값을 선택하며 여기서는 mapping이 필수입니다
if Pdf.LoadFromMappedFile('archive-2026.pdf', '', 0,
PDF_MAPPED_FILE_REQUIRE_MAPPING) <> 1 then
raise Exception.CreateFmt('mapped open refused, LastErrorCode=%d',
[Pdf.LastErrorCode]);
// 지연 extraction은 이제 seek 대신 mapped window를 순회합니다
Payload := Pdf.GetEmbeddedFileContentToString(1);
if Pdf.GetMappedFileInfo(Info) = 1 then
Writeln(Info);
finally
Pdf.Free;
end;
end;
PDF_MAPPED_FILE_REQUIRE_MAPPING이 실제로 강제하는 것
PDF_MAPPED_FILE_REQUIRE_MAPPING은 조용한 fallback을 open 시점의 즉시 진단 가능한 실패로 바꿉니다. Options를 0으로 두면 두 entry point 모두 read-only file-stream fallback을 허용합니다. 플랫폼에 mapping code가 없거나 mapping call이 실패해도 문서는 열리고 모든 read는 일반 file stream을 통해 진행됩니다. flag를 설정하면 첫 view가 만들어진 경우에만 PDFlibPas가 input을 허용하며, 이전 경로와 똑같이 조용히 동작하는 문서를 로드하는 대신 LastErrorCode 401로 거부를 보고합니다
Windows에서 mapped stream은 FILE_SHARE_READ, FILE_SHARE_WRITE, FILE_SHARE_DELETE와 FILE_FLAG_RANDOM_ACCESS를 함께 사용해 두 번째 read-only handle을 열고, 그 위에 PAGE_READONLY mapping을 만든 뒤 constructor 안에서 첫 window를 매핑합니다. mapping을 eager하게 하는 것이 핵심입니다. "mapping required" failure가 rendering job 중간의 첫 lazy object read가 아니라 LoadFromMappedFile에서 드러나기 때문입니다. 다만 보장이 끝나는 지점은 분명히 해야 합니다. mapping code는 Windows target에만 컴파일되고 zero-byte file은 mapping을 전혀 시도하지 않으므로 PDF_MAPPED_FILE_REQUIRE_MAPPING은 정당하게 실패할 수 있는 요청이지 portable promise가 아닙니다. 음수 WindowSize나 문서화된 유일한 값 이외의 bit가 Options에 들어가면 같은 error 401로 즉시 거부됩니다
하나의 window를 allocation granularity에 맞춰 다시 매핑하기
항상 하나의 view만 유지하며 이것이 address-space 사용량을 파일 크기와 무관하게 만드는 핵심입니다. WindowSize가 0이면 64 MiB를 선택하고, system allocation granularity보다 작으면 그 값까지 올리며, 1 GiB보다 크면 제한하고, 결과를 granularity unit의 정수 배수로 올림합니다. Windows에서는 GetSystemInfo가 다른 dwAllocationGranularity를 보고하지 않는 한 65536 byte입니다. read가 current view 밖에 놓이면 PDFlibPas가 view를 unmap하고 요청 offset을 granularity 경계 아래로 맞춘 뒤 그 위치에 새 window를 매핑합니다. 마지막 window는 physical file size에 맞춰 clamp하므로 view가 파일 끝을 넘어가지 않습니다
한 번의 read가 여러 window를 가로지를 수 있습니다. loop는 current view가 제공할 수 있는 만큼 복사하고 remap한 다음 계속하며, 끝을 벗어나는 요청은 실패하지 않고 short count를 반환합니다. PDFlibPas가 의도적으로 하지 않는 일은 view 내부의 pointer를 caller에게 넘기는 것입니다. 다음 cross-window read가 그 pointer를 무효화하고 caller가 이를 합리적으로 방어할 방법도 없기 때문입니다. mapped byte는 parser가 소유한 destination buffer로 곧바로 복사되어 추가 file input buffer와 position switching이 사라지지만, library는 최종 parser storage에 대한 zero-copy를 주장하지 않습니다. read-side windowing은 write side와도 결합됩니다. 빠른 PDF merge 중 byte-level reference shifting이 object byte를 밖으로 stream하는 동안 mapped source는 안으로 stream하기 때문입니다. window 크기의 trade-off는 분명합니다. 작은 window는 address space를 덜 차지하지만 더 자주 remap하며, 보통 32-bit process 안에서는 이것이 올바른 선택입니다
lock이 보호하는 것과 GetMappedFileInfo가 보고하는 것
하나의 critical section이 mapped view, fallback file cursor, logical position과 statistics를 모두 감싸며 두 read method의 분리는 여기서 자연스럽게 나옵니다. ReadAt은 lock을 잡고 lock-free internal reader를 호출하고, Read는 같은 lock을 잡아 current logical position에서 같은 internal reader를 호출한 뒤 position을 전진시킵니다. public ReadAt 대신 internal function을 재사용하는 것은 recursive locking을 피하기 위해서이며, 전체 copy loop 동안 lock을 유지하는 것은 concurrent call 아래에서도 single-window remap을 올바르게 유지하기 위해서입니다. port하기 전에 알아 둘 Free Pascal 세부 사항도 있습니다. FPC Windows unit은 TCriticalSection이라는 자체 record를 선언하므로 field와 construction은 SyncObjs.TCriticalSection으로 작성해야 합니다. Delphi는 한정하지 않은 형태를 문제없이 컴파일하지만 FPC는 이를 Create, Enter, Leave가 없는 record로 resolve합니다
var
Pdf: TPDFlib;
Handle, PageRef: Integer;
Info: WideString;
begin
Pdf := TPDFlib.Create;
try
Handle := Pdf.DAOpenMappedFile('archive-2026.pdf', '',
16 * 1024 * 1024, PDF_MAPPED_FILE_REQUIRE_MAPPING);
if Handle = 0 then
Exit;
try
PageRef := Pdf.DAFindPage(Handle, 1);
Writeln(Pdf.DAExtractPageText(Handle, PageRef, 0));
// {"memoryMapped":true,"fileSize":...,"remapCount":...}
if Pdf.DAGetMappedFileInfo(Handle, Info) = 1 then
Writeln(Info);
finally
Pdf.DACloseFile(Handle);
end;
finally
Pdf.Free;
end;
end;
memoryMapped는 portable file-stream fallback이 활성화된 경우 false이며 mapping이 한 번도 만들어지지 않았음을 증명하는 유일한 field입니다windowSize는 요청한 값이 아니라 실제로 정렬된 window이며 tail window에서는mappedBytes가 그보다 작습니다mappedOffset은 유지 중인 view의 allocation-aligned 시작 위치이며 현재 활성 view가 없으면 -1입니다readCalls는 성공한 in-range read request 수를 세고bytesRead는 caller에게 복사한 byte 수를 세며remapCount에는 initial view도 포함됩니다
대상 회귀 테스트는 cross-window absolute read, logical cursor 보존, tail의 short read, 잘못된 offset, 거부된 write, 서로 떨어진 window 사이의 remap, 220 KB incompressible attachment의 지연 extraction, DACloseFile 후 statistics 무효화를 다룹니다. Win32와 Win64 headless suite는 각각 1467개 테스트를 발견했고 ignored, failed, errored 또는 leaked 결과 없이 전부 통과했습니다. Delphi나 C++Builder에서 기가바이트 PDF를 다루면서 profiler가 parsing보다 file read를 계속 가리킨다면 mapped-file entry point를 반나절 측정해 볼 가치가 있으며 GetMappedFileInfo가 실제로 mapping을 얻었는지 알려 줍니다. 전체 API reference와 trial build는 PDFlibPas Delphi PDF library 페이지에 있습니다