Bài viết kỹ thuật

Liên kết tĩnh jbig2enc vào Free Pascal mà không cần DLL

PDFlibPas 3.538.0 liên kết tĩnh encoder JBIG2 bên ngoài vào các chương trình Free Pascal và Lazarus. Dự án thêm unit PDFlibJBIG2EncC, chính là unit mà Delphi và C++Builder đã dùng, và encoder nằm ngay trong executable nên không cần phân phối thêm tệp nào bên cạnh nó. Kết quả này đảo ngược kết luận trước đây về tính năng này, vốn cho rằng Free Pascal chỉ có thể truy cập encoder bên ngoài thông qua DLL

Vì sao DLL trông như lựa chọn duy nhất?

DLL trông như lựa chọn duy nhất vì ba đường liên kết thất bại theo ba cách không liên quan, còn không có compiler switch nào chạm được vào chúng. Linker nội bộ từ chối thẳng các section COMDAT liên kết. External link qua binutils đi kèm lại crash trong quá trình garbage collection của section, bước mà Free Pascal luôn truyền vào target Windows 64-bit. binutils mới hơn hoàn toàn không xử lý được link script của Free Pascal. Rebuild phía C++ bằng toolchain kia chỉ đổi một kiểu từ chối thành kiểu khác, vì việc instantiate template và inline vốn phát ra weak external symbol, còn Free Pascal báo chúng là Unsupported COFF symbol type 105. Không bằng chứng nào trong số đó sai, và bài viết trước về backend encoder JBIG2 và linker Free Pascal lần lượt đi qua từng ngõ cụt theo cách đến nay vẫn tái hiện được. Điều sai là giả định về nơi có thể đặt bản sửa. Mọi lần thử đều đi qua compiler hoặc linker, nhưng không bên nào thay đổi được nội dung đã có trong object file. Object file mới là vấn đề từ đầu. ObjConv đọc COFF và ghi lại COFF, còn mỗi cấu trúc khiến Free Pascal nghẽn đều có một dạng tương đương cơ học mà nó chấp nhận

Lỗi không bao giờ nói nguyên nhân

Linker nội bộ của Free Pascal chỉ triển khai pick-any COMDAT được một nửa, và chính phần triển khai nửa vời này là thứ khó chẩn đoán nhất ở đây. Nó vẫn fold các định nghĩa trùng lặp như format yêu cầu. Nhưng TExeOutput.RemoveUnreferencedSections chuyển hướng qua exesymbol tới định nghĩa thắng khi đánh dấu section đang được dùng, trong khi TCoffexeoutput.DoRelocationFixup lại đọc trực tiếp objreloc.symbol.objsection. Khi một section đang dùng tham chiếu tới symbol mà object của nó định nghĩa trong một bản sao đã thua khi fold, hai pass nhìn thấy hai section khác nhau và link dừng ở Internal error 200603061

Hãy so sánh với hai giới hạn ở hai phía của nó. Unsupported COFF symbol type 105 nói rằng đó là weak external. Associative or exact match COMDAT sections are not yet supported nói đó là associative COMDAT và còn chỉ ra symbol gây lỗi. Internal error 200603061 thì không nói gì: không có tên symbol, tên section, tên file hay thông tin phase. Đây cũng là trường hợp thông thường chứ không phải góc cạnh hiếm gặp, vì MSVC đưa mọi string literal và mọi inline hoặc template instantiation vào pick-any COMDAT, còn trong 186 object của bộ encoder này linker đã thực hiện 2656 lần fold. Build với /Gy- giữ các hàm thông thường bên ngoài section COMDAT theo từng hàm, nhưng để string literal và template instantiation nguyên tại chỗ

Vì sao việc tạo stub cho symbol CRT luôn trông như symbol cuối cùng đã làm hỏng nó?

