여러분의 pdfium.dll은 정상적으로 로드되는데 프로시저 하나가 여전히 빠져 있습니다. PDFium Component는 이를 바인딩을 두 부류로 나누어 처리합니다. 로드를 완전히 중단시키는 CheckGetProcAddress로 해석되는 필수 익스포트와, nil 포인터와 기능 검사를 남기는 TryGetProcAddress로 해석되는 선택적 익스포트입니다
이는 찾을 수 없는 DLL과는 다른 문제입니다. 애플리케이션이 잘못된 EXE 형식 오류, 누락된 파일, 또는 아키텍처 불일치로 죽는다면 그 이야기는 pdfium.dll 배포와 로드 실패 진단에 관한 자매편 글에서 다룹니다. 여기서는 로더가 성공했습니다. 모듈 핸들은 유효하고, 수백 개의 익스포트가 해소되었으며, 그럼에도 실행은 여러분의 첫 페이지가 렌더링되기 전에 끝나버립니다. 더 새로운 PDFium 빌드에 함께 온 진입점 하나가 디스크상의 바이너리에는 없기 때문입니다
왜 익스포트 하나가 없다고 라이브러리 전체가 깨지는가
필수 바인딩은 하드 계약이며, 단일 전부-아니면-전무 바인드 시퀀스 안에서 강제되기 때문입니다. PDFium Component는 LoadLibrary 안에서 전체 익스포트 테이블을 CheckGetProcAddress 호출을 하나씩 이어가며 해석합니다. 첫 번째 nil 결과는 EPdfError를 발생시키며, 그 전에 UnloadLibrary를 호출합니다. 이는 의도적입니다. 부분적인 바인드는 그러지 않으면 이미 해소된 포인터들을 곧 해제될 모듈로 향한 채 남겨두어, 아래에 있는 모든 Assigned 가드를 조용히 무력화할 것입니다
그 결과는 여러분을 이 글로 이끄는 실패 형태입니다. 컴포넌트를 업그레이드하고, 2년 동안 배포해온 것과 같은 pdfium.dll을 그대로 출하했는데, 애플리케이션이 시작조차 되지 않습니다. 오류는 여러분이 한 번도 호출한 적 없는 기능의 익스포트 이름을 댑니다. 호출 지점에서 무엇을 해도 도움이 되지 않는데, 호출 지점이 애초에 실행되지 않기 때문입니다. 실패는 바인딩 도중에, 어떤 문서가 열리기도 전에 일어났습니다
function CheckGetProcAddress(const Name: string): Pointer;
begin
Result := GetProcAddress(PDFiumLibrary, PChar(Name));
if Result = nil then
begin
// A missing required export means the deployed pdfium.dll is older
// than this build of the binding. Drop every pointer resolved so far
// so no caller can reach into the module we are about to free.
UnloadLibrary;
raise EPdfError.Create('Required PDFium export not found: ' + Name);
end;
end;
function TryGetProcAddress(const Name: string): Pointer;
begin
// Optional export. nil is a legitimate answer here; every caller is
// required to test Assigned() before dereferencing the variable.
Result := GetProcAddress(PDFiumLibrary, PChar(Name));
end;
필수인가 선택인가: 실제 경계선이 있는 곳
PDFium Component가 적용하는 규칙은 단순합니다. 익스포트는 그것의 부재가 컴포넌트를 자신이 존재하는 이유인 그 일을 하지 못하게 만들면 필수이고, 그 부재가 잎사귀 하나짜리 기능만 제거하면 선택입니다. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage는 필수이며, 이들에 대해 큰 소리로 실패하는 것은 올바릅니다. 렌더링할 수 없는 뷰어는 저하된 뷰어가 아니라 고장 난 뷰어입니다
오늘날 관용적인 로더를 통해 도달하는 모든 것은 잎사귀입니다. FPDFBookmark_GetColor는 M109 이후에 등장했고 아웃라인 항목의 선택적 /C 색상 배열만 제공하므로, 그것보다 앞선 DLL은 그냥 북마크 색상이 없다고 보고합니다. V8 헬퍼인 FPDF_GetRecommendedV8Flags와 FPDF_GetArrayBufferAllocatorSharedInstance, XFA 문자열 헬퍼인 FPDF_BStr_Init, FPDF_BStr_Set, FPDF_BStr_Clear는 구조상 V8이 아닌 어떤 빌드에도 없으므로, 이들을 필수로 취급하면 평범한 pdfium.dll이 로드 불가능해질 것입니다. 그리고 이 글을 촉발한 쌍이 있습니다. 상류에서 2026-07-13에 추가된 FPDFAttachment_SetDescription과 FPDFAttachment_GetDescription은, 프로젝트가 DLLs/Win32와 DLLs/Win64 아래 배포하는 네 개의 PDFium 바이너리 빌드 날짜보다 뒤에 나왔습니다. 이 마지막 경우는 일회성이 아니라 문제의 일반적인 형태입니다. 바인딩 계층은 끊임없이 이동하는 상류 헤더를 따라가지만, 여러분의 설치 프로그램 안의 DLL은 누군가 재빌드할 때마다 불연속적으로 도약합니다. Pascal 쪽이 배포된 바이너리가 갖지 않은 익스포트를 알고 있는 창이 항상 존재하며, 새 익스포트마다 필수/선택 경계선의 어느 쪽에 속할지를 미리 결정해두는 것만이 그 창을 견딜 만하게 만들어줍니다
FPDFDoc_GetAttachmentCount := CheckGetProcAddress('FPDFDoc_GetAttachmentCount');
FPDFDoc_AddAttachment := CheckGetProcAddress('FPDFDoc_AddAttachment');
FPDFAttachment_GetName := CheckGetProcAddress('FPDFAttachment_GetName');
FPDFAttachment_GetStringValue := CheckGetProcAddress('FPDFAttachment_GetStringValue');
// Attachment descriptions were added after the bundled DLL revision.
// Keep them optional so older deployments continue to load.
FPDFAttachment_SetDescription := TryGetProcAddress('FPDFAttachment_SetDescription');
FPDFAttachment_GetDescription := TryGetProcAddress('FPDFAttachment_GetDescription');
FPDFAttachment_SetFile := CheckGetProcAddress('FPDFAttachment_SetFile');
FPDFAttachment_GetFile := CheckGetProcAddress('FPDFAttachment_GetFile');
호출 지점에서 기능 게이트는 무엇을 해야 하는가
비대칭이어야 하며, 그 비대칭이 설계의 전부입니다. 실행할 수 없는 읽기는 정직한 빈 답을 가집니다. 실행할 수 없는 쓰기는 정직한 답이 전혀 없으므로 반드시 예외를 발생시켜야 합니다. PDFium Component는 첨부 파일 설명 속성을 정확히 그 선을 따라 나누며, 그 분리가 바로 누락된 익스포트가 조용한 데이터 손실로 변하지 못하게 막는 것입니다. TPdf.GetAttachmentDescription은 Assigned(FPDFAttachment_GetDescription)을 검사하고 빈 WString으로 빠져나갑니다. 이는 거짓말이 아닙니다. 그 익스포트가 없는 DLL에서는 컴포넌트가 첨부 파일이 /Desc 항목을 담고 있는지 정말로 알 수 없으며, 빈 설명은 애초에 하나도 가진 적 없는 첨부 파일과 똑같이 읽힙니다. Delphi에서 PDF 첨부 파일 다루기에서 다루는 나머지 첨부 파일 API는 손대지 않은 채로 계속 작동합니다
TPdf.SetAttachmentDescription은 반대 경로를 택합니다. 같은 Assigned 검사에 대해 Check를 호출하고 "Attachment descriptions are not supported by the loaded PDFium DLL"이라는 텍스트와 함께 EPdfError를 발생시킵니다. 여기서 조용히 반환하는 것은 가능한 최악의 선택일 것입니다. 호출자는 설명을 설정하고, 오류를 받지 못하고, 파일을 저장하고, 설명이 그냥 없는 PDF를 출하하게 됩니다. 하류의 소비자가 그것이 어디로 갔는지 물을 때까지 아무도 알아채지 못합니다
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
CheckActive;
Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
Result := '';
// Read side degrades: an old DLL cannot report /Desc, and '' is
// indistinguishable from an attachment that carries no description.
if not Assigned(FPDFAttachment_GetDescription) then
Exit;
// ... two-pass buffer sizing against FPDFAttachment_GetDescription ...
end;
procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
CheckActive;
Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
// Write side refuses: silently dropping the value would produce a file
// the caller believes carries a description and does not.
Check(Assigned(FPDFAttachment_SetDescription),
'Attachment descriptions are not supported by the loaded PDFium DLL');
// ... FPDFDoc_GetAttachment, then FPDFAttachment_SetDescription ...
end;
기능을 제공하기 전에 능력을 프로브하기
예외를 붙잡는 것은 여러분의 배포가 무엇을 할 수 있는지 알아내기에는 좋지 못한 방법이므로, PDFium Component는 같은 검사를 이름이 붙은 함수로도 노출합니다. AttachmentDescriptionFeaturesAvailable은 LoadLibrary를 호출하고 그 쌍의 양쪽이 모두 해소되었는지 반환합니다. 이는 V8FeaturesAvailable, XfaBStrHelpersAvailable, XfaFeaturesAvailable과 나란히 있으며, 이들은 자신의 선택적 그룹에 대해 동일한 패턴을 따릅니다. 프로브에 이름을 붙이는 것은 보기보다 더 중요합니다. AttachmentDescriptionFeaturesAvailable이라는 Boolean은 다음 유지보수자에게 이 기능이 배포된 바이너리에 따라 조건부라는 것을 알려주며, 이는 속성 세터 안에 묻힌 맨 Assigned 검사가 절대 해주지 않는 일입니다. 이는 또한 UI 계층에 바인딩할 무언가를 제공하므로, 설명 편집 상자는 저장 시점에 거부당하는 대신 미리 비활성화됩니다
procedure TAttachmentFrame.SyncCapabilities;
begin
// Ask once, at form setup, instead of discovering the limit on save.
DescriptionEdit.Enabled := AttachmentDescriptionFeaturesAvailable;
if not DescriptionEdit.Enabled then
DescriptionEdit.TextHint := 'Requires a newer pdfium.dll';
end;
procedure TAttachmentFrame.SaveDescription(Pdf: TPdf; Index: Integer);
begin
if not AttachmentDescriptionFeaturesAvailable then
Exit;
Pdf.AttachmentDescription[Index] := DescriptionEdit.Text;
end;
바인딩 커버리지는 왜 도구로 증명되어야 하는가
그 숫자들이 사람이 신뢰받을 수 있는 지점을 이미 지났기 때문입니다. PDFium Component는 21개의 공개 PDFium 헤더를 2026-07-29 상류 기준선에 대해 감사해서 470개의 익스포트된 C ABI 함수를 찾아냈습니다. 바인딩은 이미 그중 468개를 커버하고 있었습니다. 그 두 개의 간극을 찾아낸 것은 헤더를 읽은 사람이 아니라 스크립트였고, 1초 만에 찾아냈으며, 다음 상류 업데이트에서도 다시 그렇게 할 것입니다. tools/audit_pdfium_public_api.py는 의도적으로 작습니다. 공개 디렉터리의 모든 헤더에서 FPDF_EXPORT ... FPDF_CALLCONV name(을 정규식으로 매칭하고, PDFium.pas 안의 모든 CheckGetProcAddress('Name')과 TryGetProcAddress('Name')을 정규식으로 매칭한 다음, 두 집합의 차집합을 출력합니다. 바인딩이 없는 익스포트를 위한 missing과, 익스포트가 더 이상 상류에 존재하지 않는 바인딩을 위한 stale입니다. 둘 중 하나라도 비어 있지 않으면 0이 아닌 값으로 종료하므로, 별다른 격식 없이 빌드 단계에 끼워 넣을 수 있습니다. 현재 결과는 470개 중 470개 바인딩됨, 누락 0, stale 0입니다
stale 방향도 missing 방향만큼이나 제 몫을 합니다. 상류가 제거한 익스포트는 미래의 모든 로드를 강하게 실패시킬 CheckGetProcAddress 줄을 남기며, 그런 종류의 부패는 누군가 DLL을 업데이트하는 그날까지 보이지 않습니다. 수동 검토는 여러분이 생각하고 있던 함수는 찾아내지만, 생각하지 않고 있던 함수는 찾지 못합니다. 또한 이 감사는 두 로더 모두를 커버리지로 의도적으로 카운트한다는 점도 알아두십시오. 이는 API 드리프트에 맞는 올바른 판단이며, 필수/선택 분리가 누가 그 줄을 추가했는지의 부산물이 아니라 문서화된 결정이어야 하는 이유입니다
선택적 바인딩이 정직함을 멈추는 지점
이 패턴은 과잉 적용하기 쉬우므로 분명히 짚어둘 만한 경계가 두 가지 있습니다. 첫째는 nil 함수 포인터가 그것을 건드리는 문자 그대로 모든 경로가 먼저 Assigned를 검사할 때만 안전하다는 것입니다. 수백 개의 cdecl 함수 변수를 선언하는 유닛에서, 가드 없는 호출 하나는 스택 트레이스에서 아무 의미 없는 주소에서 일어나는 액세스 위반입니다. C 경계를 넘나드는 호출 규약과 수명을 다스리는 것과 같은 규율이 여기에도 적용되며, 이는 PDFium 바인딩을 ABI와 메모리 안전 결함에 대해 강화하기에서 다루는 주제입니다
두 번째 경계는 범위입니다. 선택적 바인딩은 모든 것을 관용적으로 만들어도 된다는 일반적인 허가가 아닙니다. FPDF_RenderPageBitmap이 선택이었다면 컴포넌트는 기꺼이 로드된 뒤 모든 페이지에서 실패해서, 하나의 명확한 시작 오류를 원인이 뚜렷하지 않은 런타임 오류들의 산발로 바꿔놓았을 것입니다. 필수가 올바른 기본값입니다. 선택은 기능이 진정으로 잎사귀이고, 부재가 읽기 쪽에서 방어 가능한 저하된 동작을 가지며, 쓰기 쪽이 이유를 명시하는 메시지로 거부할 수 있을 때 손을 뻗는 예외입니다
여기서 설명한 로더 설계, 기능 프로브, 감사 도구는 Delphi와 C++Builder용 PDFium Component의 일부로 제공됩니다. 제품 페이지에는 번들된 PDFium 바이너리와 이들이 노출하는 전체 API 표면이 실려 있습니다