مقاله فنی

لینک استاتیک jbig2enc در Free Pascal بدون DLL

PDFlibPas 3.538.0 رمزگذار خارجی JBIG2 را به‌صورت استاتیک داخل برنامه‌های Free Pascal و Lazarus لینک می‌کند. پروژه فقط unit PDFlibJBIG2EncC را اضافه می‌کند؛ همان unitی که Delphi و C++Builder از قبل استفاده می‌کنند، و رمزگذار داخل executable قرار می‌گیرد، بدون اینکه فایل دیگری برای توزیع لازم باشد. این نتیجه‌گیری قبلی درباره این قابلیت را تغییر می‌دهد که Free Pascal فقط از راه یک DLL می‌تواند به رمزگذار خارجی دسترسی پیدا کند

چرا DLL تنها گزینه به نظر می‌رسید؟

DLL تنها گزینه به نظر می‌رسید، چون سه مسیر لینک در سه نقطه مستقل شکست می‌خوردند و هیچ سوییچ کامپایلری به ریشه هیچ‌کدام نمی‌رسید. linker داخلی بخش‌های associative COMDAT را بی‌درنگ رد می‌کند. لینک خارجی با binutils همراه، درون section garbage collection از کار می‌افتد؛ قابلیتی که Free Pascal در هدف Windows 64-bit بدون شرط ارسال می‌کند. binutils جدیدتر اصلاً نمی‌تواند linker script مربوط به Free Pascal را پردازش کند. بازسازی بخش C++ با toolchain دیگر هم یک رد کردن را با رد کردن دیگری عوض می‌کند، چون template و inline instantiation ذاتاً weak external symbol تولید می‌کنند و Free Pascal آن‌ها را به‌صورت Unsupported COFF symbol type 105 گزارش می‌دهد. هیچ‌کدام از این شواهد اشتباه نبودند و شرح قبلی backendهای رمزگذار JBIG2 و linker فری پاسکال هر بن‌بست را به شکلی توضیح می‌دهد که هنوز هم قابل بازتولید است. اشتباه، فرض درباره محل قرار گرفتن راه‌حل بود. هر تلاش از compiler یا linker عبور می‌کرد، در حالی که هیچ‌کدام نمی‌توانند محتوای از قبل موجود در object file را عوض کنند. خود object file از ابتدا مشکل بود. ObjConv، COFF را می‌خواند و COFF می‌نویسد و هر سازه‌ای که Free Pascal با آن مشکل دارد، معادل مکانیکی قابل‌قبولی برای آن دارد

خطایی که هیچ‌وقت علتش را نام نمی‌برد

linker داخلی Free Pascal قابلیت pick-any COMDAT را فقط نصفه پیاده کرده و همین پیاده‌سازی ناقص سخت‌ترین بخش تشخیص این مشکل است. linker طبق هدف قالب، تعریف‌های تکراری را fold می‌کند. اما TExeOutput.RemoveUnreferencedSections هنگام علامت‌گذاری sectionهای استفاده‌شده، از طریق exesymbol به تعریف برنده redirect می‌شود، در حالی که TCoffexeoutput.DoRelocationFixup مستقیماً objreloc.symbol.objsection را می‌خواند. وقتی یک section استفاده‌شده به symbolی ارجاع می‌دهد که object خودش آن را در نسخه‌ای تعریف کرده که در fold بازنده شده است، دو pass به sectionهای متفاوت نگاه می‌کنند و لینک با Internal error 200603061 متوقف می‌شود

این وضعیت را با دو خطای دو طرفش مقایسه کنید. Unsupported COFF symbol type 105 یعنی weak external. Associative or exact match COMDAT sections are not yet supported یعنی associative COMDAT و حتی symbol متخلف را نام می‌برد. اما Internal error 200603061 هیچ چیز نمی‌گوید: نه نام symbol، نه نام section، نه نام file و نه مرحله اجرا. این خطا حالت معمول است، نه یک گوشه نادر، چون MSVC هر string literal و هر inline یا template instantiation را در یک pick-any COMDAT قرار می‌دهد و linker در مجموعه 186 object این رمزگذار، 2656 fold انجام داده است. ساختن با /Gy- تابع‌های عادی را از sectionهای COMDAT هر تابع بیرون می‌آورد، اما string literalها و template instantiationها را دقیقاً همان‌جا باقی می‌گذارد

چرا stub کردن symbolهای CRT همیشه طوری به نظر می‌رسد که آخرین مورد آن را خراب کرده است؟