Vì linker chỉ đi tới pass fixup sau khi mọi symbol đã được resolve. Khi vẫn còn thiếu bất kỳ thứ gì, tiến trình kết thúc sớm với Undefined symbol và lỗi COMDAT chưa có cơ hội lộ ra. Điền stub runtime C cuối cùng vào, linker tiến thêm một phase và đâm thẳng vào internal error 200603061. Vì vậy triệu chứng ngoài thực tế đánh lừa một cách có hệ thống: khi lần lượt thêm các thân hàm Pascal cho những C symbol được tham chiếu, lúc nào cũng có vẻ như phần thêm gần nhất đã phá build, hoặc như thể đã vượt qua một ngưỡng khoảng một trăm stub. Cả hai đều không đúng. Symbol nào được thêm cuối và tổng cộng thêm bao nhiêu symbol đều không liên quan, vì lỗi đã tiềm ẩn từ object đầu tiên và chỉ có thể chạm tới sau khi resolve thành công. Khi linker đổi thông báo sau khi bạn sửa một vấn đề không liên quan, hãy tự hỏi liệu bạn chỉ vừa đẩy tiến trình sang phase tiếp theo thay vì gây ra regression

Bản sửa là một pass ObjConv, không phải compiler flag

Toàn bộ bản sửa là một lệnh hậu xử lý duy nhất chạy trên từng object đã biên dịch: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Ba option trong đó được thêm cho công việc này. -xw chuyển symbol IMAGE_SYM_CLASS_WEAK_EXTERNAL thành external thông thường. -xn chuẩn hóa symbol IMAGE_SYM_CLASS_NULL như _fltused, nếu không Free Pascal sẽ báo Unsupported COFF symbol type 0. -xc mới là phần xử lý chính: nó hạ mọi section COMDAT thành section thường và biến các symbol do chúng định nghĩa thành static. Cách này loại bỏ lỗi bằng cách loại bỏ chính quyết định gây lỗi, vì khi không còn section COMDAT thì không có folding, không có bản sao thắng để một pass chuyển hướng tới còn pass khác không tìm thấy, và các section unwind associative .pdata cùng .xdata cũng biến mất theo. Cái giá là có thật nhưng nhỏ: những bản sao vốn có thể hợp nhất hợp lệ giờ mỗi bản đều được giữ lại

Việc đổi prefix -np:__imp_:pdflibimp_ giải quyết một xung đột riêng. MSVC gọi Win32 API được import qua các ô gián tiếp có tên __imp_*, Free Pascal dành prefix đó cho cơ chế import của mình, và định nghĩa trực tiếp bất kỳ tên nào như vậy lại kích hoạt cùng internal error 200603061. Đổi tên các ô cho phép phía Pascal công bố chúng như biến thường và điền giá trị lúc runtime. Các object được biên dịch với bộ cờ static-link /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, đồng thời tắt image codec, nên các đường file I/O và codec không dùng tới cần ít link-only stub hơn nhiều. Chúng nằm trong Lib\thirdparty\Win64f, còn đường Delphi và C++Builder tiếp tục link bộ Win64x riêng của nó không thay đổi, đúng với kết quả cần có của một bản sửa portability chỉ giới hạn ở một toolchain

Phía Pascal vẫn phải export những gì

Free Pascal resolve import của C object bằng tên symbol và cần tên đó được ghi rõ, vì vậy mọi routine Pascal thay cho C entry point đều phải có clause public name tường minh. Delphi lấy tên routine làm tên symbol nên không cần clause nào, đó là lý do một unit phục vụ được cả hai compiler với các clause nằm dưới {$IFDEF FPC}. Cái bẫy là khai báo external 'msvcrt.dll' không đáp ứng được gì: nó chỉ tạo một import, không bao giờ tạo ra definition mà linked object có thể bind vào. Phải có thân hàm forwarding

// Khai báo external chỉ tạo import, không có linked object nào
// có thể bind vào nó
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// Thân hàm Pascal được công bố dưới đúng tên C symbol mới là nơi
// bộ object thực sự bind vào
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

