Teknisk artikel

HotPDF på Free Pascal: Deflate, AES og codec-grænser

HotPDF kompilerer og kører under Free Pascal 3.2.2 med Lazarus, og den ærlige opsummering af den portering er to sætninger. Dokumentoprettelse, indlæsning, gemning, komprimering, dekomprimering, kryptering og dekryptering virker alle på rene Pascal-backends, så en Lazarus-applikation kan producere og indtage ægte PDF uden nogen C-afhængighed. De valgfrie native image codecs gør ikke, for de forbyggede Win64-objekter bruger en COFF-variant, som ingen af Free Pascal-linkerne kan indtage, så på den toolchain opløses entry points til stubs, der fejler lukket

HotPDF Free Pascal-evnekort: virkende Pascal deflate-, AES- og dokument-backends ved siden af image codec-stubs, der fejler lukket
Dokument-, komprimerings- og krypteringsfunktioner kører på rene Pascal-backends, mens native image codecs opløses til stubs, der fejler lukket

At komme fra "kompilerer" til "virker" krævede et bestemt sæt rettelser, og hver eneste af dem er en fælde, der vil ramme enhver anden Delphi-kodebase, der flytter til Free Pascal. De er værd at nedskrive i den rækkefølge, de gør ondt

Hvorfor beviser en kompilerende unit ingenting?

Fordi en Pascal-unit kan referere et symbol, der aldrig vil gøre noget nyttigt, og stadig tilfredsstille compileren. Da alle 113 biblioteksunits byggede rent under Free Pascal, virkede arkivcontainerhåndteringerne genuint, verificeret af en smoke test, der åbnede en CBZ og konverterede den til PDF. XFA-formularfladning virkede slet ikke, for fladning skal inflate den komprimerede /XFA-pakkestrøm, og deflate-entry pointet var stadig en stub. Intet i build-outputtet skelnede de to tilfælde

Reglen, der kom ud af det, er kort. Før du skriver i en release note, at en funktion virker på en ny toolchain, skriv en runtime-probe, der afprøver funktionen ende til ende på den toolchain. Compile-dækning er en forudsætning, aldrig bevis. Det bredere billede af, hvad porteringen dækker, findes i noterne om Free Pascal- og Lazarus Win64-understøttelse

En raise inde i en cdecl-stub når ikke kalderen

Denne fortjener sin egen sektion, for symptomet er så vildledende. Stub-uniterne eksponerer C-entry points, som et statisk bibliotek ville, så en stub ser sådan ud

// Ser rimeligt ud. Er det ikke.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

På Free Pascal for Win64 propagerer den exception ikke til kalderen. Der er ingen try..except-handler, der ser den, for at unwinde hen over en sådan erklæret cdecl-grænse medfører ikke Pascal-exceptionframen; processen terminerer med exit code 217. Set fra applikationen er der ingen fejl, ingen besked og ingen loglinje, bare et program, der forsvinder. Det er strengt taget værre end et forkert svar, for et forkert svar kan håndteres

Hvorfor en exception, der kastes inde i en cdecl-stub, afslutter en Free Pascal-proces med exit code 217, og hvordan gating af Pascal-entry pointet retter det
Pascal-exceptionframen kan ikke unwinde hen over cdecl-grænsen, så processen dør i stilhed; rettelsen gater, før stubben nogensinde nås

Den fristende rettelse er at få stubben til at returnere en fejlkode i stedet, og for inflate er det rigtigt, for zlib har en veldefineret fejlretur. Det er forkert generelt: en stub for jpeg_read_header, der returnerer nul, fortæller kalderen at fortsætte med en struktur, ingen har initialiseret. Den holdbare rettelse er at gate ved Pascal-entry pointet frem for inde i den C-formede stub under brug af den fejlkonvention, API'en allerede har

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Nægt, før stubben nogensinde nås, med dette API's egen
  // fejlkonvention frem for en exception hen over cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib er ikke zlib, og forskellen er to dokumentklasser

Den Pascal-deflate-implementation, der findes til Free Pascal, håndterer to indramninger: zlib-wrapperen og rå deflate. Den håndterer ikke gzip-ramme, som zlib vælger gennem windowBits-værdier fra 16 til 31, og den håndterer ikke den automatiske detektionstilstand, som værdierne 32 til 47 vælger. HotPDF behøver begge. Den sikre SVG-importvej beder om 31, og loaderen har en fallback-stige, der beder om 47, når en strøms indramning er tvetydig. Spring én af dem over, og en hel familie af dokumenter holder op med at åbne, med en dekodefejl, der peger på strømmen frem for den manglende indramning

windowBits-dækning i paszlib frem for zlib: gzip-ramme og auto-detect-intervaller mangler til HotPDF SVG-import og loader-fallback
paszlib håndterer zlib-wrapperen og rå deflate, men HotPDF behøver også windowBits 31 og 47, så shimmen selv må levere gzip-rammen

