Technischer Artikel

Free Pascal Win32: C-Symbol-Dekoration in HotPDF

Free Pascal stellt unter Win32 jedem cdecl; external-Import automatisch einen führenden Unterstrich voran, während public name die geschriebene Zeichenkette Zeichen für Zeichen exportiert. HotPDF muss beiden Konventionen im selben Quellbaum genügen, denn der Delphi-Build liefert bereits Importdeklarationen mit, in denen der Unterstrich von Hand ausgeschrieben ist. Wer diese Asymmetrie vermasselt, erhält Link-Fehler, die ein Symbol nennen, das niemand geschrieben hat

Eine Delphi-Bibliothek auf Free Pascal auszuweiten wird meist als Portabilitätsproblem beschrieben, und auf Win64 ist es das auch meistens. Win32 ist anders. Die 32-Bit-x86-Windows-ABI trägt dreißig Jahre angesammelter Konventionen darüber, wie C-Symbole geschrieben werden, wer den Stack aufräumt und welche compiler-privaten Helfer eine Übersetzungseinheit voraussetzen darf — und jede davon ist eine Stelle, an der zwei Pascal-Compiler, die sich über die Sprache einig sind, über die Objektdatei trotzdem streiten können

Warum löst dasselbe Symbol auf Win64 auf und scheitert auf Win32?

Weil der Unterstrich-Präfix eine 32-Bit-Konvention ist, die Free Pascal auf Imports anwendet, nicht auf Exports. Deklarieren Sie function deflate(...): Integer; cdecl; external;, sucht FPC auf Win32 nach _deflate in der Objektdatei und auf Win64 nach deflate. Das ist korrektes Verhalten und entspricht dem, was ein C-Compiler erzeugt. Die Falle sitzt auf der anderen Seite der Brücke: Eine Routine mit public name 'deflate' exportiert auf beiden Zielen exakt deflate, ohne hinzugefügten Präfix

Jetzt kommt das historische Detail, das es konkret macht. Der Delphi-Build deklariert einige dieser Einsprungpunkte bereits mit in den Namen geschriebenem Unterstrich, weil genau das in seinen eigenen Objektdateien steckt. Geben Sie dieselbe Deklaration FPC auf Win32, stellt der Compiler den Präfix pflichtschuldig erneut voran, also sucht der Linker nach __deflate, einem Symbol, das nichts exportiert. Der naheliegende Fix — überall einen Unterstrich ergänzen — bricht die Imports, die bereits korrekt geschrieben waren

Was funktioniert, ist ein Paar von Präfix-Konstanten statt einer einzigen. HPDFFPCZLib und HPDFFPCCodecStubs benutzen einen Präfix für schlichte C-Imports und einen anderen für Imports, die bereits einen Delphi-seitigen Präfix tragen, und auf Win64 sind beide Konstanten leer, damit die bestehenden Link-Namen unangetastet bleiben. Zwei Konstanten statt einer sind die gesamte Korrektur, und sie ist erst offensichtlich, wenn man die Import-Regel von der Export-Regel getrennt hat

Dieselben C-Symboldeklarationen, aufgelöst von Free Pascal und Delphi auf Win64 und Win32: cdecl-Imports bekommen nur auf dem 32-Bit-Ziel einen Unterstrich, eine bereits mit Unterstrich geschriebene Delphi-Deklaration wird zu __deflate und scheitert beim Linken, während public name-Exports auf beiden Architekturen wörtlich bleiben
Eine Präfix-Konstante kann nicht beiden Regeln dienen: Schlichte cdecl-Imports und Imports, die bereits den Delphi-Unterstrich tragen, dekorieren unter FPC auf Win32 unterschiedlich, also hält HotPDF zwei und lässt beide auf Win64 leer
// Zwei Präfixe, nicht einer: schlichte C-Imports und Imports, die schon
// einen handgeschriebenen Delphi-Präfix tragen, dekorieren unter FPC/Win32 unterschiedlich
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC fügt das selbst für cdecl external hinzu
  DelphiCName = '';    // im Quelltext bereits mit Unterstrich geschrieben
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// Exportseite: public name ist auf jedem Ziel wörtlich
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 verrät die Architektur, nicht die ABI

