Teknisk artikel

Free Pascal Win32: C-symboldecoration i HotPDF

Free Pascal på Win32 tilføjer automatisk et indledende underscore til hver cdecl; external-import, mens public name eksporterer strengen, du skrev, tegn for tegn. HotPDF skal tilfredsstille begge konventioner i det samme source tree, for Delphi-builden shipper allerede importdeklarationer, der staver underscoren ud i hånden. Tager man fejl af denne asymmetri, får man link-fejl, der nævner et symbol, ingen har skrevet

Udvidelse af et Delphi-bibliotek til Free Pascal beskrives normalt som et portabilitetsproblem, og på Win64 er det for det meste dét. Win32 er anderledes. 32-bit x86 Windows ABI bærer tredive års akkumuleret konvention om, hvordan C-symboler staves, hvem der rydder stakken, og hvilke compiler-private hjælpere en translation unit må antage findes, og hver af dem er et sted, hvor to Pascal-compilere, der er enige om sproget, stadig kan være uenige om objektfilen

Hvorfor resolver samme symbol på Win64 og fejler på Win32?

Fordi underscore-præfikset er en 32-bit konvention, som Free Pascal anvender på imports, men ikke på exports. Deklarerer man function deflate(...): Integer; cdecl; external;, leder FPC efter _deflate i objektfilen på Win32 og efter deflate på Win64. Det er korrekt adfærd og matcher, hvad en C-compiler genererer. Fælden er på den anden side af broen: en rutine markeret public name 'deflate' eksporterer præcis deflate på begge targets uden noget præfiks tilføjet

Læg nu den historiske detalje til, der gør det konkret. Delphi-builden deklarerer allerede nogle af disse entry points med underscore skrevet ind i navnet, for det er, hvad dens egne objektfiler indeholder. Giver man samme deklaration til FPC på Win32, sætter compileren pligtopfyldende præfikset på igen, så linkeren jager __deflate, et symbol, intet eksporterer. Den intuitive fix, at tilføje én underscore overalt, bryder imports, der allerede var stavet korrekt

Det, der virker, er et par præfiks-konstanter frem for én enkelt. HPDFFPCZLib og HPDFFPCCodecStubs bruger ét præfiks til rene C-imports og et andet til imports, der allerede bærer et Delphi-sidepræfiks, og på Win64 er begge konstanter tomme, så de eksisterende link-navne overlever urørte. To konstanter i stedet for én er hele fixet, og det er kun åbenlyst, når du først har adskilt importreglen fra eksportreglen

Samme C-symboldeklarationer resolveret af Free Pascal og Delphi på Win64 og Win32: cdecl-importer får en underscore kun på 32-bit-target, en Delphi-deklaration allerede stavet med underscore bliver __deflate og fejler ved linking, mens public name-exports forbliver bogstavelige på begge arkitekturer
Én præfiks-konstant kan ikke tjene begge regler: rene cdecl-imports og imports, der allerede bærer Delphi-underscore, dekorerer forskelligt under FPC på Win32, så HotPDF holder to og lader begge stå tomme på Win64
// To præfikser, ikke ét: rene C-imports og imports, der allerede bærer
// et håndskrevet Delphi-præfiks, dekorerer forskelligt under FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC tilføjer denne selv ved cdecl external
  DelphiCName = '';    // allerede stavet med underscore i source
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// Eksportsiden: 'public name' er bogstavelig på alle targets
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 fortæller dig arkitekturen, ikke ABI

Dette er betinget kompilering-fejlen med den længste debugging-hale, og det er værd at sige klart ud: WIN32 og WIN64 beskriver target-arkitekturen og siger intet om, hvilke compiler-private runtime-hjælpere der findes. Free Pascal definerer begge symboler på de tilsvarende Windows-targets, præcis som Delphi gør. En guard skrevet som {$IFDEF WIN32} omkring kode, der kalder en Delphi-runtime-hjælper, kompilerer derfor under FPC og fejler ved link-tid

Konkret falder tre familier af kode i denne fælde. Delphis 64-bit integer-trampoliner nået gennem System.@_ll-hjælpere, MSVC Win32 assembly support-rutinerne og de importslots, der følger med, eksisterer alle for at betjene forudkompilerede C-objekter, som Delphi-builden linker. Free Pascal linker ikke de objekter, så det behøver intet af det maskineri, og hver reference til det skal forsvinde. Det subtile er, at både deklarationen og implementationen skal udelades sammen. Udelader man kun én, melder compileren noget uhelpfuldt om en identifikator, den ikke kan matche til noget

Reglen, der falder ud af det, er kort. Guard på compileren, når spørgsmålet handler om ABI eller runtime-support, guard på arkitekturen, når spørgsmålet handler om pointer-bredde eller registerantal, og lad aldrig den ene stå ind for den anden

Guard deklarationer og implementationer sammen

