Technical Article

การจัดส่งไฟล์ PDFium DLL ใน Delphi: การแก้ไขปัญหาความล้มเหลวของการโหลด

เมื่อไฟล์ pdfium.dll ไม่ยอมโหลดบนเครื่องของลูกค้า PDFium Component สำหรับ Delphi และ Lazarus จะช่วยเปลี่ยนรหัสข้อผิดพลาดของระบบที่เข้าใจยากให้กลายเป็นสาเหตุที่เฉพาะเจาะจง ฟังก์ชัน CheckLoadLibrary จะตีความรหัสข้อผิดพลาด ERROR_BAD_EXE_FORMAT (193) เป็นความไม่เข้ากันของสถาปัตยกรรม 32/64 บิต และ CheckGetProcAddress จะรายงานกรณีที่ไม่มีการส่งออกฟังก์ชัน (missing export) ว่าเป็นเพราะไฟล์ pdfium.dll เก่ากว่ารหัสอ้างอิง ข้อความแจ้งเตือนทั้งสองรูปแบบนี้จะระบุแนวทางแก้ไขให้คุณทราบโดยตรงแทนที่จะปล่อยให้คุณต้องคาดเดาเอง

สิ่งนี้มีความสำคัญเนื่องจากไฟล์ DLL ดั้งเดิมสามารถล้มเหลวได้ในสามรูปแบบที่แตกต่างกัน และข้อความดิบจากระบบปฏิบัติการมักจะนำสาเหตุทั้งหมดมารวมกัน สถาปัตยกรรมอาจจะไม่ถูกต้อง, ไฟล์ไบนารีที่จัดส่งอาจจะเก่าเกินไปสำหรับตัวอ้างอิง Pascal ที่เรียกใช้ หรือเกิดปัญหา Race Condition ระหว่างเธรดสองเธรดที่พยายามโหลดและผูกไฟล์ในเวลาเดียวกัน แต่ละสาเหตุมีแนวทางแก้ไขที่แตกต่างกัน และจุดประสงค์ของการเพิ่มระบบวินิจฉัยในการปรับปรุงรอบการโหลดเวอร์ชัน v2.11.0 คือการชี้แจงให้คุณทราบอย่างชัดเจนว่าคุณกำลังเผชิญกับปัญหาใดอยู่

เหตุใด pdfium.dll จึงโหลดไม่สำเร็จพร้อมข้อผิดพลาดรูปแบบ EXE ที่ไม่ถูกต้อง?

รหัสข้อผิดพลาด Windows 193, ERROR_BAD_EXE_FORMAT หมายความว่าระบบค้นพบและเปิดไฟล์แล้ว แต่ประเภท PE machine ของไฟล์ไม่ตรงกับโปรเซสที่เรียกใช้งาน โปรแกรมที่รันแบบ 64 บิตไม่สามารถโหลด pdfium.dll แบบ 32 บิตได้ และโปรแกรมแบบ 32 บิตก็ไม่สามารถโหลดไฟล์แบบ 64 บิตได้เช่นกัน เนื่องจากไฟล์นั้นมีอยู่จริง สัญชาตญาณที่จะค้นหาไฟล์ DLL ที่ขาดหายไปจึงเป็นแนวทางที่ผิดพลาดอย่างสิ้นเชิง

PDFium Component จะเข้ามาสกัดปัญหานี้ในฟังก์ชัน CheckLoadLibrary: เมื่อคำสั่ง LoadLibrary ส่งคืนแฮนเดิลว่างเปล่า (null handle) และรหัส GetLastError เป็น 193 ระบบจะแสดงคำแนะนำเพิ่มเติมว่าตรวจพบไฟล์ DLL แล้วแต่สถาปัตยกรรมไม่ตรงกับโปรเซสที่เรียกใช้ และชี้ไปยังโฟลเดอร์ผลลัพธ์ DLLs/Win32 หรือ DLLs/Win64 ที่ถูกต้อง โดยโฟลเดอร์ย่อยจะถูกเลือกผ่าน BuildDllSubDir ซึ่งจะส่งกลับ Win64 เมื่อ IsWin64 เป็นจริง และส่งกลับ Win32 ในกรณีอื่น การจัดส่งไฟล์ DLL ไว้ภายใต้โฟลเดอร์ที่โค้ดค้นหาจริงจะช่วยแก้ปัญหาความไม่เข้ากันนี้ได้

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;

กรณีที่ไม่มีการส่งออก PDFium (Missing Export) กำลังบอกอะไรคุณ?

