Technisch artikel

HotPDF in Free Pascal: deflate, AES en codeclimieten

HotPDF compileert en draait onder Free Pascal 3.2.2 met Lazarus, en de eerlijke samenvatting van die portering is twee zinnen lang. Documentcreatie, laden, opslaan, compressie, decompressie, versleuteling en ontsleuteling werken allemaal op backends van puur Pascal, dus een Lazarus-applicatie kan echte PDF produceren en verbruiken zonder enige C-afhankelijkheid. De optionele native beeldcodecs doen dat niet, want de voorgebouwde Win64-objecten gebruiken een COFF-variant die geen enkele Free Pascal-linker kan verwerken, dus op die toolchain lossen de toegangspunten op in stubs die veilig falen

HotPDF Free Pascal-capaciteitenkaart: werkende Pascal deflate, AES en documentbackends naast beeldcodec-stubs die veilig falen
Document-, compressie- en versleutelingsfuncties draaien op backends van puur Pascal, terwijl native beeldcodecs uitkomen op stubs die veilig falen

Van "compileert" naar "werkt" komen vergde een specifieke set fixes, en elk daarvan is een valkuil die elke andere Delphi-codebase die naar Free Pascal verhuist zal vinden. Ze zijn het opschrijven waard, in de volgorde waarin ze pijn deden

Waarom bewijst het compileren van een unit niets?

Omdat een Pascal-unit naar een symbool kan verwijzen dat nooit iets nuttigs zal doen en toch aan de compiler voldoet. Op het moment dat alle 113 bibliotheekunits schoon bouwden onder Free Pascal, werkten de archive-containerhandlers echt, geverifieerd door een smoketest die een CBZ opende en naar PDF converteerde. XFA-formuliervlakmaking werkte helemaal niet, want vlakmaking moet de gecomprimeerde /XFA-packetstream inflaten en het deflate-toegangspunt was nog een stub. Niets in de builduitvoer onderscheidde die twee gevallen

De regel die daaruit volgde is kort. Schrijf, voordat u in een releasenotitie neerschrijft dat een functie op een nieuwe toolchain werkt, een runtime-probe die de functie end-to-end op die toolchain oefent. Compileerdekking is een voorwaarde, nooit bewijs. Het bredere beeld van wat de portering dekt staat in de supportnotities voor Free Pascal en Lazarus Win64

Een raise binnen een cdecl-stub bereikt de aanroeper niet

Deze verdient een eigen sectie omdat het symptoom zo misleidend is. De stub-units geven C-toegangspunten bloot zoals een statische bibliotheek dat zou doen, dus een stub ziet er zo uit

// Ziet er redelijk uit. Is het niet.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

Onder Free Pascal voor Win64 propageert die exception niet naar de aanroeper. Er is geen try..except-handler die die ziet, want het afwikkelen over een cdecl-grens die op deze manier is gedeclareerd draagt het Pascal-exceptionframe niet mee; het proces eindigt met exitcode 217. Vanuit de applicatie is er geen fout, geen bericht en geen logregel, alleen een programma dat verdwijnt. Dat is strikt slechter dan een verkeerd antwoord, want een verkeerd antwoord kan worden afgehandeld

Waarom een exception in een cdecl-stub een Free Pascal-proces beëindigt met exitcode 217 en hoe het afsluiten van het Pascal-toegangspunt dat oplost
Het Pascal-exceptionframe kan niet over de cdecl-grens afwikkelen, dus het proces sterft geruisloos; de oplossing sluit af voordat de stub bereikt wordt

De verleidelijke oplossing is de stub een foutcode te laten teruggeven, en voor inflate klopt dat, omdat zlib een goed gedefinieerde foutreturn heeft. In het algemeen is het fout: een stub voor jpeg_read_header die nul teruggeeft zegt tegen de aanroeper door te gaan met een structuur die niemand heeft geïnitialiseerd. De duurzame oplossing is af te sluiten bij het Pascal-toegangspunt in plaats van binnen de C-vormige stub, met de foutconventie die die API al heeft

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Weiger voordat de stub ooit bereikt wordt, met de eigen
  // foutconventie van deze API in plaats van een exception over cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib is niet zlib, en het verschil is twee documentklassen

De onder Free Pascal beschikbare Pascal-deflate-implementatie behandelt twee framings: de zlib-wrapper en raw deflate. Ze behandelt geen gzip-framing, die zlib selecteert via windowBits-waarden van 16 tot 31, en ze behandelt de automatische detectiemodus niet die door waarden 32 tot 47 wordt geselecteerd. HotPDF heeft beide nodig. Het veilige SVG-importpad vraagt om 31, en de loader heeft een fallback-ladder die om 47 vraagt wanneer de framing van een stream dubbelzinnig is. Er één overslaan en een hele familie documenten stopt met openen, met een decodeerfout die naar de stream wijst in plaats van naar de ontbrekende framing

windowBits-dekking van paszlib versus zlib: gzip-framing en auto-detect-bereiken ontbreken voor HotPDF SVG-import en loader-fallback
paszlib behandelt de zlib-wrapper en raw deflate, maar HotPDF heeft ook windowBits 31 en 47 nodig, dus de shim moet de gzip-framing zelf leveren