En betinget blok i interface-sektionen er let at falde i uden at lægge mærke til det, og den resulterende fejlmeddelelse peger ud over alt andet end på årsagen. Tilføjer man en metodedeklaration til et class interface, er det naturlige sted ved de relaterede metoder, hvilket er fint helt op til det øjeblik, naboerne tilfældigvis sidder inde i en eksisterende {$IFDEF}-blok. Betingede direktiver indenteres ikke, så en blok, der åbnede fyrre linjer oppe, er reelt usynlig, mens man læser de omkringliggende deklarationer

Det, der sker bagefter, er en kompilering, der lykkes på én toolchain og producerer en kaskade på en anden. Er den omkringliggende guard et Delphi-versionstjek, som Free Pascal ikke opfylder, forsvinder deklarationen for FPC, mens den ubetingede implementation forbliver, og compileren melder en lang liste af klager over metodeidentifikatorer, den forventede og ikke fandt. Ingen af meddelelserne nævner den betingede blok, der forårsagede det

To vaner forhindrer hele fejlklassen. Inden du indsætter i en interface-sektion, så kig opad efter den nærmeste åbne betingede i stedet for at stole på den visuelle gruppering. Og behandl en grøn Delphi-test suite som bevis udelukkende om Delphi: Free Pascal-biblioteksbuilden er en separat gate, og den eneste måde at vide, at den består, er at køre build-Win32-Lib-FPC.cmd og build-Win64-Lib-FPC.cmd som del af samme ændring

Hvad bryder i 32-bit aritmetikkode

Én sprogbegrænsning dukker op i præcis den kode, der mindst vil ændres: 32-bit Free Pascal accepterer ikke en UInt64 som kontrolvariabel for en for-løkke. I elliptic curve-uniterne, der bærer X25519 og X448, var løkkerne, der vandrer gennem limb arrays, skrevet med 64-bit tællere, ganske enkelt fordi alt andet i filen er 64-bit

Fixet skal være kirurgisk, for i field-aritmetik er variablens bredde en del af korrekthedsargumentet. Løkkeindekser bliver Integer, da et limb array har en håndfuld elementer, og intet indeks nærmer sig nogensinde 32-bit-området. Alt, der deltager i aritmetikken, limbene selv, carry-propageringen og maskerne, forbliver UInt64, for at indsnævre nogen af dem ændrer lydløst resultatet modulo feltprimet

// 32-bit FPC afviser en UInt64-løkkevariabel. Indsnævr kun indekset;
// limbs, masks og carries beholder deres bredde, ellers ændres field-matematikken
var
  I: Integer;                 // var 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;

Verificeringen af en sådan ændring kan ikke være en round-trip-test. Krypterer og dekrypterer man med samme ødelagte implementation, er den perfekt enig med sig selv, hvilket er grunden til, at known-answer vectors ikke er til forhandling her: kør de publicerede X25519- og X448-testvektorer, og sammenlign de præcise output-byte. Det er det eneste tjek, der adskiller en korrekt implementation fra en selvkonsekvent forkert en, og det gælder i lige så høj grad de symmetriske primitiver behandlet i Free Pascal deflate- og AES-codec-grænserne

De to brudpunkter i en HotPDF Win32 Free Pascal-build: en {$IFDEF WIN32}-guard omkring Delphi-runtime-hjælpere, der kompilerer men fejler ved link, medmindre deklaration og implementation udelades sammen, og UInt64-løkkevariablen i X25519- og X448-limb walks indsnævret til Integer, mens limbs, carries og masks beholder deres bredde
Guard på compileren, når spørgsmålet er ABI eller runtime-support, og på arkitekturen, når det er pointer-bredde, og bevis så aritmetikændringer mod publicerede known-answer vectors frem for round-trip-tests

Hvad en Win32 Free Pascal-build er værd

Den praktiske udbetaling er, at en Lazarus-applikation targetet mod 32-bit Windows får den samme dokumentmotor som sit Delphi-modstykke, uden en separat binær kontrakt at vedligeholde. Det betyder mest for de deployments, folk sjældent taler om: industrielle controllere, point-of-sale-terminaler og langlevet line-of-business-software, hvor 32-bit runtime ikke er et legacy-valg, men en hardwarebegrænsning

Win64-historien kom først og er beskrevet i Free Pascal- og Lazarus-understøttelse på Win64. Win32 er ikke en omgang af den. Win64 har én calling convention, ingen name decoration og ingen Delphi-private integer-hjælpere at arbejde sig udenom, så næsten alt i denne artikel er specifikt for 32-bit-target. Aritmetik-uniterne, der behøvede løkkevariabel-ændringen, er de samme som beskrevet i Montgomery-aritmetik over NIST-kurverne, hvor bredde-disciplinen forklares mere i dybden

Den generelle lektie er, at cross-compiler portabilitetsarbejde primært ikke handler om sprogfeatures. Begge compilere accepterer samme Object Pascal her. Det, der adskiller sig, er objektfilen: hvordan symboler staves, hvilke hjælperutiner runtime antages at levere, og hvilke forudkompilerede objekter der er i linket. HotPDF shipper Free Pascal- og Lazarus-pakkerne ved siden af Delphi- og C++Builder-pakkerne i HotPDF Delphi PDF-komponenten, så samme source tree fodrer hver toolchain i stedet for at forke pr. compiler