ประเภทความล้มเหลวที่สองคือความไม่สอดคล้องของเวอร์ชัน (version skew) PDFium มีการส่งออกฟังก์ชันใหม่ๆ เพิ่มขึ้นตามเวลา และตัวอ้างอิงของ Pascal จะทำการเชื่อมโยงทุกฟังก์ชันที่ต้องการผ่าน CheckGetProcAddress ในระหว่างขั้นตอน LoadLibrary หากไม่มีการส่งออกฟังก์ชันที่จำเป็น แสดงว่าตัวอ้างอิงเป็นเวอร์ชันที่ใหม่กว่า pdfium.dll ที่มีอยู่บนดิสก์ และการวินิจฉัยที่ถูกต้องคือไฟล์ DLL ที่จัดส่งนั้นเก่าเกินไป ไม่ใช่ไฟล์เสียหาย ฟังก์ชัน CheckGetProcAddress จะแจ้งปัญหานี้อย่างตรงไปตรงมา: โดยจะแสดงข้อผิดพลาด EPdfError รายงานว่าไฟล์ pdfium.dll ที่จัดส่งนั้นเก่ากว่าตัวเชื่อมโยง PDFiumPas เวอร์ชันนี้ และระบุพาธ DLLs/<subdir> ที่ถูกต้องที่ควรจะเก็บไฟล์ไบนารีที่ตรงกันไว้

รายละเอียดสองประการที่ช่วยให้กระบวนการนี้มีความเสถียร ประการแรกคือ CheckGetProcAddress จะเรียกใช้ UnloadLibrary ก่อนที่จะแสดงข้อผิดพลาด เพื่อไม่ให้การเชื่อมโยงที่ไม่สมบูรณ์เหลือตัวชี้ฟังก์ชันที่แก้ไขไม่เสร็จสิ้นค้างไว้ให้เกิดปัญหาในครั้งถัดไป ประการที่สองคือ ชื่อไฟล์ DLL ที่รายงานจะดึงมาจาก BuildDllName ซึ่งจะส่งคืน pdfium.dll ตามปกติ และส่งคืน pdfium.v8.dll เมื่อมีการเปิดใช้งานแฟล็กโกลบอล EnableV8Engine หากแอปพลิเคชันของคุณเปิดใช้งานเอนจิน JavaScript V8 สำหรับแบบฟอร์ม XFA หรือแบบฟอร์มสคริปต์ ข้อความข้อผิดพลาดจะชี้ไปยังไฟล์เวอร์ชัน V8 ไม่ใช่ไฟล์ปกติ เพื่อให้คุณเปลี่ยนไฟล์ได้ถูกต้องตั้งแต่ครั้งแรก

การโหลดไฟล์ PDFium DLL จากเธรดเบื้องหลังปลอดภัยหรือไม่?

ในปัจจุบัน การโหลดไลบรารีมีความปลอดภัยเมื่อทำจากเธรดใดๆ ทว่าการเรียกใช้งาน PDFium API จากหลายๆ เธรดพร้อมกันยังคงไม่ปลอดภัย ซึ่งทั้งสองส่วนนี้มีการรับประกันการทำงานที่แยกจากกัน และการแยกแยะข้อนี้คือจุดแบ่งระหว่างพูลของผู้ทำงานที่เสถียรกับการแครชของโปรแกรมแบบสุ่ม การเรนเดอร์เบื้องหลังมักจะประมวลผลการเรนเดอร์แต่ละหน้าผ่านผู้ทำงาน TPdfFuture<T> และงานแรกที่เรียกใช้งาน TPdf จะเป็นตัวกระตุ้นการเชื่อมโยงแบบ Lazy Bind หากไม่มีการป้องกัน ผู้ทำงานสองคนอาจตรวจพบว่าไลบรารียังไม่ได้รับการโหลดพร้อมกัน และต่างรันขั้นตอนการเชื่อมโยง เขียนทับแฮนเดิลของโมดูล และทำให้เกิดการรั่วไหลของการโหลดครั้งแรก

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;

สิ่งที่ตัวล็อคการโหลด (Load Lock) ไม่ได้ปกป้อง

ขอบเขตการทำงานในส่วนนี้ควรชี้แจงอย่างชัดเจน เพราะง่ายต่อการตีความความสามารถระบบเกินจริง PDFiumLoadLock จะควบคุมเฉพาะขั้นตอนการเชื่อมโยงในระดับโกลบอลเท่านั้น: เช่น แฮนเดิลโมดูลและการกำหนดตัวชี้ฟังก์ชัน FPDF_* ส่วนตัวโครงสร้าง PDFium C API หลักนั้นยังคงไม่รองรับความปลอดภัยของเธรด (not thread-safe) ตามที่ระบุในข้อกำหนดต้นทาง ดังนั้นการเรียกใช้ฟังก์ชัน FPDF_* หรือการใช้แฮนเดิล FPDF_DOCUMENT ร่วมกันข้ามเธรดจึงยังคงเป็นหน้าที่ของคุณที่จะต้องจัดระเบียบการทำงานเอง รูปแบบโครงสร้างที่ปลอดภัยคือรูปแบบตัวอย่างข้างต้น: โดยผู้ทำงานแต่ละคนจะเป็นเจ้าของ TPdf ของตัวเอง และไม่มีการส่งต่อเอกสารที่กำลังใช้งานไปยังเธรดอื่น สำหรับรายละเอียดเพิ่มเติมเกี่ยวกับขอบเขตและกฎข้อบังคับ ABI สามารถดูได้ในหัวข้อ hardening the PDFium VCL binding against ABI and memory faults

