pdfium.dll của bạn tải bình thường và một thủ tục vẫn bị thiếu. PDFium Component xử lý điều này bằng cách chia các binding của nó thành hai lớp: export bắt buộc được phân giải qua CheckGetProcAddress, hủy bỏ việc tải hoàn toàn, và export tùy chọn được phân giải qua TryGetProcAddress, để lại một con trỏ nil và một phép kiểm tra năng lực thay vào đó
Đây không phải cùng vấn đề với một DLL không tìm thấy được. Nếu ứng dụng của bạn chết với lỗi định dạng EXE sai, một file bị thiếu, hoặc không khớp kiến trúc, câu chuyện đó được kể trong bài đồng hành về triển khai pdfium.dll và chẩn đoán lỗi tải. Ở đây trình tải đã thành công. Handle module hợp lệ, hàng trăm export đã phân giải, và lần chạy vẫn kết thúc trước khi trang đầu tiên của bạn render vì một điểm vào đến trong một bản build PDFium mới hơn không có trong binary trên đĩa
Vì sao một export bị thiếu lại phá vỡ toàn bộ thư viện?
Vì một binding bắt buộc là một hợp đồng cứng, và nó được thực thi trong một chuỗi liên kết tất-cả-hoặc-không-gì duy nhất. PDFium Component phân giải toàn bộ bảng export của nó bên trong LoadLibrary, hết lệnh gọi CheckGetProcAddress này đến lệnh gọi khác. Kết quả nil đầu tiên raise EPdfError và gọi UnloadLibrary trước khi làm vậy, đó là có chủ đích: nếu không, một lần liên kết một phần sẽ để lại các con trỏ đã phân giải trỏ vào một module sắp bị giải phóng, âm thầm đánh bại mọi điều kiện canh gác Assigned ở phía dưới
Hệ quả là kiểu thất bại đưa mọi người đến đây. Bạn nâng cấp component, gửi cùng pdfium.dll bạn đã gửi suốt hai năm, và ứng dụng sẽ không khởi động. Lỗi nêu tên một export cho một tính năng bạn chưa bao giờ gọi. Không gì bạn làm tại chỗ gọi giúp được gì, vì chỗ gọi không bao giờ chạy; thất bại xảy ra trong lúc liên kết, trước khi bất kỳ tài liệu nào được mở
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;
Bắt buộc hay tùy chọn: ranh giới thực sự nằm ở đâu
Quy tắc mà PDFium Component áp dụng khá thẳng thắn. Một export là bắt buộc khi sự vắng mặt của nó khiến component không thể làm công việc mà nó tồn tại để làm, và tùy chọn khi sự vắng mặt của nó chỉ loại bỏ một tính năng lá đơn lẻ. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage là bắt buộc, và thất bại rõ ràng trên những cái đó là đúng: một trình xem không thể render không phải một trình xem suy giảm, đó là một trình xem hỏng
Mọi thứ được tiếp cận qua trình tải khoan dung ngày nay đều là một lá. FPDFBookmark_GetColor xuất hiện sau M109 và chỉ cung cấp mảng màu /C tùy chọn của một mục outline, nên một DLL có trước nó đơn giản báo cáo không có màu bookmark. Các trợ giúp V8 FPDF_GetRecommendedV8Flags và FPDF_GetArrayBufferAllocatorSharedInstance, và các trợ giúp chuỗi XFA FPDF_BStr_Init, FPDF_BStr_Set và FPDF_BStr_Clear, vắng mặt trong bất kỳ bản build không-V8 nào theo cấu trúc, nên coi chúng là bắt buộc sẽ khiến pdfium.dll thuần túy không thể tải được. Và cặp đã thúc đẩy bài viết này: FPDFAttachment_SetDescription và FPDFAttachment_GetDescription, được thêm vào thượng nguồn ngày 2026-07-13, muộn hơn ngày build của cả bốn binary PDFium mà dự án gửi kèm dưới DLLs/Win32 và DLLs/Win64. Trường hợp cuối đó là hình dạng chung của vấn đề, không phải một lần đơn lẻ: một lớp binding theo dõi các header thượng nguồn, vốn liên tục di chuyển, trong khi DLL trong bộ cài đặt của bạn di chuyển theo những bước nhảy rời rạc mỗi khi ai đó rebuild nó. Luôn luôn có một khoảng thời gian mà phía Pascal biết về các export mà binary được triển khai không có, và quyết định trước ranh giới bắt buộc/tùy chọn cho mỗi export mới là thứ duy nhất khiến khoảng thời gian đó sống sót được
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');
Một cổng năng lực nên làm gì tại chỗ gọi?
Nó nên bất đối xứng, và sự bất đối xứng đó chính là toàn bộ thiết kế. Một lần đọc không chạy được có một câu trả lời rỗng trung thực. Một lần ghi không chạy được không có câu trả lời trung thực nào cả, nên nó phải raise. PDFium Component tách thuộc tính mô tả tệp đính kèm chính xác theo ranh giới đó, và sự tách biệt đó là thứ ngăn một export bị thiếu biến thành mất dữ liệu âm thầm. TPdf.GetAttachmentDescription kiểm tra Assigned(FPDFAttachment_GetDescription) và thoát với một WString rỗng. Đó không phải một lời nói dối: trên một DLL không có export, component thực sự không thể biết liệu tệp đính kèm có mang một mục /Desc hay không, và một mô tả rỗng đọc giống hệt một tệp đính kèm chưa bao giờ có mô tả. Phần còn lại của API tệp đính kèm, được trình bày trong bài về làm việc với tệp đính kèm PDF trong Delphi, tiếp tục hoạt động không bị đụng đến
TPdf.SetAttachmentDescription đi theo hướng ngược lại. Nó gọi Check trên cùng phép kiểm tra Assigned và raise EPdfError với văn bản "Attachment descriptions are not supported by the loaded PDFium DLL". Trả về lặng lẽ ở đây sẽ là lựa chọn tệ nhất có thể: bên gọi sẽ đặt một mô tả, không nhận được lỗi nào, lưu file, và gửi đi một PDF nơi mô tả đơn giản là vắng mặt. Không ai để ý cho đến khi một bên tiêu thụ hạ nguồn hỏi nó đã đi đâu
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;
Thăm dò năng lực trước khi bạn cung cấp tính năng
Bắt một exception là một cách tồi để khám phá bản triển khai của bạn có thể làm gì, nên PDFium Component phơi bày cùng phép kiểm tra đó như một hàm có tên. AttachmentDescriptionFeaturesAvailable gọi LoadLibrary và trả về liệu cả hai nửa của cặp đó đã phân giải hay chưa. Nó đứng bên cạnh V8FeaturesAvailable, XfaBStrHelpersAvailable và XfaFeaturesAvailable, theo cùng khuôn mẫu y hệt cho các nhóm tùy chọn riêng của chúng. Việc đặt tên cho phép thăm dò quan trọng hơn vẻ ngoài của nó: một boolean tên AttachmentDescriptionFeaturesAvailable nói cho người bảo trì tiếp theo biết tính năng này có điều kiện dựa trên binary được triển khai, điều mà một phép kiểm tra Assigned trơn chôn trong một setter thuộc tính không bao giờ làm được. Nó cũng cho lớp UI thứ gì đó để bind vào, nên ô nhập mô tả bị vô hiệu hóa ngay từ đầu thay vì chấp nhận đầu vào rồi từ chối nó khi lưu
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;
Vì sao độ phủ binding phải được một công cụ chứng minh?
Vì các con số đã vượt qua điểm mà con người có thể được tin cậy để theo dõi chúng. PDFium Component đã kiểm toán 21 header PDFium công khai đối chiếu với baseline thượng nguồn ngày 2026-07-29 và tìm thấy 470 hàm C ABI được export. Binding đã bao phủ 468 trong số đó. Không ai định vị khoảng hở hai đó bằng cách đọc header; một script đã làm, trong một giây, và nó sẽ làm lại vào lần nâng cấp thượng nguồn tiếp theo. tools/audit_pdfium_public_api.py cố tình nhỏ gọn: nó khớp regex FPDF_EXPORT ... FPDF_CALLCONV name( qua mọi header trong thư mục công khai, khớp regex mọi CheckGetProcAddress('Name') và TryGetProcAddress('Name') trong PDFium.pas, và in ra hai hiệu tập hợp: missing cho các export không có binding, stale cho các binding mà export của chúng không còn tồn tại ở thượng nguồn. Nó thoát khác không khi một trong hai tập không rỗng, nên nó thả vào một bước build mà không cần thêm nghi thức nào. Kết quả hiện tại là 470/470 đã bind, thiếu 0, cũ 0
Hướng "cũ" xứng đáng được coi trọng ngang với hướng "thiếu". Một export mà thượng nguồn gỡ bỏ để lại một dòng CheckGetProcAddress sẽ khiến mọi lần tải trong tương lai thất bại cứng, và kiểu mục nát đó vô hình cho đến ngày ai đó cập nhật DLL. Review thủ công tìm ra hàm bạn đang nghĩ tới; nó không tìm ra hàm bạn không nghĩ tới. Cũng lưu ý rằng cuộc kiểm toán cố tình đếm cả hai loại trình tải như độ phủ, đó là quyết định đúng cho việc trôi dạt API và là lý do sự tách biệt bắt buộc/tùy chọn phải là một quyết định được tài liệu hóa chứ không phải một sản phẩm phụ của việc ai đó thêm dòng đó vào
Nơi binding tùy chọn ngừng trung thực
Hai ranh giới đáng được nói rõ ràng, vì khuôn mẫu này dễ bị áp dụng quá đà. Ranh giới đầu tiên là một con trỏ hàm nil chỉ an toàn nếu đúng theo nghĩa đen mọi đường chạm vào nó đều kiểm tra Assigned trước. Trong một unit khai báo hàng trăm biến hàm cdecl, một lệnh gọi không được canh gác duy nhất là một access violation tại một địa chỉ vô nghĩa trong một stack trace. Cùng kỷ luật chi phối các calling convention và vòng đời qua ranh giới C áp dụng ở đây, và đó là chủ đề của bài về tăng cường binding PDFium chống lỗi ABI và an toàn bộ nhớ
Ranh giới thứ hai là phạm vi. Binding tùy chọn không phải một giấy phép chung để khiến mọi thứ khoan dung. Nếu FPDF_RenderPageBitmap là tùy chọn, component sẽ tải vui vẻ rồi thất bại trên mọi trang, biến một lỗi khởi động rõ ràng duy nhất thành một loạt lỗi runtime rải rác không có nguyên nhân rõ ràng. Bắt buộc là mặc định đúng. Tùy chọn là ngoại lệ bạn dùng đến khi một tính năng thực sự là một lá, khi sự vắng mặt có một hành vi suy giảm biện minh được ở phía đọc, và khi phía ghi có thể từ chối với một thông báo nêu tên lý do
Thiết kế trình tải, các phép thăm dò năng lực và công cụ kiểm toán mô tả ở đây đi kèm trong PDFium Component cho Delphi và C++Builder; trang sản phẩm liệt kê các binary PDFium đi kèm và toàn bộ bề mặt API mà chúng phơi bày