Technischer Artikel

jbig2enc statisch in Free Pascal linken – ohne DLL

PDFlibPas 3.538.0 bindet den externen JBIG2-Encoder statisch in Free-Pascal- und Lazarus-Programme ein. Ein Projekt fügt die Unit PDFlibJBIG2EncC hinzu, dieselbe Unit, die Delphi und C++Builder bereits verwenden, und der Encoder landet direkt in der ausführbaren Datei, sodass außer ihr nichts Zusätzliches ausgeliefert werden muss. Damit ist die frühere Einschätzung zu diesem Feature überholt, nach der Free Pascal den externen Encoder nur über eine DLL erreichen konnte

Warum sah die DLL wie die einzige Option aus?

Die DLL wirkte wie die einzige Option, weil drei Linkwege auf drei voneinander unabhängige Arten scheiterten und kein Compiler-Schalter einen davon erreichte. Der interne Linker weist assoziative COMDAT-Sections direkt zurück. Ein externer Link mit den mitgelieferten Binutils stürzt bei der Bereinigung ungenutzter Sections ab, die Free Pascal auf dem 64-Bit-Windows-Ziel bedingungslos aktiviert. Neuere Binutils können das Free-Pascal-Linkerskript überhaupt nicht verarbeiten. Die C++-Seite mit der anderen Toolchain neu zu bauen, tauscht nur eine Zurückweisung gegen die nächste, weil Template- und Inline-Instanziierungen konstruktionsbedingt schwache externe Symbole erzeugen und Free Pascal diese als Unsupported COFF symbol type 105 meldet. Keine dieser Beobachtungen war falsch, und die frühere Darstellung der JBIG2-Encoder-Backends und des Free-Pascal-Linkers geht weiterhin reproduzierbar durch jede Sackgasse. Falsch war die Annahme, wo eine Lösung liegen konnte. Jeder Versuch führte über einen Compiler oder Linker, doch keiner von beiden kann ändern, was bereits in einer Objektdatei steht. Die Objektdatei war die ganze Zeit das Problem. ObjConv liest COFF und schreibt COFF, und jedes Konstrukt, an dem Free Pascal scheitert, hat ein mechanisches Gegenstück, das es akzeptiert

Der Fehler, der seine Ursache nie nennt

Der interne Free-Pascal-Linker implementiert Pick-any-COMDAT nur zur Hälfte, und genau diese Teilimplementierung ist hier am schwersten zu diagnostizieren. Doppelte Definitionen werden wie vom Format vorgesehen zusammengelegt. Aber TExeOutput.RemoveUnreferencedSections leitet beim Markieren verwendeter Sections über exesymbol auf die gewinnende Definition um, während TCoffexeoutput.DoRelocationFixup direkt objreloc.symbol.objsection liest. Wenn eine verwendete Section auf ein Symbol verweist, das sein eigenes Objekt in einer Kopie definiert, die den Fold verloren hat, betrachten die beiden Durchläufe unterschiedliche Sections, und der Link bricht mit Internal error 200603061 ab

Vergleichen Sie das mit den beiden Grenzen auf beiden Seiten. Unsupported COFF symbol type 105 bedeutet ein schwaches externes Symbol. Associative or exact match COMDAT sections are not yet supported bedeutet assoziatives COMDAT und nennt sogar das beanstandete Symbol. Der interne Fehler 200603061 sagt überhaupt nichts: keinen Symbolnamen, keinen Section-Namen, keinen Dateinamen, keine Phase. Er ist außerdem der Normalfall und kein Randfall, weil MSVC jedes Stringliteral sowie jede Inline- oder Template-Instanziierung in ein Pick-any-COMDAT legt und der Linker über die 186 Objekte dieses Encoders 2656 Folds ausgeführt hat. Ein Build mit /Gy- hält gewöhnliche Funktionen aus den Funktions-COMDAT-Sections heraus, lässt Stringliterale und Template-Instanziierungen aber genau dort, wo sie waren

Warum wirkt es immer so, als hätte der letzte CRT-Stub den Fehler ausgelöst?