เหตุใดส่วน Finalization และ FPDF_DestroyLibrary จึงมีความสำคัญ

ในอดีตมีปัญหาซ่อนอยู่ในส่วนขั้นตอนการปิดระบบการทำงาน ยูนิต PDFium ไม่มีส่วน finalization ส่งผลให้ฟังก์ชัน FPDF_DestroyLibrary ไม่เคยถูกรันเมื่อโปรเซสปิดตัวลง ระบบปฏิบัติการจะเรียกคืนหน่วยความจำของ DLL และข้ามขั้นตอนการทำความสะอาดไฟล์ของ PDFium เอง (เช่น การเชื่อมเธรดผู้ทำงาน, การทำลายพื้นที่ V8 Isolate เมื่อตั้งค่า EnableV8Engine ตลอดจนการล้างข้อมูลฟอนต์และแคช) ในการเรนเดอร์ทั่วไป ปัญหานี้อาจเป็นเพียงการรั่วไหลของหน่วยความจำเงียบๆ ในขั้นตอนปิดโปรแกรม แต่หากมีการเปิดใช้งานเอนจิน V8 มันจะทำให้เกิดการรั่วไหลของ JavaScript Isolate ในทุกรอบการรัน ซึ่งจะแสดงอาการรวดเร็วเมื่อมีการรันงานระบบอัตโนมัติซ้ำๆ

ปัจจุบัน ส่วน finalization จะทำหน้าที่เรียกใช้งาน UnloadLibrary ซึ่งจะส่งไปเรียก FPDF_DestroyLibrary และคืนหน่วยความจำโมดูล การตั้งคำสั่งในระดับขอบเขตยูนิตนี้มีเหตุผลคือ: เมธอด TPdf.Destroy จะทำหน้าที่เพียงกำหนด Active := False เพื่อปิดเอกสารปัจจุบันเท่านั้น และตั้งใจที่จะไม่โหลดไลบรารีระดับโกลบอลออก เพื่อให้แอปพลิเคชันของคุณที่มีการรันหลายอินสแตนซ์ยังคงสามารถใช้การเชื่อมโยง PDFium ร่วมกันได้ตลอดอายุโปรเซส การใส่คำสั่งทำลายในส่วน finalization หมายความว่าไลบรารีร่วมจะถูกปล่อยหน่วยความจำเพียงครั้งเดียวเมื่อทำการปิดยูนิต และ UnloadLibrary จะตรวจสอบแฟล็ก Loaded ก่อนเสมอ ทำให้การเรียกคำสั่งไม่มีผลใดๆ หากไม่มีการเรียกใช้ PDFium เลย

รายการตรวจสอบการจัดส่งระบบฉบับย่อ

นิสัยการทำงานสามประการจะช่วยป้องกันปัญหาความล้มเหลวของการโหลดในฝั่งผู้ใช้ได้เกือบทั้งหมด ข้อแรกคือ จับคู่สถาปัตยกรรมไฟล์ DLL ให้ตรงกับโปรเซสและจัดส่งภายใต้โฟลเดอร์ DLLs/Win32 หรือ DLLs/Win64 ข้อสองคือ ตรวจสอบให้เวอร์ชันของไฟล์ pdfium.dll ตรงกับเวอร์ชันตัวเชื่อมโยงเพื่อป้องกันไม่ให้ฟังก์ชันที่จำเป็นขาดหายไป และข้อสามคือ ห้ามแชร์ออบเจกต์ TPdf หรือแฮนเดิลเอกสารข้ามเธรด แม้ว่าขั้นตอนการเชื่อมโยงจะมีความปลอดภัยของเธรดแล้วก็ตาม หากคุณสร้างโปรแกรมจากโค้ดชุดเดียวกันสำหรับทั้ง Delphi และ Lazarus ความแตกต่างของการแพ็กเกจที่อาจทำให้เกิดความสับสนในการจัดส่งข้ามเป้าหมายจะอธิบายไว้ในหัวข้อ Delphi and FPC cross-compiler pitfalls ส่วนระบบการวินิจฉัย, ตัวล็อคการโหลด และการทำความสะอาดในขั้นตอน Finalization ที่อธิบายในบทความนี้ ถูกจัดส่งมาพร้อมกับ PDFium Component สำหรับ Delphi และ C++Builder