Bài viết kỹ thuật

Nạp thư viện native PDFium trên mọi mục tiêu

Thành phần PDFium tìm thư viện native của nó qua một chuỗi tìm kiếm cố định, có thứ tự, thay vì để mặc loader hệ điều hành, vì một cây triển khai tường minh là một cây triển khai bạn có thể debug. Trên Windows, chuỗi đó tìm một thư mục con Win32 hay Win64 mà trình cài đặt đã phát hành sẵn. Trên các mục tiêu khác, nó dựng tên thư mục con từ các macro mục tiêu Free Pascal, dạng <cpu>-<os>, để cây triển khai đọc giống hệt cây unit đã biên dịch. Quyết định cuối đó đã giới thiệu một bug đáng cả bài viết này, vì nguyên nhân là một chữ cái hoa và triệu chứng là sự im lặng

Chuỗi, theo thứ tự

Bốn vị trí, thử lần lượt, rồi loader nền tảng như phương án cuối cùng. Một là bố cục ưa thích, một thư mục DLLs cạnh tệp thực thi chứa một thư mục con cho mỗi mục tiêu. Hai là bố cục thay thế với thư mục con mục tiêu nằm ngay cạnh tệp thực thi. Ba là bố cục phẳng kiểu cũ, thư viện nằm cạnh tệp thực thi mà chẳng có thư mục con nào. Bốn, chỉ trên Windows, thư mục hệ thống, cần chú ý vì một tiến trình 32-bit phải tìm trong SysWOW64 và một tiến trình 64-bit trong System32, và trên Windows 32-bit cái trước không tồn tại nên tra cứu phải lùi lại. Chỉ sau tất cả những điều đó loader mới được nhờ tự tìm kiếm

Sơ đồ chuỗi tìm thư viện native PDFium cho Delphi, từ thư mục con mục tiêu DLLs qua các bố cục thay thế, phẳng và thư mục hệ thống Windows tới loader nền tảng
Bốn vị trí tường minh được dò lần lượt trước khi loader hệ điều hành được nhờ tự tìm kiếm

Cố ý không có bước thư mục hệ thống ngoài Windows. Đường tìm kiếm riêng của loader nền tảng, được dẫn dắt bởi cấu hình linker runtime và môi trường library path, đã phủ sẵn vùng đất đó, và việc nhân bản nó trong Pascal nghĩa là hiện thực lại những quy tắc thay đổi theo từng bản phân phối. Chẩn đoán thất bại trong chuỗi Windows được đề cập riêng trong triển khai DLL PDFium và chẩn đoán thất bại nạp

Tên thư mục con đến từ đâu

Trên Windows, nó là Win32 hay Win64, do độ rộng bit của tiến trình đang chạy quyết định chứ không phải của hệ điều hành, vì đó là thứ quyết định binary nào có thể được nạp. Ở mọi nơi khác, tên được dựng từ các macro mục tiêu của trình biên dịch để một máy build cho hai kiến trúc tạo ra hai cây tách bạch rõ ràng, và để thư mục chứa thư viện native nằm cạnh thư mục chứa các unit đã biên dịch với cùng một cái tên

function BuildDllSubDir(UseV8: Boolean): string;
begin
{$IFDEF MSWINDOWS}
  if IsWin64 then
    Result := 'Win64'
  else
    Result := 'Win32';
{$ELSE}
  // Các macro trình biên dịch viết hoa hệ điều hành ("Linux", "Darwin")
  // trong khi thư mục đầu ra unit của package thì không, nên hai bên chỉ
  // khớp nhau sau khi gập chữ. Trên một hệ tệp phân biệt chữ hoa-thường,
  // sự khác biệt đó chính là toàn bộ phép tra cứu
  Result := LowerCase({$I %FPCTARGETCPU%} + '-' + {$I %FPCTARGETOS%});
{$ENDIF}
end;

Vì sao một chữ cái hoa phá sập cả chuỗi

Macro trình biên dịch viết hệ điều hành mục tiêu với chữ cái đầu viết hoa: Win64, Linux, Darwin. Package Lazarus ghi đầu ra unit của nó vào một thư mục đặt tên từ biến mục tiêu riêng của nó, vốn là chữ thường: win64, linux, darwin. Hai cách đánh vần của cùng một thứ, và không cách nào nhận ra trên Windows, nơi hệ tệp không phân biệt chúng

Trên Linux, chúng là hai thư mục khác nhau. Một bản triển khai đặt shared object vào DLLs/x86_64-linux thì vô hình với một loader đang tìm DLLs/x86_64-Linux, nên cả bốn bước tường minh của chuỗi đều trượt và mã rơi tiếp xuống việc để loader nền tảng tự tìm kiếm. Đôi khi điều đó chạy được, nếu thư viện tình cờ được cài toàn hệ thống, và đôi khi thì không, và dù thế nào thì cây triển khai được sắp đặt công phu cũng chẳng đóng góp gì. Sự thất bại không có thông điệp lỗi vì chẳng gì thất bại cả: mỗi bước đều báo đúng rằng tệp không nằm ở nơi nó đã tìm

Một chữ cái hoa trong macro mục tiêu FPC phá phép tra cứu DLL PDFium trên Linux ra sao: loader tìm DLLs/x86_64-Linux trong khi thư mục đã triển khai là DLLs/x86_64-linux, thứ chỉ khớp trên Windows không phân biệt chữ hoa-thường
Cùng một mục tiêu đánh vần hai kiểu khớp nhau trên Windows và âm thầm trượt trên một hệ tệp phân biệt chữ hoa-thường

