고객의 로컬 PC 환경에서 pdfium.dll이 정상적으로 로드되지 않는 오동작이 발생할 때, Delphi 및 Lazarus용 PDFium 컴포넌트는 다소 모호한 OS 저수준의 네이티브 에러를 명확한 원인 코드로 변환하여 알려줍니다. CheckLoadLibrary 루틴은 ERROR_BAD_EXE_FORMAT(Windows 에러 코드 193)을 32비트와 64비트 간의 아키텍처 불일치(Architecture mismatch)로 해석하며, CheckGetProcAddress는 필수 익스포트 함수 유실 건을 바인딩 규격보다 구버전인 pdfium.dll이 기입된 탓으로 보고합니다. 두 에러 메시지 모두 추측에 의존하지 않고 명확한 해결책을 제시해 줍니다
이는 네이티브 DLL 로드 실패가 완전히 상이한 세 가지 원인(아키텍처 불일치, Pascal 바인딩 호출 규격에 비해 구형인 바이너리 배포, 혹은 여러 스레드가 동시에 바인딩하려고 시도하는 스레드 경합)에 의해 유발되는데, OS가 제공하는 가공되지 않은 텍스트로는 이들을 구별하기 힘들기 때문입니다. 개별 원인마다 해결책이 다르며, v2.11.0 로드 수명 주기 개선 작업의 일환으로 강화된 진단 기능은 개발자가 실제로 마주한 오류의 물리적 원인을 명확히 지시해 줍니다
왜 pdfium.dll 로드 시 bad EXE format 에러가 발생하나요?
Windows 에러 코드 193(ERROR_BAD_EXE_FORMAT)은 대상 파일이 디스크에 실재하고 열렸으나, 이의 PE 머신 형식(PE machine type)이 호스트 프로세스의 규격과 일치하지 않음을 의미합니다. 64비트 실행 파일(EXE)은 32비트 pdfium.dll을 로드할 수 없고, 반대로 32비트 EXE는 64비트 DLL을 메모리에 올릴 수 없습니다. 파일이 분명 존재하기 때문에, 단순히 유실된 DLL을 수소문하려 드는 본능적인 대처는 엉뚱한 방향으로 시간을 낭비하게 만듭니다
PDFium 컴포넌트는 CheckLoadLibrary 루틴에서 이 상황을 인터셉트(Intercept)합니다: LoadLibrary 호출 결과가 널(Null) 핸들이고 GetLastError 결과가 193인 경우, 파일은 발견되었으나 아키텍처가 일치하지 않는다는 친절한 힌트를 보강하고 해당 EXE 빌드 형식에 대응하는 DLLs/Win32 또는 DLLs/Win64 바이너리를 경로로 지시합니다. 서브디렉토리는 BuildDllSubDir에 의해 판별되는데, IsWin64가 참이면 Win64를, 거짓이면 Win32를 반환합니다. 바인딩 엔진이 실제로 탐색하는 폴더 내에 정확한 아키텍처 규격의 DLL을 배포해 주면 오류가 즉시 해결됩니다
uses
SysUtils, PDFium;
procedure OpenDocument(const AFileName: string);
var
Pdf: TPdf;
begin
// 실행 파일(.exe)과 동일 폴더 내에 빌드 타겟 규격에 맞춰 pdfium.dll을 배포합니다:
// <AppDir>\DLLs\Win32\pdfium.dll 32비트 호스트 프로세스용
// <AppDir>\DLLs\Win64\pdfium.dll 64비트 호스트 프로세스용
Pdf := TPdf.Create(nil);
try
Pdf.FileName := AFileName;
try
Pdf.Active := True; // 첫 사용 시 지연 바인딩(Lazy bind) 방식으로 pdfium.dll을 로드합니다
except
on E: EPdfError do
// 에러 메시지에 이미 실제 물리적 원인(32/64비트 아키텍처 불일치
// 또는 바인딩보다 구버전인 pdfium.dll 탑재 등)이 기입되어 있습니다
raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
end;
if Pdf.Active then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
PDFium 익스포트 함수 누락이 시사하는 바
두 번째 로드 실패 범주는 버전 어긋남(Version skew)입니다. PDFium 라이브러리는 업데이트되면서 새로운 API 익스포트 함수를 지속적으로 보강하며, Pascal 바인딩 엔진은 LoadLibrary 기동 시 CheckGetProcAddress를 통해 필요한 모든 함수의 오프셋 주소를 확인합니다. 필요한 익스포트 함수가 유실된 상황은 디스크 상의 pdfium.dll이 호출하는 바인딩 라이브러리보다 구형 버전임을 반증합니다. CheckGetProcAddress는 이를 감지하여 배포된 pdfium.dll이 현재 컴파일된 PDFiumPas 바인딩 버전보다 오래되었음을 명시하고 호환되는 바이너리가 있어야 할 DLLs/<subdir> 경로를 에러 메시지(EPdfError)로 상세히 인계합니다
이 경로는 두 가지 세부 제어 덕분에 매우 견고하게 작동합니다. 첫째, CheckGetProcAddress는 예외를 던지기 전 UnloadLibrary를 선제 호출하여, 바인딩에 실패한 엉뚱한 함수 포인터 찌꺼기가 잔류하여 다음 호출 시 크래시를 내는 현상을 철저히 예방합니다. 둘째, 에러 문구에 표기되는 DLL 파일명은 BuildDllName이 결정하는데, 일반적인 경우 pdfium.dll을 반환하고 전역 EnableV8Engine 플래그가 활성화된 경우에는 pdfium.v8.dll을 지정합니다. 개발자의 프로그램이 XFA 대화형 양식이나 스크립트 기능 처리를 위해 V8 자바스크립트 엔진을 구동한다면, 에러 텍스트는 일반 바이너리가 아닌 V8 바이너리를 정확하게 지시하여 엉뚱한 파일을 교체하느라 고생하는 현상을 예방합니다
백엔드 백그라운드 스레드에서 PDFium DLL을 로드해도 안전한가요?
라이브러리를 메모리에 올리는 로드(Load) 동작 자체는 이제 어떤 스레드에서 수행해도 완벽히 안전합니다; 다만 로드된 PDFium API 함수들을 다중 스레드에서 무단 혼용 호출하는 행위는 여전히 극히 위험합니다. 이 두 가지 보장 범위를 철저히 인지하고 격리해야 스레드 풀의 안정성을 확보하고 간헐적인 크래시를 차단할 수 있습니다. 백그라운드 페이지 렌더링은 대개 TPdfFuture<T> 워커를 통해 동기화 처리를 거치며, 첫 번째 워커가 TPdf에 접근하는 시점에 지연 바인딩(Lazy bind)이 발동됩니다. 보호 장치가 장착되지 않는다면, 동시에 구동된 두 워커가 라이브러리 미로드 상태를 인지하고 동시에 바인딩을 기동하여 모듈 핸들을 덮어쓰고 첫 번째 로드 리소스를 유실하는 문제를 유발합니다
uses
PDFium, FPdfAsync;
// 백그라운드 스레드에서 실행되는 워커 메서드. pdfium.dll을 바인딩하는
// 최초의 future는 PDFiumLoadLock에 의해 순차적으로 처리되므로, 동시에 실행된
// 두 번째 워커가 이중 바인딩을 기동하거나 모듈 핸들을 유실시키지 않습니다.
function TReportForm.RenderThumbnail(
const AToken: IPdfCancellationToken): TBitmap;
var
Pdf: TPdf;
begin
Pdf := TPdf.Create(nil);
try
Pdf.FileName := 'report.pdf';
Pdf.Active := True; // safe concurrent bind
AToken.ThrowIfCancelled;
Result := Pdf.Thumbnail; // 이 TPdf 인스턴스는 해당 스레드에 의해서만 소유되고 관리됩니다
finally
Pdf.Free;
end;
end;
procedure TReportForm.StartRender;
begin
TPdfFuture<TBitmap>.Run(RenderThumbnail, ThumbnailReady);
end;
procedure TReportForm.ThumbnailReady(
const AResult: TPdfFutureResult<TBitmap>);
begin
if AResult.IsSuccess then
Preview.Picture.Assign(AResult.Value);
end;
로드 락(Load lock)이 방어하지 못하는 구역
이 안전 제어 장치를 과신하여 오용하는 것을 막기 위해, 기능적 경계선을 명확하게 밝혀둘 필요가 있습니다. PDFiumLoadLock은 오직 전역 바인딩 절차(모듈 핸들 관리 및 FPDF_* 함수 포인터 대입문)의 동기화만 책임집니다. 하부의 원시 PDFium C API 자체는 표준 헤더 파일 사양에 적혀 있듯 스레드 세이프(Thread-safe)하지 않으므로, 여러 스레드에서 동시에 FPDF_*를 무단 교차 호출하거나 하나의 FPDF_DOCUMENT 핸들을 다중 스레드에서 공유하는 행위는 차단해야 합니다. 안전한 설계 정석은 위의 코드와 같습니다: 개별 워커 스레드가 고유의 TPdf 인스턴스를 소유하게 만들고 활성 문서 핸들을 절대 다른 스레드에 임의로 넘겨주지 마십시오. 이 경계 규칙과 이와 관련한 ABI 메모리 제어 규칙에 관해서는 PDFium VCL 바인딩의 ABI 오류 및 메모리 보안 강화 가이드에서 상세히 안내합니다
finalization 구문과 FPDF_DestroyLibrary 호출의 중요성
프로그램 종료 처리 흐름에 간과하기 쉬운 틈새가 존재했었습니다. 기존 PDFium 유닛에는 finalization 구문이 선언되어 있지 않아, 프로세스가 소멸되는 시점에 FPDF_DestroyLibrary가 호출되지 못했습니다. OS는 DLL이 점유하던 메모리 영역만 회수할 뿐, PDFium 엔진이 백그라운드 스레드를 결합하고 EnableV8Engine용 V8 자바스크립트 엔진 리소스를 정리하며 서체 및 캐시 데이터를 소멸시키는 내부 청소 루틴은 무시해 버립니다. 단순 렌더링 작업 시에는 메모리 누수가 경미하지만, V8 자바스크립트 엔진이 개입하는 복잡한 문서 처리 시에는 매 구동 시마다 자바스크립트 리소스가 정리되지 않고 누적 유실되어 반복적인 자동화 기동 환경에서 성능 저하를 초래합니다
개선된 코드의 finalization 단은 이제 UnloadLibrary를 명확하게 호출하여, 내부의 FPDF_DestroyLibrary 작동을 정상 구동하고 메모리를 깨끗하게 정리합니다. 이 정리가 유닛 소멸 시점에 위치하는 명확한 아키텍처적 이유가 있습니다: TPdf.Destroy 소멸자는 현재 열린 문서를 닫기 위해 Active := False 속성 조율만 진행하며, 전역 DLL 바인딩을 함부로 메모리에서 내리지 않습니다. 이 덕분에 여러 개의 PDF 컴포넌트 인스턴스를 동시 구동하는 응용 프로그램이 매번 로드/언로드를 반복하는 성능 저하 없이 단 하나의 공유 바인딩을 전 수명 주기에 걸쳐 고효율로 사용하게 됩니다. 정리 코드가 finalization 단에 수립됨으로써, 유닛이 해제되는 유일한 시점에 공유 라이브러리가 안정적으로 단 한 번 정리되며, UnloadLibrary 내부에 Loaded 플래그 검사 로직이 있어 PDFium 엔진을 기동하지 않았던 시나리오에서도 불필요한 예외를 던지지 않고 안전하게 건너뜁니다
간단한 배포 사전 체크리스트
세 가지 개발 습관을 정립해 두면 실무 배포 환경의 DLL 로드 실패 유형을 대다수 예방할 수 있습니다. 첫째, 호스트 프로세스의 비트와 DLL의 아키텍처 비트를 정확히 일치시키고 이를 DLLs/Win32 또는 DLLs/Win64 폴더 아래에 배포하십시오. 둘째, 익스포트 함수 누락 에러 방지를 위해 pdfium.dll의 버전을 Pascal 바인딩 소스 버전과 항상 일치시키십시오. 셋째, 로드 바인딩 과정이 동기화(Atomic)되었더라도 TPdf 인스턴스나 문서 핸들을 스레드 간에 무단 공유하지 마십시오. Delphi와 Lazarus용 크로스 빌드를 동시 타겟팅하는 경우 발생하는 빌드 패키지 차이점에 대해서는 Delphi 및 FPC 크로스 컴파일러 설정 주의 사항 가이드에서 정밀히 안내합니다. 여기에 설명된 정밀 진단 시스템, 로드 락 스레드 잠금 및 finalization 마감 루틴은 모두 Delphi 및 C++Builder용 PDFium 컴포넌트 패키지 최신 버전에 탑재되어 배포됩니다