Das ist der Fehler bei bedingter Kompilierung mit dem längsten Debug-Nachlauf, und er verdient eine klare Aussage: WIN32 und WIN64 beschreiben die Zielarchitektur und sagen nichts darüber, welche compiler-privaten Runtime-Helfer existieren. Free Pascal definiert beide Symbole auf den jeweiligen Windows-Zielen, genau wie Delphi. Eine als {$IFDEF WIN32} geschriebene Guard um Code, der einen Delphi-Runtime-Helfer aufruft, kompiliert unter FPC also und scheitert erst zur Linkzeit

Konkret geraten drei Familien von Code in diese Falle. Die Delphi-64-Bit-Integer-Trampolines, die über die System.@_ll-Helfer erreichbar sind, die MSVC-Win32-Assembler-Unterstützungsroutinen und die zugehörigen Import-Slots existieren alle, um vorkompilierte C-Objekte zu bedienen, die der Delphi-Build linkt. Free Pascal linkt diese Objekte nicht, braucht also keine dieser Mechanik, und jede Referenz darauf muss verschwinden. Der feine Unterschied: Deklaration und Implementierung müssen zusammen ausgeschlossen werden. Schließt man nur eines aus, meldet der Compiler etwas Unhilfreiches über einen Bezeichner, den er nichts zuordnen kann

Die Regel, die daraus fällt, ist kurz: Auf den Compiler guarden, wenn die Frage ABI oder Runtime-Unterstützung betrifft, auf die Architektur guarden, wenn sie Zeigerbreite oder Registerzahl betrifft — und niemals das eine für das andere stehen lassen

Deklarationen und Implementierungen zusammen guarden

Ein Conditional-Block im Interface-Abschnitt ist leicht unbemerkt zu erwischen, und die entstehende Fehlermeldung zeigt überallhin, nur nicht auf die Ursache. Fügt man eine Methodendeklaration zu einem Klasseninterface hinzu, ist der naheliegende Platz neben den verwandten Methoden — solange völlig in Ordnung, bis diese Nachbarn zufällig in einem bestehenden {$IFDEF}-Block sitzen. Conditional-Direktiven werden nicht eingerückt, also ist ein Block, der sich vierzig Zeilen weiter oben öffnete, beim Lesen der umliegenden Deklarationen praktisch unsichtbar

Was danach passiert, ist eine Kompilierung, die auf einer Toolchain durchläuft und auf einer anderen eine Kaskade erzeugt. Ist die umgebende Guard eine Delphi-Versionsprüfung, die Free Pascal nicht erfüllt, verschwindet die Deklaration für FPC, während die unbedingte Implementierung bleibt, und der Compiler meldet eine lange Liste von Beschwerden über Methodenbezeichner, die er erwartete und nicht fand. Keine der Meldungen erwähnt den Conditional-Block, der das verursacht hat

Zwei Gewohnheiten verhindern diese ganze Fehlerklasse. Vor dem Einfügen in einen Interface-Abschnitt nach oben zum nächsten offenen Conditional schauen, statt auf die visuelle Gruppierung zu vertrauen. Und eine grüne Delphi-Testsuite nur als Beleg für Delphi behandeln: Der Free-Pascal-Bibliotheksbuild ist eine eigene Schranke, und die einzige Art zu wissen, dass er durchläuft, ist build-Win32-Lib-FPC.cmd und build-Win64-Lib-FPC.cmd als Teil derselben Änderung laufen zu lassen

Was in 32-Bit-Arithmetikcode bricht

Eine Sprachbeschränkung taucht exakt in dem Code auf, der sich am wenigsten ändern lassen will: 32-Bit-Free-Pascal akzeptiert kein UInt64 als for-Schleifenkontrollvariable. In den elliptischen-Kurven-Units mit X25519 und X448 waren die Schleifen, die Limb-Arrays durchlaufen, schlicht mit 64-Bit-Zählern geschrieben, weil alles andere in der Datei 64 Bit ist