Weil der Linker die Fixup-Phase erst erreicht, wenn jedes Symbol aufgelöst ist. Solange noch etwas fehlt, endet der Lauf früh mit Undefined symbol, und das COMDAT-Problem kann gar nicht sichtbar werden. Ergänzen Sie den letzten C-Runtime-Stub, rückt der Linker eine Phase weiter und läuft direkt in den internen Fehler 200603061. Das beobachtete Verhalten ist deshalb systematisch irreführend: Fügt man die Pascal-Implementierungen für die referenzierten C-Symbole einzeln hinzu, wirkt es immer so, als hätte die letzte Ergänzung den Build kaputtgemacht oder als sei eine Schwelle von ungefähr hundert Stubs überschritten worden. Beides stimmt nicht. Welches Symbol zuletzt hinzukam und wie viele insgesamt hinzukamen, ist irrelevant, denn der Fehler schlummerte schon im ersten Objekt und wurde erst erreichbar, als die Auflösung erfolgreich war. Wenn ein Linker nach der Behebung eines unabhängigen Problems eine andere Meldung ausgibt, fragen Sie, ob Sie nur eine Phase weitergekommen sind, statt eine Regression verursacht zu haben

Die Lösung ist ein ObjConv-Durchlauf, kein Compiler-Schalter

Die gesamte Korrektur ist ein einzelner Nachbearbeitungsbefehl für jedes kompilierte Objekt: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Drei dieser Optionen wurden für diese Arbeit ergänzt. -xw löst Symbole vom Typ IMAGE_SYM_CLASS_WEAK_EXTERNAL in gewöhnliche externe Symbole auf. -xn normalisiert Symbole vom Typ IMAGE_SYM_CLASS_NULL wie _fltused, die Free Pascal als Unsupported COFF symbol type 0 meldet. -xc erledigt den Hauptteil: Es stuft jede COMDAT-Section zu einer gewöhnlichen Section herab und macht die von ihr definierten Symbole statisch. Der Fehler verschwindet, weil die Entscheidung verschwindet: Ohne COMDAT-Sections gibt es kein Folding, keine gewinnende Kopie, auf die ein Durchlauf umleiten kann, während der andere sie verfehlt, und die assoziativen .pdata- und .xdata-Unwind-Sections verschwinden ebenfalls. Der Preis ist real, aber gering: Kopien, die sich rechtmäßig hätten zusammenlegen lassen, bleiben nun jeweils erhalten

Die Umbenennung mit dem Präfix -np:__imp_:pdflibimp_ löst einen separaten Konflikt. MSVC ruft importierte Win32-APIs über Indirection-Zellen mit Namen __imp_* auf, Free Pascal reserviert dieses Präfix für seine eigene Importmechanik, und die direkte Definition eines solchen Namens löst denselben internen Fehler 200603061 aus. Durch die Umbenennung kann die Pascal-Seite die Zellen als gewöhnliche Variablen veröffentlichen und zur Laufzeit befüllen. Die Objekte selbst werden mit gesetztem Static-Link-Flag /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- und deaktivierten Bild-Codecs kompiliert, sodass die toten Datei-I/O- und Codec-Pfade deutlich weniger reine Link-Stubs benötigen. Sie landen in Lib\thirdparty\Win64f, während der Delphi- und C++Builder-Weg weiterhin unverändert sein eigenes Win64x-Set linkt. Das ist das richtige Ergebnis für eine Portabilitätskorrektur, die auf eine Toolchain begrenzt bleibt

Was die Pascal-Seite weiterhin exportieren muss

Free Pascal löst den Import eines C-Objekts über den Symbolnamen auf, daher muss jede Pascal-Routine, die für einen C-Einstiegspunkt einspringt, eine ausdrückliche public name-Klausel tragen. Delphi verwendet den Routinenamen als Symbolnamen und benötigt keine Klausel, weshalb eine Unit beide Compiler mit den Klauseln unter {$IFDEF FPC} bedient. Die Falle ist, dass eine Deklaration external 'msvcrt.dll' nichts erfüllt: Sie erzeugt einen Import, aber keine Definition, an die sich ein gelinktes Objekt binden kann. Der Weiterleitungskörper muss tatsächlich existieren

