Technisch artikel

Free Pascal Win32: C-symbolen decoreren in HotPDF

Free Pascal zet op Win32 automatisch een voorloopunderscore bij elke cdecl; external-import, terwijl public name de string exporteert die u schreef, teken voor teken. HotPDF moet beide conventies in dezelfde source tree bedienen, want de Delphi-build levert al importdeclaraties mee waarin de underscore met de hand is uitgeschreven. Deze asymmetrie verkeerd hebben levert link-errors op die een symbool noemen dat niemand heeft geschreven

Een Delphi-library uitbreiden naar Free Pascal wordt meestal een portabiliteitsprobleem genoemd, en op Win64 is dat meestal ook zo. Win32 is anders. De 32-bit x86 Windows ABI draagt dertig jaar aan opgestapelde conventie over hoe C-symbolen gespeld worden, wie de stack opschoont, en welke compilerprivate helpers een translation unit mag aannemen, en elk van die punten is een plek waar twee Pascal-compilers die het over de taal eens zijn het toch oneens kunnen zijn over het objectbestand

Waarom resolved hetzelfde symbool op Win64 en faalt het op Win32?

Omdat het underscore-voorvoegsel een 32-bit conventie is die Free Pascal toepast op imports maar niet op exports. Declareert u function deflate(...): Integer; cdecl; external;, dan zoekt FPC op Win32 naar _deflate in het objectbestand en op Win64 naar deflate. Dat is correct gedrag en matcht wat een C-compiler uitzendt. De val zit aan de andere kant van de bridge: een routine gemarkeerd met public name 'deflate' exporteert op beide targets precies deflate, zonder toegevoegd voorvoegsel

Voeg daar het historische detail aan toe dat het concreet maakt. De Delphi-build declareert sommige van deze entry points al met de underscore in de naam verwerkt, want dat is wat zijn eigen objectbestanden bevatten. Geeft u dezelfde declaratie aan FPC op Win32, dan zet de compiler plichtsgetrouw nog een prefix ervoor, en jaagt de linker op __deflate, een symbool dat niets exporteert. De intuïtieve fix, overal één underscore toevoegen, breekt juist de imports die al correct gespeld waren

Wat werkt, is een paar prefix-constanten in plaats van één enkele. HPDFFPCZLib en HPDFFPCCodecStubs gebruiken één prefix voor gewone C-imports en een andere voor imports die al een Delphi-prefix meedragen, en op Win64 zijn beide constanten leeg zodat de bestaande linknamen onaangetast blijven. Twee constanten in plaats van één is de hele fix, en pas als u de importregel van de exportregel hebt gescheiden, is dat vanzelfsprekend

Dezelfde C-symbooldeclaraties resolved door Free Pascal en Delphi op Win64 en Win32: cdecl-imports krijgen alleen op het 32-bit target een underscore, een Delphi-declaratie die al met underscore gespeld is wordt __deflate en faalt bij het linken, terwijl public name-exports op beide architecturen letterlijk blijven
Eén prefix-constante kan niet beide regels dienen: gewone cdecl-imports en imports die al de Delphi-underscore dragen decoreren verschillend onder FPC op Win32, dus HotPDF houdt er twee bij en laat beide leeg op Win64
// Twee prefixes, niet één: gewone C-imports en imports die al een met de hand
// geschreven Delphi-prefix dragen, decoreren verschillend onder FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC voegt dit zelf toe bij cdecl external
  DelphiCName = '';    // in de source al met de underscore gespeld
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// Exportkant: 'public name' is letterlijk op elke target
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 vertelt u de architectuur, niet de ABI

Dit is de conditionele-compilatiefout met de langste debugstaart, en hij verdient het rondeuit gezegd te worden: WIN32 en WIN64 beschrijven de target-architectuur en zeggen niets over welke compilerprivate runtime helpers bestaan. Free Pascal definieert beide symbolen op de bijbehorende Windows-targets, precies zoals Delphi dat doet. Een guard geschreven als {$IFDEF WIN32} om code die een Delphi-runtime-helper aanroept, compileert dus gewoon onder FPC en faalt pas bij het linken

Concreet lopen drie families code in deze val. De Delphi 64-bit integer-trampolines die via de System.@_ll-helpers worden bereikt, de MSVC Win32 assembly support-routines, en de importslots die erbij horen bestaan allemaal om voorgecompileerde C-objecten te bedienen die de Delphi-build linkt. Free Pascal linkt die objecten niet, dus het heeft geen van die machinery nodig, en elke referentie ernaar moet verdwijnen. Het subtiele is dat declaratie en implementatie samen uitgesloten moeten worden. Sluit er maar één uit, dan rapporteert de compiler iets onbruikbaars over een identificator die hij nergens bij kan matchen

De regel die overblijft is kort. Guard op de compiler als de vraag over ABI of runtime support gaat, guard op de architectuur als de vraag over pointerbreedte of registeraantal gaat, en laat de ene nooit voor de andere doorgaan

Declaraties en implementaties samen guarden