Der Fix muss chirurgisch sein, denn in der Körperarithmetik ist die Breite einer Variablen Teil des Korrektheitsarguments. Schleifenindizes werden zu Integer, da ein Limb-Array eine Handvoll Elemente hat und kein Index je an den 32-Bit-Bereich herankommt. Alles, was an der Arithmetik teilnimmt — die Limbs selbst, die Carry-Propagation und die Masken — bleibt UInt64, denn eines davon zu verschmälern ändert das Ergebnis modulo der Körperprimzahl lautlos

// 32-Bit-FPC lehnt eine UInt64-Schleifenvariable ab. Nur den Index verschmälern;
// Limbs, Masken und Carries behalten ihre Breite, sonst ändert sich die Körper-Mathematik
var
  I: Integer;                 // war UInt64
  Carry, Mask: UInt64;
begin
  Carry := 0;
  for I := 0 to High(Limbs) do
  begin
    Limbs[I] := Limbs[I] + Carry;
    Carry := Limbs[I] shr 51;
    Limbs[I] := Limbs[I] and Mask;
  end;
end;

Die Verifikation für so eine Änderung kann kein Round-Trip-Test sein. Verschlüsseln und Entschlüsseln mit derselben kaputten Implementierung stimmt perfekt mit sich selbst überein, deshalb sind Known-Answer-Vektoren hier nicht verhandelbar: die veröffentlichten X25519- und X448-Testvektoren laufen lassen und die exakten Ausgabe-Bytes vergleichen. Das ist die einzige Prüfung, die eine korrekte Implementierung von einer selbstkonsistent falschen unterscheidet, und sie gilt gleichermaßen für die symmetrischen Primitive aus den Free-Pascal-deflate- und AES-Codec-Grenzen

Die zwei Bruchstellen eines HotPDF-Win32-Free-Pascal-Builds: eine {$IFDEF WIN32}-Guard um Delphi-Runtime-Helfer, die kompiliert, aber beim Linken scheitert, solange nicht Deklaration und Implementierung zusammen ausgeschlossen sind, und die UInt64-Schleifenvariable in den X25519- und X448-Limb-Durchläufen, verschmälert auf Integer, während Limbs, Carries und Masken ihre Breite behalten
Auf den Compiler guarden, wenn die Frage ABI oder Runtime-Unterstützung ist, und auf die Architektur, wenn es Zeigerbreite ist; Arithmetikänderungen dann an veröffentlichten Known-Answer-Vektoren beweisen statt an Round-Trip-Tests

Was ein Win32-Free-Pascal-Build wert ist

Der praktische Ertrag: Eine Lazarus-Anwendung für 32-Bit-Windows bekommt dieselbe Dokumenten-Engine wie ihr Delphi-Pendant, ohne einen separaten Binärvertrag zu pflegen. Das zählt am meisten bei den Deployments, über die selten gesprochen wird: Industrie-Controller, Point-of-Sale-Terminals und langlebige Fachanwendungen, wo die 32-Bit-Runtime keine Legacy-Entscheidung ist, sondern eine Hardware-Beschränkung

Die Win64-Geschichte kam zuerst und ist in Free-Pascal- und Lazarus-Unterstützung auf Win64 beschrieben. Win32 ist keine Wiederholung davon. Win64 hat eine Aufrufkonvention, keine Namensdekoration und keine Delphi-privaten Integer-Helfer, um die man herumarbeiten müsste, also ist fast alles in diesem Artikel spezifisch für das 32-Bit-Ziel. Die Arithmetik-Units, die die Schleifenvariablen-Änderung brauchten, sind dieselben wie in Montgomery-Arithmetik über die NIST-Kurven beschrieben, wo die Breitendisziplin tiefer erklärt wird

Die allgemeine Lektion: Cross-Compiler-Portabilitätsarbeit dreht sich nicht in erster Linie um Sprachfeatures. Beide Compiler akzeptieren hier dasselbe Object Pascal. Anders ist die Objektdatei: wie Symbole geschrieben werden, welche Helferroutinen die Runtime bereitstellen soll und welche vorkompilierten Objekte im Link stecken. HotPDF liefert die Free-Pascal- und Lazarus-Pakete neben den Delphi- und C++Builder-Paketen in der HotPDF Delphi PDF component aus, sodass derselbe Quellbaum jede Toolchain speist, statt pro Compiler einen Fork zu fahren