Chương trình thăm dò, được biên dịch và chạy

Lớp bug này không thể được tìm ra bằng cách đọc, và cũng không thể bằng cách biên dịch. Kỹ thuật quen thuộc để xác minh một nhánh nền tảng mà chưa từng biên dịch trên máy phát triển là sao chép unit vào một thư mục tạm, đổi tên nó, thay conditional nền tảng bằng một symbol chưa từng được định nghĩa, và biên dịch bản sao; nếu nó biên dịch được, mệnh đề uses và các chữ ký gọi trên đường đó ít nhất tự nhất quán. Cách đó chạy tốt với một unit tự chứa

Ở đây thì không chạy. Unit binding chính rất lớn và kéo theo LCL, nên nó không thể đơn giản được sao chép và biên dịch với symbol Windows bị tắt. Vì vậy thay vào đó, một ít hàm mà thay đổi chạm tới được chép nguyên văn vào một chương trình nhỏ tự chứa, và chương trình đó được chạy. Nó in ra x86_64-Win64, và sự lệch nhau hiện rõ trong một dòng đầu ra. Biên dịch cùng chương trình đó đã chẳng nói với bạn điều gì, vì chuỗi hoàn toàn hợp lệ; chỉ có giá trị của nó là sai

program ProbeSubDir;
{$MODE DELPHI}
uses
  SysUtils;
begin
  // In ra, đừng khẳng định. Điểm mấu chốt là nhìn giá trị mà một macro
  // thực sự mở rộng thành trên toolchain này
  Writeln('raw:    ', {$I %FPCTARGETCPU%}, '-', {$I %FPCTARGETOS%});
  Writeln('folded: ', LowerCase({$I %FPCTARGETCPU%} + '-' +
    {$I %FPCTARGETOS%}));
end.

Bài học chung: khi một thay đổi đa nền tảng liên quan đến giá trị của một thứ chứ không phải kiểu của nó, xác minh chỉ-bằng-biên-dịch không phải là xác minh. Hãy in nó ra. Bộ khác biệt đa trình biên dịch rộng hơn giữa Delphi và Free Pascal được gom trong bài viết về các cạm bẫy đa trình biên dịch Delphi và FPC

Để nền tảng tự giải thích các thất bại nạp của chính nó

Nhánh Windows của loader liệt kê bằng tay các lý do một lần nạp có thể thất bại, vì những phân biệt hữu ích ở đó — một lệch kiến trúc, một phụ thuộc bậc trung gian bị thiếu, một đường không phân giải được — ánh xạ tới các mã lỗi đáng được gọi tên riêng. Ngoài Windows, unit loader di động đã trả về một chuỗi mô tả phủ cùng vùng đất, nên nhánh không-Windows dùng nó trực tiếp thay vì suy lại các phân loại từ một số lỗi mà nghĩa thay đổi theo từng hệ thống

Cưỡng lại cám dỗ chuẩn hóa hai thứ đó thành một thông điệp là có chủ đích. Một thất bại nạp là một vấn đề triển khai, và người đọc thông điệp cần vốn từ riêng của nền tảng để tìm kiếm nó

Một vụ va chạm tên có tính đệ quy

Một cái bẫy nữa, nhỏ và sắc. Unit loader di động export một thủ tục tên UnloadLibrary, và unit binding có một thủ tục cùng tên tự làm sổ sách của mình trước khi thả handle. Bên trong thủ tục đó, một lệnh gọi không định danh tới UnloadLibrary phân giải về cái trong unit hiện tại, thứ tự gọi chính nó. Bản sửa là định danh lệnh gọi bằng tên unit

Đây là cùng hình dạng với các vấn đề che khuất định danh thống trị các bản chuyển nền Free Pascal nói chung: unit Windows export các hàm min và max kiểu nguyên che khuất các bản dấu phẩy động, và một kiểu đồng bộ hóa che khuất lớp cùng tên, và trong mọi trường hợp phép phân giải phụ thuộc thứ tự của mệnh đề uses. Định danh call site là bản sửa không phụ thuộc vào việc ai đó giữ nguyên thứ tự đó về sau

Va chạm tên UnloadLibrary trong thành phần PDFium: một lệnh gọi không định danh trong unit binding Delphi đệ quy vào chính nó, trong khi lệnh gọi có định danh unit tới được unit loader di động và thả handle
Định danh call site đưa việc thả handle đi qua unit loader thay vì đệ quy vào unit binding

Danh mục kiểm tra triển khai

Ba thứ chiếm phần lớn các thất bại nạp một khi phép tính đường đã đúng. Kiến trúc phải khớp tiến trình, không phải máy, nên một ứng dụng 32-bit trên Windows 64-bit cần binary 32-bit. Bản build bật V8 có một tên tệp khác, nên một bản triển khai trộn lẫn chúng sẽ trông đúng mà chẳng nạp được gì. Và chỉ một biến thể có thể sống trong một thư mục hệ thống tại một thời điểm, lý do tốt để chuộng bố cục thư mục con tường minh thay vì cài bất cứ thứ gì toàn hệ thống

Riêng với Lazarus, hãy đặt thư viện native dưới DLLs/<cpu>-<os> bằng chữ thường, cạnh tệp thực thi, và nó sẽ được bước đầu tiên của chuỗi tìm thấy trên mọi mục tiêu. Mẫu trình xem thực hành điều này trên Lazarus được mô tả trong bài viết về trình xem Lazarus và FPC, và mức hỗ trợ nền tảng hiện tại được liệt kê trên trang sản phẩm PDFium Delphi component