Khi tệp pdfium.dll từ chối tải trên máy khách, thành phần PDFium Component dành cho Delphi và Lazarus sẽ chuyển đổi lỗi gốc bí ẩn thành một nguyên nhân cụ thể. Thủ tục CheckLoadLibrary đọc mã lỗi ERROR_BAD_EXE_FORMAT (193) là sự không tương thích kiến trúc 32/64-bit, và CheckGetProcAddress báo cáo một export bị thiếu dưới dạng một tệp pdfium.dll cũ hơn bản liên kết (binding). Cả hai thông báo đều chỉ rõ cách khắc phục thay vì để bạn phải tự mình phán đoán
Điều đó rất quan trọng bởi vì một tệp DLL gốc có thể gặp lỗi theo ba cách riêng biệt, và văn bản gốc của hệ điều hành thường gộp tất cả chúng lại làm một. Kiến trúc có thể sai, tệp nhị phân được triển khai có thể quá cũ đối với liên kết Pascal gọi nó, hoặc hai luồng có thể tranh chấp để liên kết nó tại cùng một thời điểm. Mỗi trường hợp có một cách khắc phục khác nhau, và mục tiêu cốt lõi của các chẩn đoán được thêm vào trong công việc quản lý vòng đời tải v2.11.0 là cho bạn biết chính xác trường hợp nào bạn đang thực sự đối mặt
Tại sao pdfium.dll không thể tải với lỗi định dạng EXE sai?
Lỗi Windows 193, ERROR_BAD_EXE_FORMAT, nghĩa là tệp tin đã được tìm thấy và mở nhưng kiểu máy PE (PE machine type) của nó không khớp với tiến trình máy chủ. Một tệp thực thi 64-bit không thể tải tệp pdfium.dll 32-bit, và một tệp thực thi 32-bit không thể tải tệp pdfium.dll 64-bit. Tệp tin có tồn tại, vì vậy bản năng đi tìm kiếm một DLL bị thiếu sẽ đưa bạn đi hoàn toàn sai hướng
Thành phần PDFium Component chặn trường hợp này trong CheckLoadLibrary: khi LoadLibrary trả về một handle rỗng (null) và GetLastError là 193, nó sẽ thêm một gợi ý rằng DLL đã được tìm thấy nhưng kiến trúc của nó không khớp với tiến trình máy chủ, và nó trỏ tới bản dựng DLLs/Win32 hoặc DLLs/Win64 tương ứng. Thư mục con được chọn bởi BuildDllSubDir, vốn trả về Win64 khi IsWin64 là true và Win32 trong trường hợp ngược lại. Triển khai DLL dưới cây thư mục mà liên kết thực sự tìm kiếm và sự không khớp sẽ biến mất
uses
SysUtils, PDFium;
procedure OpenDocument(const AFileName: string);
var
Pdf: TPdf;
begin
// Deploy pdfium.dll next to the executable, matched to the build target:
// <AppDir>\DLLs\Win32\pdfium.dll for a 32-bit host process
// <AppDir>\DLLs\Win64\pdfium.dll for a 64-bit host process
Pdf := TPdf.Create(nil);
try
Pdf.FileName := AFileName;
try
Pdf.Active := True; // first use lazily binds pdfium.dll
except
on E: EPdfError do
// The message already names the real cause: a 32/64-bit mismatch
// (ERROR_BAD_EXE_FORMAT) or a pdfium.dll older than this binding
raise Exception.CreateFmt('PDF engine did not start: %s', [E.Message]);
end;
if Pdf.Active then
RenderFirstPage(Pdf);
finally
Pdf.Free;
end;
end;
Một export PDFium bị thiếu cho bạn biết điều gì?
Nhóm lỗi thứ hai là sự lệch biệt phiên bản (version skew). PDFium phát hành các export mới theo thời gian, và liên kết Pascal sẽ phân giải mọi hàm nó cần thông qua CheckGetProcAddress trong quá trình LoadLibrary. Nếu một export bắt buộc bị vắng mặt, liên kết đó sẽ mới hơn so với tệp pdfium.dll trên đĩa, và chẩn đoán thực tế là DLL được triển khai đã lỗi thời chứ không phải bị hỏng. Hàm CheckGetProcAddress chỉ ra chính xác điều đó: nó nâng một ngoại lệ EPdfError báo cáo rằng tệp pdfium.dll được triển khai cũ hơn bản dựng liên kết PDFiumPas này, và nó nêu tên đường dẫn DLLs/<subdir> nơi một tệp nhị phân tương thích cần hiện diện
Hai chi tiết kỹ thuật giúp cho con đường này hoạt động mạnh mẽ. Thứ nhất, CheckGetProcAddress gọi UnloadLibrary trước khi nó nâng lỗi, nhờ vậy một liên kết dang dở không bao giờ để lại các con trỏ hàm phân giải một nửa cho lần thử tiếp theo gặp lỗi. Thứ hai, tên DLL nó báo cáo đến từ BuildDllName, vốn trả về pdfium.dll trong điều kiện bình thường và pdfium.v8.dll khi cờ toàn cục EnableV8Engine được đặt. Nếu ứng dụng của bạn bật công cụ JavaScript V8 cho XFA hoặc biểu mẫu có tập lệnh, văn bản lỗi sẽ trỏ tới bản dựng V8, chứ không phải bản thông thường, vì vậy bạn sẽ thay thế đúng tệp tin ngay trong lần đầu tiên
Tệp PDFium DLL có an toàn để tải từ một luồng nền không?
Giờ đây, việc tải thư viện là an toàn từ bất kỳ luồng nào; nhưng gọi API PDFium từ nhiều luồng khác nhau thì vẫn không an toàn. Đó là hai đảm bảo riêng biệt, và việc tách biệt chúng là sự khác nhau giữa một nhóm tiến trình làm việc (worker pool) ổn định và một sự cố sập ứng dụng gián đoạn. Việc kết xuất nền thông thường chạy từng kết xuất trang thông qua một tiến trình làm việc TPdfFuture<T>, và tiến trình tương lai đầu tiên chạm tới TPdf chính là thứ kích hoạt liên kết lười (lazy bind). Nếu không được bảo vệ, hai tiến trình làm việc có thể cùng quan sát thấy một thư viện chưa được tải, cùng chạy chuỗi liên kết, ghi đè lên handle mô-đun và rò rỉ lượt tải đầu tiên
Công việc phiên bản v2.11.0 khép lại kẽ hở đó bằng PDFiumLoadLock, một TRTLCriticalSection toàn cục của tiến trình bao bọc lối vào của cả LoadLibrary và UnloadLibrary. Phần tranh chấp găng (critical section) làm cho chuỗi kiểm tra rồi tải có tính nguyên tử (atomic), do đó không có hai luồng nào có thể đồng thời nhìn thấy Loaded=False và thực hiện liên kết kép, và không luồng nào có thể giải phóng DLL trong khi luồng khác đang ở giữa quá trình liên kết. Khóa được tạo trong phần initialization của đơn vị (unit) và bị gỡ bỏ trong phần finalization, được bảo vệ bởi cờ PDFiumLoadLockReady để việc ghép cặp là an toàn ngay cả trong quá trình tắt máy. Nếu bạn cần mô hình worker-and-reply đầy đủ với tính năng hủy bỏ, bài viết đi kèm về kết xuất nền với các tiến trình tương lai có thể hủy bỏ sẽ hướng dẫn chi tiết từ đầu đến cuối
uses
PDFium, FPdfAsync;
// Worker method runs on a background thread. The first future to bind
// pdfium.dll is serialised by PDFiumLoadLock, so a second concurrent
// worker cannot double-bind or leak the module handle.
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; // this TPdf is owned by one thread only
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;
Những gì khóa tải không bảo vệ
Ranh giới ở đây đáng được nêu rõ ràng, vì rất dễ hiểu sai tầm vóc của cách khắc phục. PDFiumLoadLock chỉ tuần tự hóa trạng thái liên kết toàn cục: handle mô-đun và các chỉ định con trỏ hàm FPDF_*. API PDFium C bên dưới vẫn không an toàn theo luồng (thread-safe), chính xác như các tiêu đề ngược dòng khai báo, vì vậy việc gọi các hàm FPDF_* hoặc chia sẻ một handle FPDF_DOCUMENT giữa các luồng vẫn là trách nhiệm của bạn để thực hiện tuần tự hóa. Hình thái an toàn là hình thái ở trên: mỗi worker sở hữu TPdf riêng của nó và không bao giờ chuyển giao một tài liệu đang hoạt động cho một luồng khác. Để tìm hiểu sâu hơn về ranh giới đó và các quy tắc ABI xung quanh nó, hãy xem bài viết về tăng cường liên kết PDFium VCL chống lại lỗi ABI và bộ nhớ
Tại sao phần finalization và FPDF_DestroyLibrary lại quan trọng
Một khoảng trống tinh tế hơn từng nằm trong đường dẫn tắt máy. Đơn vị PDFium không có phần finalization, nghĩa là FPDF_DestroyLibrary không bao giờ chạy khi thoát tiến trình; hệ điều hành thu hồi bộ nhớ DLL và bỏ qua quá trình tự dọn dẹp của PDFium, cụ thể là việc ghép luồng làm việc (worker-thread join), giải phóng V8 isolate khi EnableV8Engine được đặt, và hủy bỏ phông chữ cũng như bộ nhớ đệm. Trên một khối lượng công việc kết xuất thông thường, đó là một sự rò rỉ âm thầm khi gỡ bỏ, nhưng khi bật công cụ V8, nó sẽ rò rỉ một JavaScript isolate ở mỗi lần chạy, hiển thị rất nhanh dưới các tác vụ tự động hóa lặp đi lặp lại
Phần finalization giờ đây gọi UnloadLibrary, vốn sẽ kích hoạt FPDF_DestroyLibrary và giải phóng mô-đun. Việc này nằm ở phạm vi đơn vị (unit scope) vì một lý do: TPdf.Destroy chỉ đặt Active := False để đóng tài liệu hiện tại, và nó chủ định không dỡ bỏ thư viện toàn cục, giúp một ứng dụng đa thực thể giữ một liên kết PDFium chia sẻ hoạt động trong suốt vòng đời tiến trình. Việc đưa phần tháo dỡ vào finalization nghĩa là thư viện chia sẻ được phát hành chính xác một lần, khi đơn vị dỡ tải, và UnloadLibrary kiểm tra cờ Loaded của nó trước tiên để cuộc gọi là một no-op vô hại nếu PDFium chưa bao giờ được sử dụng
Một danh sách kiểm tra triển khai ngắn gọn
Ba thói quen sẽ giúp ngăn chặn hầu như mọi lỗi tải trong thực tế. Khớp kiến trúc DLL với tiến trình máy chủ và phân phối nó dưới thư mục DLLs/Win32 hoặc DLLs/Win64; giữ cho tệp pdfium.dll đồng bộ với phiên bản liên kết để không bị thiếu export bắt buộc nào; và không bao giờ chia sẻ một đối tượng TPdf hoặc một handle tài liệu qua các luồng, ngay cả khi bản thân liên kết hiện tại đã có tính nguyên tử. Nếu bạn xây dựng cùng một mã nguồn cho cả Delphi và Lazarus, những khác biệt về đóng gói gây cản trở triển khai đa mục tiêu được đề cập trong các ghi chú về các cạm bẫy trình biên dịch chéo Delphi và FPC. Các chẩn đoán, khóa tải và dọn dẹp finalization được mô tả ở đây đều đi kèm trong PDFium Component dành cho Delphi và C++Builder