Техническа статия

Статично свързване на jbig2enc с Free Pascal без DLL

PDFlibPas 3.538.0 свързва външния JBIG2 encoder статично в програми за Free Pascal и Lazarus. Проектът добавя unit-а PDFlibJBIG2EncC, същия unit, който вече използват Delphi и C++Builder, и encoder-ът се оказва вътре в изпълнимия файл, без нищо допълнително за доставяне. Това обръща предишния извод за тази функционалност, според който Free Pascal може да достигне външния encoder само през DLL

Защо DLL изглеждаше като единствената възможност?

DLL изглеждаше като единствената възможност, защото три маршрута за свързване се проваляха по три несвързани начина и нито един compiler switch не достигаше до тях. Вътрешният linker отхвърля асоциативните COMDAT секции още в началото. Външното свързване през включените binutils се срива в garbage collection на секции, която Free Pascal включва безусловно за 64-bit Windows target-а. По-новите binutils изобщо не могат да обработят link script-а на Free Pascal. Прекомпилирането на C++ страната с другия toolchain заменя единия отказ с друг, защото template и inline инстанцирането по замисъл излъчва weak external symbols, а Free Pascal ги докладва като Unsupported COFF symbol type 105. Нито едно от тези наблюдения не беше грешно и предишният разказ за JBIG2 encoder backend-ите и Free Pascal linker-а проследява всеки dead end по начин, който и днес се възпроизвежда. Грешно беше предположението къде може да живее поправката. Всеки опит минаваше през compiler или linker, а нито един от тях не може да промени съдържанието на вече готов object file. Object file-ът е бил проблемът през цялото време. ObjConv чете COFF и записва COFF, а всяка конструкция, на която Free Pascal се задавя, има механичен еквивалент, който приема

Грешката, която никога не назовава причината си

Вътрешният linker на Free Pascal реализира pick-any COMDAT само наполовина и точно тази половинчата реализация е най-трудната за диагностициране част тук. Той наистина слива дублираните дефиниции, както изисква форматът. Но TExeOutput.RemoveUnreferencedSections минава през exesymbol към печелившата дефиниция, когато маркира секции като използвани, докато TCoffexeoutput.DoRelocationFixup чете директно objreloc.symbol.objsection. Когато използвана секция препраща към символ, който нейният object дефинира в копие, загубило сливането, двата прохода гледат различни секции и link-ът спира с Internal error 200603061

Сравнете това с двете ограничения от двете му страни. Unsupported COFF symbol type 105 означава weak external. Associative or exact match COMDAT sections are not yet supported означава associative COMDAT и дори назовава проблемния символ. Internal error 200603061 не казва нищо: нито име на символ, нито име на секция, нито име на файл, нито етап. Това е и нормалният случай, а не крайна особеност, защото MSVC поставя всеки string literal и всяка inline или template инстанциация в pick-any COMDAT, а сред 186-те object-а в този encoder linker-ът е изпълнил 2656 сливаня. Компилирането с /Gy- държи обикновените функции извън COMDAT секциите за отделни функции, но оставя string literal-ите и template инстанциациите точно там, където са били

Защо stub-овете за CRT символи винаги изглеждат като последната промяна, която е счупила всичко?

Защото linker-ът достига до fixup прохода едва след като всички символи са разрешени. Докато нещо още липсва, изпълнението приключва по-рано с Undefined symbol и COMDAT проблемът изобщо не получава шанс да се покаже. Добавете последния C runtime stub и linker-ът преминава един етап напред, право към Internal error 200603061. Симптомът на практика подвежда систематично: когато добавяте Pascal тела за реферираните C символи едно по едно, винаги изглежда, че последното добавяне е счупило build-а или че е преминат някакъв праг около сто stub-а. Нито едното не е вярно. Кой символ е добавен последен и колко са добавени общо са без значение, защото failure-ът е бил скрит още в първия object и е станал достижим едва след успешното разрешаване. Когато linker промени оплакването си, след като сте поправили нещо несвързано, попитайте дали сте преминали към следващ етап, а не дали сте причинили regression

Поправката е един ObjConv pass, не compiler flag

Цялата корекция е една post-processing команда, изпълнена върху всеки компилиран object: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Три от тези опции са добавени за тази работа. -xw превръща символите IMAGE_SYM_CLASS_WEAK_EXTERNAL в обикновени externals. -xn нормализира символите IMAGE_SYM_CLASS_NULL, например _fltused, които Free Pascal докладва като Unsupported COFF symbol type 0. -xc върши тежката работа: понижава всяка COMDAT секция до обикновена секция и прави символите, които тя дефинира, static. Така failure-ът изчезва, защото изчезва самото решение: без COMDAT секции няма сливане, няма печелившо копие, към което единият проход да пренасочи, а другият да не намери, и асоциативните .pdata и .xdata unwind секции изчезват заедно с него. Цената е реална, но малка: копията, които легитимно са могли да се слеят, вече оцеляват поотделно

