Teknisk artikkel

HotPDF på Free Pascal: Deflate, AES og codec-grenser

HotPDF kompilerer og kjører under Free Pascal 3.2.2 med Lazarus, og den ærlige oppsummeringen av den porteringen er to setninger. Dokumentoppretting, lasting, lagring, komprimering, dekomprimering, kryptering og dekryptering fungerer alle på ren-Pascal-backends, så en Lazarus-applikasjon kan produsere og konsumere ekte PDF uten noen C-avhengighet. De valgfrie native image-codecs gjør det ikke, fordi de forhåndsbygde Win64-objektene bruker en COFF-variant som ingen av Free Pascal-linkerne kan konsumere, så på den verktøykjeden løses inngangspunktene til stubber som feiler lukket

HotPDF Free Pascal-evnekart: fungerende Pascal deflate, AES og dokument-backends ved siden av image-codec-stubber som feiler lukket
Dokument-, komprimerings- og krypteringsfunksjoner kjører på ren-Pascal-backends, mens native image-codecs løses til stubber som feiler lukket

Å komme fra «kompilerer» til «fungerer» tok et bestemt sett med fikser, og hver eneste av dem er en felle som vil finne enhver annen Delphi-kodebase som flytter til Free Pascal. De er verdt å skrive ned i den rekkefølgen de gjør vondt

Hvorfor beviser en enhet som kompilerer ingenting?

For en Pascal-enhet kan referere et symbol som aldri vil gjøre noe nyttig og likevel tilfredsstille kompilatoren. Da alle 113 bibliotekenhetene bygget rent under Free Pascal, fungerte arkivbeholderhåndtererne genuint, verifisert av en røyktest som åpnet en CBZ og konverterte den til PDF. XFA-skjemaflating fungerte ikke i det hele tatt, fordi flating må inflate den komprimerte /XFA-pakkestrømmen, og deflate-inngangspunktet var fortsatt en stub. Ingenting i byggoutputen skilte de to tilfellene

Regelen som kom ut av det, er kort. Før du skriver i en utgivelsesnote at en funksjon fungerer på en ny verktøykjede, skriv en runtime-probe som trener funksjonen ende til ende på den verktøykjeden. Kompileringsdekning er en forutsetning, aldri et bevis. Det bredere bildet av hva porteringen dekker, står i notatene om Free Pascal- og Lazarus Win64-støtte

Et raise inne i en cdecl-stub når ikke kalleren

Denne fortjener sin egen seksjon fordi symptomet er så villedende. Stubbenhetene eksponerer C-inngangspunkter slik et statisk bibliotek ville gjort, så en stub ser slik ut

// Ser fornuftig ut. 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 det unntaket ikke til kalleren. Det finnes ingen try..except-håndterer som ser det, fordi avvikling på tvers av en cdecl-grense deklarert på denne måten ikke bærer Pascal-unntaksrammen; prosessen terminerer med exit code 217. Fra applikasjonssiden er det ingen feil, ingen melding og ingen logglinje, bare et program som forsvinner. Det er strengt tatt verre enn et feil svar, fordi et feil svar kan håndteres

Hvorfor et unntak kastet inne i en cdecl-stub avslutter en Free Pascal-prosess med exit code 217, og hvordan gating av Pascal-inngangspunktet fikser det
Pascal-unntaksrammen kan ikke vikle seg over cdecl-grensen, så prosessen dør lydløst; fiksen gater før stubben noensinne nås

Den fristende fiksen er å la stubben returnere en feilkode i stedet, og for inflate er det riktig fordi zlib har en veldefinert feilretur. Det er feil generelt: en stub for jpeg_read_header som returnerer null, forteller kalleren å fortsette med en struktur ingen initialiserte. Den varige fiksen er å gate ved Pascal-inngangspunktet snarere enn inne i den C-formede stubben, ved å bruke den feilkonvensjonen API-en allerede har

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Avslå før stubben noensinne nås, med denne API-ens egen
  // feilkonvensjon snarere enn et unntak på tvers av cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib er ikke zlib, og forskjellen er to dokumentklasser

Pascal deflate-implementeringen som finnes på Free Pascal, håndterer to innramminger: zlib-wrapperen og rå deflate. Den håndterer ikke gzip-innramming, som zlib velger gjennom windowBits-verdier fra 16 til 31, og den håndterer ikke den automatiske deteksjonsmodusen som verdier 32 til 47 velger. HotPDF trenger begge. Den trygge SVG-importveien ber om 31, og lasteren har en fallback-stige som ber om 47 når en strøms innramming er tvetydig. Hopp over en av dem, og en hel familie av dokumenter slutter å åpne, med en dekodefeil som peker på strømmen snarere enn på den manglende innrammingen

windowBits-dekning i paszlib mot zlib: gzip-innramming og auto-detekteringsområder mangler for HotPDF SVG-import og laster-fallback
paszlib håndterer zlib-wrapperen og rå deflate, men HotPDF trenger også windowBits 31 og 47, så shimen må levere gzip-innramming selv