Der er en anden, skarpere inkompatibilitet. Den z_stream-record, som paszlib erklærer, har ikke det samme hukommelseslayout som C-udgaven: dens msg-felt er en short string frem for en pointer, og total_in og total_out er 64-bit, hvor C ABI har maskinord. En kalder-record kan derfor ikke sendes direkte igennem. Den arbejdende ordning er at holde paszlib-tilstanden bag den state-pointer, som den offentlige record allerede reserverer, og at kopiere de offentlige felter ind og ud omkring hvert kald. gzip-CRC'en og den otte byte lange længde-trailer håndteres i samme shim-lag, hvilket er det naturlige sted for dem, da det allerede ejer indramningsbeslutningen

At sende et dynamisk array til en utypet var-parameter

Dette er fejlen, der med størst sandsynlighed sidder i din kode lige nu. Når du sender et dynamisk array til en utypet var-parameter, modtager den kaldte adressen på arrayvariablen, hvilket er adressen på en pointer, ikke adressen på payloaden. Så en læsning ind i den overskriver selve variablen og alt, hvad der ligger ved siden af

var
  FBuffer: TBytes;
begin
  SetLength(FBuffer, 65536);

  // Forkert: overdrager adressen på FBuffer-variablen
  FStream.Read(FBuffer, Length(FBuffer));

  // Rigtigt: overdrager adressen på den første payload-byte
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

På Delphi ser den forkerte form ofte ud til at virke, for det, den korrumperer, er en tilstødende stack-plads, som intet læser bagefter. På Free Pascal giver samme linje en segmentation fault ved første brug. Det, der gør den så svær at få øje på, er, at statiske arrays ikke har det problem, da en statisk arrayvariabel er sin egen payload, så begge skrivemåder er korrekte i samme fil afhængigt af deklarationen et par hundrede linjer væk

ZIP-containere uden System.Zip

Free Pascal har ingen ækvivalent til RTL zip-uniten, og det tilgængelige alternativ har både en anden API-overflade og ingen understøttelse af den legacy-kryptering, som ældre containerformater stadig bruger, så en lille læser i biblioteket viste sig at være kortere end at tilpasse sig det. To formatdetaljer kostede tid og er lette at tage fejl af

Den første er krypteringsheaderens tjekbyte. Dens tolvte byte er normalt den høje byte af CRC'en, men når general-purpose flag bit 3 er sat, hvilket betyder, at størrelserne ligger i en efterfølgende data descriptor, og CRC'en endnu ikke er kendt, kommer tjekbytten fra den høje byte af ændringstidspunktet i stedet. Implementér kun CRC-formen, og hvert arkiv skrevet i streamingtilstand afviser en korrekt adgangskode. Den anden er ZIP64 extra-feltet: dets tre 64-bit-felter optræder i fast rækkefølge, men skrives kun, når det tilsvarende 32-bit-felt er mættet, så læsning af dem ved faste offsets virker på de arkiver, du testede, og fejler på det næste. Parse dem positionelt mod, hvilke 32-bit-felter der er mættede

En bekvemmelighed værd at kende: Free Pascal dekomprimeringsstrøm tager et andet konstruktorargument, der springer zlib-headeren over, hvilket er præcis, hvad ZIP-entries behøver, da de gemmer rå deflate. Den vej rører slet ikke bibliotekets zlib-shim, så den påvirkes ikke af den manglende C-backend

Farveglyph-gennemsigtighed under LCL

At læse alfakanalen i et rasteriseret farveglyph er den ene grafikdetalje uden direkte oversættelse. LCL PNG-klassen har ingen scanline-accessor, der eksponerer alpha, og at assigne en PNG til en bitmap smider den væk, så et farve-emoji ankommer fuldstændig opaqt og komponeres med en sort boks bag sig. Den virkende vej er interfacet image: skab det ud fra PNG'en, og læs derefter pixels gennem farve-accessoren og husk, at dets komponenter er 16-bit og behøver at skifte otte ned for at blive bytes. Den overflade bruger også naturlig top-down-rækkefølge, så Height - 1 - Y-inversionen, som VCL scanline-kode behøver, må fjernes frem for porteres

To build-system-noter, før du rapporterer en fejl

Et fuldt genbuild fejler af og til med et udefineret symbol, hvis navn slutter med et $crc-suffiks og en hex-værdi. Det suffiks beregnes ud fra parametertyperne, og det fejler at matche, når ét build kompilerer en unit mod to forskellige interfaceversioner i samme gennemløb. At køre buildet igen rydder det; signaturen er ikke forkert

For det andet har Free Pascal 3.2.2 ingen anonyme metoder, så alle de steder, hvor biblioteket brugte closures til at koble en parallel pipeline op, tager Free Pascal-bygget en deterministisk serial-fallback i stedet. Output er identisk, gennemløbshastighed er det ikke; hvis du er afhængig af parallel siderendering, er det en grund til at blive på Delphi foreløbigt, og pipeline-designet er beskrevet i artiklen om parallel render-pipeline. Image codec-situationen er det andet sted, hvor toolchain-valget ændrer evner frem for kun hastighed, så en Lazarus-udrulning bør planlægge sine billedformater derefter; den aktuelle matrix pr. toolchain findes på produktsiden HotPDF Delphi PDF component