Een conditioneel blok in de interface-section valt makkelijk in zonder dat u het merkt, en de foutmelding die eruit rolt wijst overal naartoe behalve naar de oorzaak. Voegt u een methoddeclaratie toe aan een class interface, dan is de logische plek naast de verwante methoden, en dat gaat goed tot het moment dat die buren toevallig binnen een bestaand {$IFDEF}-blok zitten. Conditionele directives worden niet ingesprongen, dus een blok dat veertig regels hoger opende, is in wezen onzichtbaar terwijl u de omliggende declaraties leest

Wat er daarna gebeurt, is een compile die op de ene toolchain slaagt en op de andere een lawine oplevert. Als de omliggende guard een Delphi-versiecheck is die Free Pascal niet haalt, verdwijnt de declaratie voor FPC terwijl de onconditionele implementatie blijft staan, en de compiler rapporteert een lange lijst klachten over method-identificatoren die hij verwachtte en niet vond. Geen enkele melding noemt het conditionele blok dat het veroorzaakte

Twee gewoontes voorkomen de hele faalcategorie. Kijk vóór het invoegen in een interface section omhoog naar de dichtstbijzijnde open conditional in plaats van op de visuele groepering te vertrouwen. En zie een groene Delphi-testsuite als bewijs over Delphi en niets anders: de Free Pascal library-build is een aparte gate, en de enige manier om te weten dat die haalt, is build-Win32-Lib-FPC.cmd en build-Win64-Lib-FPC.cmd draaien als deel van dezelfde wijziging

Wat er in 32-bit rekencode breekt

Eén taalbeperking duikt op in precies de code die het minst wil veranderen: 32-bit Free Pascal accepteert geen UInt64 als controlevariabele van een for-loop. In de elliptic-curve-units die X25519 en X448 dragen, zijn de loops die limb-arrays aflopen geschreven met 64-bit tellers, puur omdat de rest van de file 64-bit is

De fix moet chirurgisch zijn, want in veldrekenkunde is de breedte van een variabele deel van het correctheidsargument. Loop-indices worden Integer, want een limb-array heeft een handvol elementen en geen enkele index nadert ooit het 32-bit bereik. Alles wat aan de rekenkunde meedoet, de limbs zelf, de carry-propagatie en de masks, blijft UInt64, want een van die dingen versmallen verandert stilletjes het resultaat modulo het veldprimegetal

// 32-bit FPC weigert een UInt64-loopvariabele. Versmal alleen de index;
// limbs, masks en carries houden hun breedte, anders verandert de veldrekenkunde
var
  I: Integer;                 // was 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;

De verificatie voor zo'n wijziging kan geen round-trip-test zijn. Versleutelen en ontsleutelen met dezelfde kapotte implementatie bevestigt zichzelf feilloos, en daarom zijn known-answer vectors hier niet onderhandelbaar: draai de gepubliceerde X25519- en X448-testvectoren en vergelijk de exacte outputbytes. Dat is de enige check die een correcte implementatie onderscheidt van een zelfconsistente foute, en hij geldt evenzeer voor de symmetrische primitieven uit de Free Pascal deflate- en AES-codecgrenzen

De twee breekpunten van een HotPDF Win32 Free Pascal-build: een {$IFDEF WIN32}-guard om Delphi-runtime-helpers die compileert maar bij het linken faalt tenzij declaratie en implementatie samen worden uitgesloten, en de UInt64-loopvariabele in X25519- en X448-limb-loops versmald tot Integer terwijl limbs, carries en masks hun breedte houden
Guard op de compiler als de vraag over ABI of runtime support gaat en op de architectuur als het om pointerbreedte gaat, en bewijs rekenkundige wijzigingen vervolgens tegen gepubliceerde known-answer vectors in plaats van round-trip-tests

Wat een Win32 Free Pascal-build waard is

De praktische opbrengst is dat een Lazarus-applicatie die op 32-bit Windows mikt, dezelfde documentengine krijgt als zijn Delphi-tegenhanger, zonder een apart binair contract dat onderhouden moet worden. Dat telt het zwaarst voor de deployments waar mensen zelden over praten: industriële controllers, point-of-sale-terminals en langlevende line-of-business-software waarin de 32-bit runtime geen legacy-keuze is maar een hardwarebeperking

Het Win64-verhaal kwam eerst en staat in Free Pascal- en Lazarus-ondersteuning op Win64. Win32 is geen herhaling daarvan. Win64 heeft één calling convention, geen name decoration en geen Delphi-private integer-helpers om heen te werken, dus vrijwel alles in dit artikel is specifiek voor het 32-bit target. De rekenunits die de loopvariabele-wijziging nodig hadden, zijn dezelfde als in Montgomery-rekenkunde over de NIST-curves beschreven, waar de breedtediscipline dieper wordt uitgelegd

De algemene les is dat cross-compiler-portabiliteitswerk niet primair over taalfeatures gaat. Beide compilers accepteren hier dezelfde Object Pascal. Wat verschilt, is het objectbestand: hoe symbolen gespeld worden, welke helproutines van de runtime wordt aangenomen dat die bestaan, en welke voorgecompileerde objecten in de link zitten. HotPDF levert de Free Pascal- en Lazarus-pakketten naast de Delphi- en C++Builder-pakketten in de HotPDF Delphi PDF component, zodat dezelfde source tree elke toolchain voedt in plaats van per compiler te forken