Det finnes en annen, skarpere inkompatibilitet. z_stream-recorden paszlib deklarerer, har ikke det samme minneoppsettet som C-én: msg-feltet er en kort streng snarere enn en peker, og total_in og total_out er 64-bit der C ABI har maskinord. En kallerrecord kan derfor ikke sendes rett gjennom. Ordningen som fungerer, er å holde paszlib-tilstanden bak state-pekeren som den offentlige recorden allerede reserverer, og å kopiere de offentlige feltene inn og ut rundt hvert kall. gzip CRC-en og den åtte byte lange lengdetraileren er innregnet i det samme shim-laget, noe som er det naturlige stedet for dem siden det allerede eier innrammingsavgjørelsen

Å sende en dynamisk array til en utypet var-parameter

Dette er feilen som mest sannsynlig sitter i koden din akkurat nå. Når du sender en dynamisk array til en utypet var-parameter, er det mottakeren får adressen til array-variabelen, som er adressen til en peker, ikke adressen til payloaden. Så en lesing inn i den overskriver variabelen selv og det som sitter ved siden av

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

  // Feil: gir fra seg adressen til FBuffer-variabelen
  FStream.Read(FBuffer, Length(FBuffer));

  // Riktig: gir fra seg adressen til den første payload-byten
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

På Delphi ser feil form ofte ut til å fungere, fordi det den korrupterer, er en tilstøtende stackplass ingenting leser etterpå. På Free Pascal segmentfeiler den samme linjen ved første bruk. Det som gjør den så vanskelig å oppdage med øyet, er at statiske arrays ikke har det problemet, siden en statisk array-variabel er sin egen payload, så begge stavemåtene er korrekte i den samme filen avhengig av deklarasjonen noen hundre linjer unna

ZIP-beholdere uten System.Zip

Free Pascal har ingen ekvivalent til RTL zip-enheten, og det tilgjengelige alternativet har både et annet API-grensesnitt og ingen støtte for legacy-krypteringen eldre containerformater fortsatt bruker, så en liten leser inne i biblioteket viste seg kortere enn å tilpasse seg den. To formatdetaljer kostet tid og er lette å ta feil på

Den første er krypteringsheaderens sjekkbyte. Den tolvte byten er normalt den høye byten av CRC-en, men når general-purpose flaggbit 3 er satt, noe som betyr at størrelsene bor i en etterfølgende databeskriver og CRC-en ennå ikke er kjent, kommer sjekkbyten fra den høye byten av endringstiden i stedet. Implementer bare CRC-formen, og hvert arkiv skrevet i strømmodus avviser et korrekt passord. Den andre er ZIP64-ekstrafeltet: dets tre 64-bit-felt opptrer i fast rekkefølge, men skrives bare når det tilsvarende 32-bit-feltet er mettet, så å lese dem ved faste forskyvninger fungerer på arkivene du testet og feiler på den neste. Parse dem posisjonelt mot hvilke 32-bit-felter som er mettet

Én bekvemmelighet verdt å kjenne: Free Pascal dekomprimeringsstrømmen tar et annet konstruktørargument som hopper over zlib-headeren, noe som er nøyaktig det ZIP-oppføringene trenger siden de lagrer rå deflate. Den veien rører ikke bibliotekets zlib-shim i det hele tatt, så den påvirkes ikke av den manglende C-backend

Fargeglyftransparens under LCL

Å lese alfakanalen til et rasterisert fargeglyf er den ene grafikkdetaljen uten direkte oversettelse. LCL PNG-klassen har ingen skannelinjeaksessor som eksponerer alfa, og å tildele en PNG til en bitmap forkaster den, så et farge-emoji ankommer fullstendig ugjennomsiktig og komponeres med en svart boks bak seg. Den fungerende ruten er grensesnittbildet: opprett det fra PNG-en, og les deretter piksler gjennom fargeaksessoren, og husk at komponentene er 16-bit og trenger å skiftes ned med åtte for å bli byte. Den overflaten bruker også naturlig topp-ned radrekkefølge, så Height - 1 - Y-inversjonen som VCL skannelinjekode trenger, må fjernes snarere enn porteres

To byggsystemnotater før du melder en feil

En full gjenoppbygging feiler av og til med et udefinert symbol hvis navn slutter på et $crc-suffiks og en heksadesimal verdi. Det suffikset beregnes fra parametertypene, og det feiler å matche når ett bygg kompilerer en enhet mot to forskjellige grensesnittversjoner i samme gjennomkjøring. Å kjøre bygget på nytt fjerner det; signaturen er ikke feil

For det andre har Free Pascal 3.2.2 ingen anonyme metoder, så der biblioteket brukte closures til å koble opp en parallell pipeline, tar Free Pascal-bygget en deterministisk seriell fallback i stedet. Output er identisk, gjennomstrømning er ikke det; hvis du er avhengig av parallell siderendering, er det en grunn til å bli på Delphi foreløpig, og pipelinedesignet er beskrevet i artikkelen om parallell render-pipeline. Image-codec-situasjonen er det andre stedet der verktøykjedevalg endrer evne snarere enn bare hastighet, så en Lazarus-utrulling bør planlegge sine bildeformater deretter; den nåværende per-verktøykjede-matrisen står på produktsiden for HotPDF Delphi PDF component