Variadic entry point phá vỡ mẫu này, vì wrapper Pascal không thể forward varargs của chính nó cho một callee varargs khác. Cách thoát là ngừng làm wrapper: export một routine naked dưới tên C rồi tail-jump tới implementation thật với các thanh ghi tham số và stack đúng nguyên trạng như caller đã sắp xếp. Lớp JPEG 2000 đã xử lý snprintfvsnprintf theo cách này, nhảy tới cách viết msvcrt có prefix gạch dưới vì chỉ UCRT export các tên không có prefix. Một giới hạn liên quan cũng xuất phát từ cùng internal error: các ô import đã đổi tên được điền từ section initialization qua GetModuleHandleAGetProcAddress, thay vì từ static initializer, vì lấy địa chỉ routine import trong initializer khiến compiler phát ra fixup mà nó không xử lý được và lại thất bại với 200603061

function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
  external 'msvcrt.dll' name 'sprintf';

var
  SPrintfTarget: Pointer = @crt_sprintf;

// Không thể forward Varargs từ wrapper Pascal, nên symbol export sẽ
// tail-jump khi frame vẫn được giữ đúng như caller đã thiết lập
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

Project Free Pascal bây giờ cần làm khác gì

Không có gì ngoài tên unit trong uses clause, và giờ cũng không còn tệp nào cần triển khai. Backend tự đăng ký từ section initialization của nó thông qua RegisterJBIG2EncoderBackend, còn caller yêu cầu nó đúng như trước: qua option bit PDF_JBIG2_OPTION_EXTERNAL_ENCODER có giá trị 4, hoặc qua tham số UseExternalEncoder của các extended entry point. Việc yêu cầu vẫn chỉ là một preference chứ không phải bảo đảm, vì build bỏ sót unit sẽ âm thầm fallback sang encoder MMR Pascal native và tạo file lớn hơn thay vì báo lỗi

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder và Free Pascal từ 3.538.0

var
  Pdf: TPDFlib;
  Scan: TStream;
  ImageId: Integer;
begin
  Pdf := TPDFlib.Create(nil);
  try
    Pdf.NewDocument;
    Pdf.NewPage;
    Scan := TFileStream.Create('scan-page-1.tif', fmOpenRead);
    try
      // Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
      // BlackDotSize, LossyLevel
      ImageId := Pdf.AddImageJBIG2FromStreamEx(Scan, 0, 1, 1, 0, 0, 0);
    finally
      Scan.Free;
    end;
    if ImageId = 0 then
      raise Exception.Create('JBIG2 encoding failed');
    Pdf.SaveToFile('archive.pdf');
  finally
    Pdf.Free;
  end;
end;

Có hai giới hạn cần nói thẳng. Hiện chỉ có bộ object Win64, nên trên mọi target Free Pascal khác, external encode entry point sẽ báo lỗi và encoder Pascal native sẽ xử lý. Regression quyết định toàn bộ tính đáng tin cậy của tính năng này cũng là so sánh render chứ không phải kiểm tra kích thước: hai encoder đều lossless trên cùng nguồn, nên output được render rồi so sánh từng byte, và test suite Lazarus đã pass 26 trên 26 test, bao gồm test đó. So sánh kích thước compressed stream không có ý nghĩa, vì một trang bị lật ngược vẫn nén ra kích thước gần tương đương trang đúng

Bài học rộng hơn không chỉ áp dụng cho JBIG2. DLL là hình thức đúng khi boundary thực sự cần động, đúng như các surface tích hợp DLL, ActiveX và dylib phục vụ; nhưng DLL là hình thức sai khi chỉ là đường vòng cho COFF reader, vì nó thêm một tệp vào mọi installer, một search path vào mọi deployment và một kiểu lỗi lệch version mà static linking không thể có. Phần upstream cũng quan trọng, vì cách tạo ảnh hai mức quyết định kích thước cuối cùng nhiều hơn bản thân encoder, và render đơn sắc theo vùng trong Delphi bao quát nửa còn lại của pipeline. Phạm vi toolchain, bộ object theo từng compiler và các target được hỗ trợ đều có trên trang sản phẩm losLab PDF Developer Library