چون linker فقط پس از resolve شدن همه symbolها به pass مربوط به fixup می‌رسد. تا وقتی چیزی missing باشد، اجرا زودتر با Undefined symbol تمام می‌شود و مشکل COMDAT فرصت ظاهر شدن پیدا نمی‌کند. وقتی آخرین stub مربوط به C runtime را اضافه می‌کنید، linker یک مرحله جلو می‌رود و مستقیم به Internal error 200603061 می‌رسد. بنابراین نشانه‌ای که در عمل می‌بینید به‌طور سیستماتیک گمراه‌کننده است: با افزودن bodyهای Pascal برای symbolهای C یکی‌یکی، همیشه این‌طور به نظر می‌رسد که آخرین مورد build را خراب کرده یا آستانه‌ای در حوالی صد stub رد شده است. هیچ‌کدام درست نیست. اینکه کدام symbol آخر اضافه شده و در مجموع چند symbol اضافه شده، بی‌اهمیت است، چون failure از اولین object پنهان بوده و فقط پس از موفق شدن resolution قابل دسترس شده است. وقتی linker بعد از رفع مشکلی نامرتبط، شکایتش را عوض می‌کند، بپرسید آیا فقط یک phase جلو رفته‌اید، نه اینکه regression ایجاد کرده باشید

راه‌حل یک pass از ObjConv است، نه یک compiler flag

کل اصلاح، یک فرمان post-processing است که روی هر object کامپایل‌شده اجرا می‌شود: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. سه گزینه از این فرمان برای همین کار اضافه شده‌اند. -xw symbolهای IMAGE_SYM_CLASS_WEAK_EXTERNAL را به externalهای معمولی تبدیل می‌کند. -xn symbolهای IMAGE_SYM_CLASS_NULL مانند _fltused را normalize می‌کند؛ همان‌هایی که Free Pascal به‌صورت Unsupported COFF symbol type 0 گزارش می‌دهد. -xc کار اصلی را انجام می‌دهد: هر COMDAT section را به section معمولی تنزل می‌دهد و symbolهایی را که تعریف می‌کند static می‌سازد. failure با حذف کردن خود تصمیم برطرف می‌شود، چون وقتی COMDAT sectionی وجود ندارد، نه foldی رخ می‌دهد، نه نسخه برنده‌ای هست که یک pass به آن redirect شود و pass دیگر آن را نبیند، و sectionهای unwind انجمنی .pdata و .xdata هم همراه آن حذف می‌شوند. هزینه واقعی اما اندک است: نسخه‌هایی که می‌توانستند به‌درستی merge شوند، حالا هرکدام باقی می‌مانند

تغییر prefix با -np:__imp_:pdflibimp_ یک collision جداگانه را حل می‌کند. MSVC، APIهای واردشده Win32 را از طریق سلول‌های indirection با نام __imp_* فراخوانی می‌کند، Free Pascal این prefix را برای import machinery خودش رزرو کرده و تعریف مستقیم یکی از این نام‌ها همان Internal error 200603061 را ایجاد می‌کند. تغییر نام سلول‌ها اجازه می‌دهد بخش Pascal آن‌ها را به‌صورت variableهای معمولی منتشر کند و در زمان اجرا مقداردهی کند. خود objectها با flag مربوط به static link، یعنی /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- کامپایل می‌شوند و codecهای تصویر نیز خاموش هستند، بنابراین مسیرهای dead مربوط به file-I/O و codec به stubهای بسیار کمتری برای لینک نیاز دارند. این objectها در Lib\thirdparty\Win64f قرار می‌گیرند، در حالی که مسیر Delphi و C++Builder همچنان مجموعه Win64x خودش را بدون تغییر لینک می‌کند؛ این نتیجه مناسبی برای اصلاح portability محدود به یک toolchain است

بخش Pascal هنوز باید چه چیزهایی را export کند؟

Free Pascal، import یک object C را با نام symbol resolve می‌کند و لازم است این نام صریحاً نوشته شود؛ بنابراین هر routine پاسکالی که جای یک C entry point می‌نشیند، clause صریح public name دارد. Delphi نام routine را همان نام symbol می‌گیرد و اصلاً به clause نیاز ندارد؛ به همین دلیل یک unit هر دو compiler را با clauseهای داخل {$IFDEF FPC} پوشش می‌دهد. دام اینجاست که اعلان external 'msvcrt.dll' هیچ چیزی را satisfy نمی‌کند: فقط یک import می‌سازد، نه تعریفی که object لینک‌شده بتواند به آن bind شود. body مربوط به forwarding باید واقعاً وجود داشته باشد