// Eine externe Deklaration erzeugt nur einen Import. Kein gelinktes Objekt
// kann daran gebunden werden.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// Ein Pascal-Körper, der unter dem exakten C-Symbolnamen veröffentlicht wird,
// ist das, woran sich die Objekte tatsächlich binden.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

Variadische Einstiegspunkte durchbrechen dieses Muster, weil ein Pascal-Wrapper seine eigenen Varargs nicht an einen anderen Varargs-Callee weiterreichen kann. Der Ausweg besteht darin, kein Wrapper mehr zu sein: Exportieren Sie eine nackte Routine unter dem C-Namen und springen Sie mit einem Tail-Jump zur echten Implementierung, wobei Argumentregister und Stack exakt so bleiben, wie der Aufrufer sie eingerichtet hat. Die JPEG-2000-Schicht behandelt snprintf und vsnprintf bereits auf diese Weise und springt zu den unterstrichenen msvcrt-Namen, weil die einfachen Namen nur von der UCRT exportiert werden. Eine verwandte Einschränkung stammt aus demselben internen Fehler: Die umbenannten Importzellen werden aus einem initialization-Abschnitt über GetModuleHandleA und GetProcAddress befüllt und nicht aus statischen Initialisierern, denn die Adressnahme einer importierten Routine in einem Initialisierer lässt den Compiler einen Fixup erzeugen, den er nicht verarbeiten kann, und löst erneut 200603061 aus

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

var
  SPrintfTarget: Pointer = @crt_sprintf;

// Varargs können nicht aus einem Pascal-Wrapper weitergereicht werden, daher
// springt das exportierte Symbol mit unverändertem Aufrufer-Frame weiter.
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

Was ein Free-Pascal-Projekt jetzt anders macht

Nichts außer dem Unit-Namen in der Uses-Klausel, und es gibt keine Datei mehr bereitzustellen. Das Backend registriert sich über seinen eigenen Initialisierungsabschnitt mit RegisterJBIG2EncoderBackend, und Aufrufer fordern es wie bisher an: über das Optionsbit PDF_JBIG2_OPTION_EXTERNAL_ENCODER mit dem Wert 4 oder über das Argument UseExternalEncoder der erweiterten Einstiegspunkte. Die Anforderung bleibt eine Präferenz und keine Garantie, denn ein Build, in dem die Unit fehlt, fällt still auf den nativen Pascal-MMR-Encoder zurück und erzeugt größere Dateien statt eines Fehlers

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder und ab 3.538.0 auch 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;

Zwei Grenzen sollte man klar benennen. Es gibt nur ein Win64-Objektset, daher meldet der externe Encode-Einstiegspunkt auf jedem anderen Free-Pascal-Ziel einen Fehler und der native Pascal-Encoder übernimmt die Arbeit. Die Regression, die all dies absichert, ist außerdem ein Rendervergleich und kein Größenvergleich: Beide Encoder sind für dieselbe Quelle verlustfrei, daher wird ihre Ausgabe gerendert und Byte für Byte verglichen; die Lazarus-Suite besteht alle 26 von 26 Tests einschließlich dieses Tests. Der Vergleich komprimierter Streamgrößen hätte nichts bewiesen, denn eine invertierte Seite komprimiert ungefähr so groß wie eine korrekte

Die weitergehende Lehre gilt über JBIG2 hinaus. Eine DLL ist die richtige Form, wenn die Grenze wirklich dynamisch ist, wie bei den DLL-, ActiveX- und dylib-Integrationsflächen; sie ist die falsche Form, wenn sie nur als Workaround für einen COFF-Leser dient, denn dann kommen eine Datei in jeden Installer, ein Suchpfad in jede Bereitstellung und ein Versionskonflikt hinzu, den statisches Linken nicht haben kann. Auch der vorgelagerte Schritt zählt: Die Erzeugung des Zweistufenbilds entscheidet mehr über die endgültige Größe als der Encoder, und die regionsbasierte monochrome Darstellung in Delphi behandelt diese Hälfte der Pipeline. Toolchain-Abdeckung, die Objektsets je Compiler und die unterstützten Ziele sind auf der losLab-Produktseite der PDF Developer Library aufgeführt