Преименуването с префикса -np:__imp_:pdflibimp_ решава отделен collision. MSVC извиква импортирани Win32 API през indirection cells с имена __imp_*, Free Pascal запазва този prefix за собствената си import machinery, а директното дефиниране на едно от тези имена предизвиква същия Internal error 200603061. Преименуването на cell-овете позволява Pascal страната да ги публикува като обикновени променливи и да ги попълни по време на изпълнение. Самите object-и са компилирани със static-link flag-а /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- и с изключени image codec-и, така че мъртвите file-I/O и codec пътища се нуждаят от много по-малко link-only stub-ове. Те попадат в Lib\thirdparty\Win64f, докато маршрутът за Delphi и C++Builder продължава да свързва собствения си набор Win64x без промяна, което е правилният резултат за portability fix, ограничен до един toolchain

Какво още трябва да export-ва Pascal страната

Free Pascal разрешава import-а на C object по име на символа и това име трябва да бъде изписано изрично, затова всяка Pascal routine, която замества C entry point, носи явна клауза public name. Delphi приема името на routine-а за име на символ и не се нуждае от клауза, поради което един unit служи и на двата compiler-а с клаузите под {$IFDEF FPC}. Капанът е, че декларация external 'msvcrt.dll' не удовлетворява нищо: тя създава import, но никога definition, към която свързан object да може да се върже. Forwarding body-то трябва да съществува

// Външна декларация създава само import. Няма свързан object, който
// може да се върже към нея.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// Pascal тяло, публикувано под точното C име, е това, към което
// реалният набор от object-и се свързва.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

Variadic entry point-ите нарушават този модел, защото Pascal wrapper не може да препрати собствените си varargs към друг varargs callee. Изходът е да спрете да бъдете wrapper: export-вайте naked routine под C името и направете tail-jump към истинската implementation с argument register-ите и stack-а точно така, както caller-ът ги е подготвил. JPEG 2000 layer-ът вече обработва snprintf и vsnprintf по този начин, като скача към spellings на msvcrt с underscore prefix, защото обикновените имена се export-ват само от UCRT. Едно свързано ограничение идва от същия Internal error: преименуваните import cell-ове се попълват от initialization section чрез GetModuleHandleA и GetProcAddress, а не от static initializer-и, тъй като вземането на адреса на import-ната 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 не могат да се препратят от Pascal wrapper, затова export-натият
// symbol прави tail-jump с frame-а точно както caller-ът го е подготвил.
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

Какво прави различно един проект за Free Pascal сега

Нищо освен името на unit-а в uses clause-а и вече няма файл за deployment. Backend-ът се регистрира от собствения си initialization section чрез RegisterJBIG2EncoderBackend, а caller-ите го заявяват точно както преди: през options bit PDF_JBIG2_OPTION_EXTERNAL_ENCODER, чиято стойност е 4, или през аргумента UseExternalEncoder на разширените entry point-и. Заявката остава предпочитание, а не гаранция, защото build, който пропусне unit-а, тихо се връща към native Pascal MMR encoder и произвежда по-големи файлове вместо грешка

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;

Две ограничения си струва да бъдат казани ясно. Съществува само Win64 object set, затова на всеки друг Free Pascal target external encode entry point-ът докладва failure и native Pascal encoder-ът поема работата. А regression-ът, който управлява всичко това, е сравнение на render, а не проверка на размера: и двата encoder-а са lossless върху един и същ source, затова output-ът се render-ва и сравнява byte по byte, като Lazarus suite-ът минава 26 от 26, включително този test. Сравняването на размерите на compressed stream-овете не би доказало нищо, защото обърната страница се компресира приблизително до размера на правилната

По-широкият извод се пренася отвъд JBIG2. DLL е правилната форма, когато границата наистина е динамична, какъвто е случаят, за който съществуват DLL, ActiveX и dylib integration surface-ите; тя е грешната форма, когато е само workaround за COFF reader, защото добавя файл към всеки installer, search path към всеки deployment и version-skew failure mode, който static linking не може да има. Входът също има значение, тъй като начинът, по който се произвежда bilevel image-ът, определя крайните размери повече от encoder-а, а region-based monochrome rendering в Delphi покрива тази половина от pipeline-а. Toolchain coverage, object set-овете за отделните compiler-и и supported target-ите са изброени на продуктовата страница на losLab PDF Developer Library