// اعلان خارجی فقط یک import ایجاد می‌کند و هیچ object لینک‌شده‌ای نمی‌تواند
// به آن bind شود
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// body پاسکالی که زیر نام دقیق C منتشر شده همان چیزی است که
// مجموعه object واقعاً به آن bind می‌شود
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

entry pointهای variadic این الگو را می‌شکنند، چون wrapper پاسکالی نمی‌تواند varargs خودش را به یک callee دیگر با varargs forward کند. راه‌حل این است که دیگر wrapper نباشید: یک routine بدون prologue را زیر نام C export کنید و با registerهای آرگومان و stack دقیقاً همان‌طور که caller آماده کرده، به implementation واقعی tail-jump بزنید. لایه JPEG 2000 از قبل snprintf و vsnprintf را به همین روش مدیریت می‌کند و به spellingهای msvcrt با underscore می‌پرد، چون نام‌های ساده فقط توسط UCRT export می‌شوند. یک محدودیت مرتبط از همان خطای داخلی می‌آید: سلول‌های import تغییرنام‌یافته از یک بخش initialization و با GetModuleHandleA و GetProcAddress پر می‌شوند، نه از static initializerها؛ زیرا گرفتن address یک routine واردشده داخل initializer باعث می‌شود compiler یک fixup غیرقابل‌پردازش تولید کند و دوباره با 200603061 شکست بخورد

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

var
  SPrintfTarget: Pointer = @crt_sprintf;

// varargs از wrapper پاسکالی قابل forward کردن نیست، پس symbol صادرشده
// با همان frameای که caller ساخته است tail-jump می‌کند
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

حالا یک پروژه Free Pascal چه تفاوتی دارد؟

هیچ تفاوتی جز نام unit در uses clause ندارد و دیگر فایلی برای deploy کردن وجود ندارد. backend از بخش initialization خودش و از طریق RegisterJBIG2EncoderBackend register می‌شود و callerها دقیقاً مثل قبل آن را درخواست می‌کنند: یا با bit گزینه PDF_JBIG2_OPTION_EXTERNAL_ENCODER که مقدارش 4 است، یا با آرگومان UseExternalEncoder در entry pointهای extended. درخواست این backend همچنان preference است، نه guarantee، چون buildی که unit را کنار گذاشته باشد بی‌سروصدا به encoder بومی Pascal از نوع MMR برمی‌گردد و به‌جای خطا فایل‌های بزرگ‌تری تولید می‌کند

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi، C++Builder و از 3.538.0، Free Pascal

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;

دو محدودیت را باید روشن گفت. فقط مجموعه object مربوط به Win64 وجود دارد، بنابراین در هر target دیگر Free Pascal، entry point مربوط به external encode failure گزارش می‌دهد و encoder بومی Pascal کار را انجام می‌دهد. همچنین regressionی که کل قابلیت را gate می‌کند مقایسه render است، نه بررسی اندازه: هر دو encoder روی یک source بدون افت هستند، پس خروجی آن‌ها render و byte به byte مقایسه می‌شود و suite مربوط به Lazarus هر 26 تست از 26 تست را، از جمله همین مورد، با موفقیت پشت سر گذاشته است. مقایسه اندازه streamهای فشرده چیزی را ثابت نمی‌کرد، چون یک صفحه وارونه تقریباً به اندازه یک صفحه درست فشرده می‌شود

درس کلی فراتر از JBIG2 هم کاربرد دارد. DLL وقتی شکل مناسبی است که boundary واقعاً dynamic باشد؛ همان حالتی که سطوح integration مربوط به DLL، ActiveX و dylib برای آن ساخته شده‌اند. اما اگر DLL فقط workaround برای COFF reader باشد، شکل مناسبی نیست، چون برای هر installer یک file، برای هر deployment یک search path و یک failure mode ناشی از version skew اضافه می‌کند که static linking اصلاً ندارد. upstream هم مهم است، چون نحوه تولید تصویر bilevel درباره اندازه نهایی بیشتر از خود encoder تعیین‌کننده است و rendering تک‌رنگ مبتنی بر region در Delphi نیمه دیگر pipeline را پوشش می‌دهد. پوشش toolchain، مجموعه objectهای هر compiler و targetهای پشتیبانی‌شده در صفحه محصول losLab PDF Developer Library فهرست شده است