Er is een tweede, scherpere incompatibiliteit. Het z_stream-record dat paszlib declareert heeft niet dezelfde geheugenlay-out als de C-variant: zijn veld msg is een shortstring in plaats van een pointer, en total_in en total_out zijn 64-bit waar de C ABI machinewoorden heeft. Een aanroepersrecord kan dus niet rechtstreeks worden doorgegeven. De werkende opzet is de paszlib-status achter de pointer state te houden die het publieke record al reserveert, en de publieke velden bij elke aanroep erin en eruit te kopiëren. De gzip-CRC en de lengtetrailer van acht bytes worden in diezelfde shim-laag verwerkt, wat de natuurlijke plek voor hen is omdat die laag al de framingbeslissing bezit

Een dynamische array aan een untyped var-parameter doorgeven

Dit is de bug die op dit moment waarschijnlijk in uw code zit. Wanneer u een dynamische array aan een untyped var-parameter doorgeeft, ontvangt de ontvanger het adres van de arrayvariabele, wat het adres van een pointer is, niet het adres van de payload. Een lezing erin overschrijft dus de variabele zelf en wat er ook naast zit

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

  // Fout: geeft het adres van de variabele FBuffer door
  FStream.Read(FBuffer, Length(FBuffer));

  // Goed: geeft het adres van de eerste payloadbyte door
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

Onder Delphi lijkt de verkeerde vorm vaak te werken, omdat wat die corrumpeert een aangrenzende stackslot is dat daarna door niets wordt gelezen. Onder Free Pascal geeft dezelfde regel een segmentation fault bij eerste gebruik. Wat het zo moeilijk maakt om met het oog te spotten, is dat statische arrays dit probleem niet hebben, want een variabele van een statische array is zijn eigen payload, dus beide schrijfwijzen zijn correct in hetzelfde bestand, afhankelijk van de declaratie een paar honderd regels verderop

ZIP-containers zonder System.Zip

Free Pascal heeft geen equivalent van de RTL zip-unit, en het beschikbare alternatief heeft zowel een ander API-oppervlak als geen ondersteuning voor de legacy-versleuteling die oudere containerformaten nog gebruiken, dus een kleine lezer in de bibliotheek zelf bleek korter dan zich daaraan aanpassen. Twee formaatdetails kostten tijd en zijn makkelijk fout te doen

De eerste is de checkbyte van de encryption-header. Zijn twaalfde byte is normaal de hoge byte van de CRC, maar wanneer bit 3 van de general-purpose flag is gezet, wat betekent dat de groottes in een trailing data descriptor zitten en de CRC nog niet bekend is, komt de checkbyte in plaats daarvan uit de hoge byte van de wijzigingstijd. Alleen de CRC-vorm implementeren en elk archief dat in streamingmodus is weggeschreven verwerpt een correct wachtwoord. De tweede is het ZIP64 extra field: diens drie 64-bit velden verschijnen in een vaste volgorde maar worden alleen weggeschreven wanneer het overeenkomstige 32-bit veld verzadigd is, dus ze op vaste offsets lezen werkt op de archieven die u testte en faalt op de volgende. Parseer ze positioneel tegen welke 32-bit velden verzadigd zijn

Eén gemak dat het kennen waard is: de Free Pascal-decompressiestream neemt een tweede constructorargument mee dat de zlib-header overslaat, en dat is precies wat ZIP-items nodig hebben omdat ze raw deflate opslaan. Dat pad raakt de zlib-shim van de bibliotheek helemaal niet, dus het blijft onaangetast door de ontbrekende C-backend

Transparantie van kleurglyphs onder de LCL

Het uitlezen van het alfakanaal van een gerasterde kleurglyph is het ene graphicsdetail zonder directe vertaling. De PNG-klasse van de LCL heeft geen scanline-accessor die alfa blootgeeft, en een PNG aan een bitmap toewijzen wist het uit, dus een kleuremoji komt volledig dekkend aan en componeert met een zwart kader erachter. De werkende route is het interfacebeeld: maak het aan vanuit de PNG en lees daarna pixels via de kleuraccessor, in het achterhoofd houdend dat de componenten 16-bit zijn en acht posities naar beneden geschoven moeten worden om bytes te worden. Dat oppervlak gebruikt ook de natuurlijke top-down rijvolgorde, dus de inversie Height - 1 - Y die VCL-scanlinecode nodig heeft moet worden verwijderd in plaats van geporteerd

Twee buildsystemnotities voordat u een bug meldt

Een volledige herbouw faalt af en toe met een undefined symbol waarvan de naam eindigt op een $crc-suffix en een hexadecimale waarde. Die suffix wordt berekend uit de parametertypes en komt niet overeen wanneer één build een unit in dezelfde pas tegen twee verschillende interfaceversies compileert. De build opnieuw draaien lost het op; de signatuur klopt wel

Ten tweede heeft Free Pascal 3.2.2 geen anonieme methoden, dus overal waar de bibliotheek closures gebruikte om een parallelle pijplijn te bekabelen kiest de Free Pascal-build voor een deterministische seriële fallback in plaats daarvan. De uitvoer is identiek, de doorvoer niet; als u afhankelijk bent van parallel paginarenderen, is dat een reden om voorlopig bij Delphi te blijven, en het pijplijnontwerp wordt beschreven in het artikel over de parallelle renderpijplijn. De beeldcodecsituatie is de andere plek waar de keuze van de toolchain capaciteit verandert en niet alleen snelheid, dus een Lazarus-uitrol moet zijn beeldformaten dienovereenkomstig plannen; de huidige matrix per toolchain staat op de productpagina van de HotPDF Delphi PDF component