Teknisk artikel

HotPDF i Free Pascal: Deflate, AES och codecgränser

HotPDF kompilerar och körs under Free Pascal 3.2.2 med Lazarus, och den ärliga sammanfattningen av den porteringen är två meningar. Documentskapande, inläsning, sparande, komprimering, dekomprimering, kryptering och dekryptering fungerar alla på enbart-Pascal-backends, så en Lazarus-applikation kan producera och konsumera riktig PDF utan något C-beroende. De valfria nativa bildcodecs gör det inte, eftersom de förbyggda Win64-objekten använder en COFF-variant som ingen av de båda Free Pascal-länkarna kan konsumera, så på den verktygskedjan löser ingångspunkterna upp sig i stubbar som misslyckas stängt

HotPDF Free Pascal-förmågekarta: fungerande Pascal-deflate, AES- och dokumentbackends jämte bildcodec-stubbar som misslyckas stängt
Dokument-, komprimerings- och krypteringsfunktioner körs på enbart-Pascal-backends, medan nativa bildcodecs löser upp sig i stubbar som misslyckas stängt

Att komma från ”kompilerar” till ”fungerar” krävde en specifik uppsättning fixar, och varenda en av dem är en fälla som kommer att träffa vilken annan Delphi-kodbas som helst som flyttar till Free Pascal. De är värda att skriva ner i den ordning de sårade

Varför bevisar en kompilerande enhet ingenting?

Eftersom en Pascal-enhet kan referera en symbol som aldrig kommer göra något användbart och ändå tillfredsställa kompilatorn. I det läge alla 113 biblioteksenheter byggde rent under Free Pascal fungerade arkivcontainerhanterarna genuint, verifierat av ett rögtest som öppnade en CBZ och konverterade den till PDF. XFA-formulärplattning fungerade inte alls, eftersom plattning måste inflate:a den komprimerade /XFA-paketströmmen och deflate-ingångspunkten fortfarande var en stub. Ingenting i byggutmatningen skilde åt de två fallen

Regeln som kom ur den är kort. Innan du skriver i en releasenotis att en funktion fungerar på en ny verktygskedja, skriv en körtidsprovning som trär funktionen ände till ände på den verktygskedjan. Kompileringstäckning är en förutsättning, aldrig bevis. Den bredare bilden av vad porteringen täcker finns i noteringarna om Free Pascal- och Lazarus-Win64-stöd

Ett raise inuti en cdecl-stub når inte anroparen

Denna förtjänar sitt eget avsnitt eftersom symptomet är så vilseledande. Stubb-enheterna exponerar C-ingångspunkter som ett statiskt bibliotek skulle göra, så en stub ser ut så här

// Ser rimligt ut. Är det inte.
function inflate(Strm: Pointer; Flush: Integer): PtrUInt; cdecl;
  public name 'inflate';
begin
  raise ENotSupportedException.Create('codec unavailable');
end;

På Free Pascal för Win64 propagerar det undantaget inte till anroparen. Det finns ingen try..except-hanterare som ser det, eftersom avveckling över en cdecl-gräns deklarerad på detta sätt inte bär med sig Pascal-undantagsramen; processen avslutas med exit code 217. Från applikationssidan finns inget fel, inget meddelande och ingen loggrad, bara ett program som försvinner. Det är strikt sämre än ett felaktigt svar, för ett felaktigt svar kan hanteras

Varför ett undantag kastat inuti en cdecl-stub avslutar en Free Pascal-process med exit code 217 och hur avspärrning av Pascal-ingångspunkten fixar det
Pascal-undantagsramen kan inte veckla ut sig över cdecl-gränsen, så processen dör tyst; fixen spärrar av innan stubben någonsin nås

Den frestande fixen är att låta stubben returnera en felkod i stället, och för inflate är det rätt eftersom zlib har en väldefinierad felretur. Det är fel i allmänhet: en stub för jpeg_read_header som returnerar noll säger till anroparen att fortsätta med en struktur ingen initierat. Den varaktiga fixen är att spärra av vid Pascal-ingångspunkten i stället för inuti den C-formade stubben, med den felkonvention API:et redan har

function TryDecodeJPEG(const Data: TBytes; out Bitmap: TBitmap): Boolean;
begin
{$IFDEF FPC}
  // Neka innan stubben någonsin nås, med detta API:s egen
  // felkonvention i stället för ett undantag över cdecl
  Bitmap := nil;
  Result := False;
  Exit;
{$ENDIF}
  Result := DecodeJPEGNative(Data, Bitmap);
end;

paszlib är inte zlib, och skillnaden är två dokumentklasser

Den Pascal-deflate-implementering som finns på Free Pascal hanterar två inramningar: zlib-wrappern och rå deflate. Den hanterar inte gzip-inramning, som zlib väljer via windowBits-värden från 16 till 31, och den hanterar inte det automatiska detekteringsläget som värdena 32 till 47 väljer. HotPDF behöver båda. Den säkra SVG-importvägen begär 31, och inläsaren har en reservstege som begär 47 när en ströms inramning är tvetydig. Hoppa över någon av dem och en hel familj av dokument slutar öppnas, med ett avkodningsfel som pekar på strömmen i stället för på den saknade inramningen

windowBits-täckning hos paszlib jämfört med zlib: gzip-inramning och auto-detekteringsintervall saknas för HotPDF SVG-import och inläsares reservväg
paszlib hanterar zlib-wrappern och rå deflate, men HotPDF behöver också windowBits 31 och 47, så shimen måste själv tillhandahålla gzip-inramning

Det finns en andra, skarpare inkompatibilitet. Posten z_stream som paszlib deklarerar har inte samma minneslayout som C-varianten: dess fält msg är en ShortString i stället för en pekare, och total_in och total_out är 64-bit där C-ABI har maskinord. En anroparpost kan därför inte skickas rakt igenom. Det fungerande upplägget är att hålla paszlib-tillståndet bakom state-pekaren som den publika posten redan reserverar, och att kopiera de publika fälten in och ut runt varje anrop. gzip-CRC:n och den åtta byte långa längdtrailern redovisas i samma shim-lager, vilket är den naturliga platsen för dem eftersom det redan äger inramningsbeslutet

Att skicka en dynamisk array till en otypad var-parameter

Detta är buggen som med störst sannolikhet sitter i din kod just nu. När du skickar en dynamisk array till en otypad var-parameter får mottagaren adressen till arrayvariabeln, vilket är adressen till en pekare, inte adressen till nyttolasten. En läsning in i den skriver alltså över variabeln själv och det som ligger bredvid

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

  // Fel: lämnar över adressen till FBuffer-variabeln
  FStream.Read(FBuffer, Length(FBuffer));

  // Rätt: lämnar över adressen till första nyttolastbyten
  FStream.Read(FBuffer[0], Length(FBuffer));
end;

På Delphi verkar den felaktiga formen ofta fungera, eftersom det den korrumperar är en angränsande stackplats som inget läser efteråt. På Free Pascal ger samma rad segmentfault vid första användningen. Det som gör den så svår att upptäcka med ögat är att statiska arrayer inte har det problemet, eftersom en statisk arrayvariabel är sin egen nyttolast, så båda stavningarna är korrekta i samma fil beroende på deklarationen några hundra rader bort

ZIP-containrar utan System.Zip

Free Pascal har ingen motsvarighet till RTL-zipenheten, och det tillgängliga alternativet har både en annan API-yta och inget stöd för den äldre kryptering som äldre containerformat fortfarande använder, så en liten läsare inuti biblioteket visade sig kortare än att anpassa sig till det. Två formatdetaljer kostade tid och är lätta att få fel

Den första är kontrollbytet i krypteringshuvudet. Dess tolfte byte är normalt CRC:ns höga byte, men när flaggbit 3 i general-purpose-flaggan är satt, vilket betyder att storlekarna bor i en avslutande data descriptor och CRC:n ännu inte är känd, kommer kontrollbytet i stället från ändringstidens höga byte. Implementera bara CRC-formen och avvisar vartenda arkiv som skrivits i strömmande läge ett korrekt lösenord. Den andra är extrafältet ZIP64: dess tre 64-bit-fält uppträder i fast ordning men skrivs bara när motsvarande 32-bit-fält är mättade, så läsning vid fasta offset fungerar på arkiven du testade och misslyckas på nästa. Parsa dem positionellt mot vilka 32-bit-fält som är mättade

En bekvämlighet värd att känna till: Free Pascal-dekomprimeringsströmmen tar ett andra konstruktorargument som hoppar över zlib-huvudet, vilket är exakt vad ZIP-poster behöver eftersom de lagrar rå deflate. Den vägen rör inte bibliotekets zlib-shim alls, så den påverkas inte av den saknade C-backend

Transparens för färgglyfer under LCL

Att läsa alfakanalen hos en rastrerad färgglyf är den grafiska detalj som saknar direkt översättning. LCL:s PNG-klass har ingen scanline-åtkomstare som exponerar alfa, och att tilldela en PNG till en bitmapp slänger den, så en färg-emoji anländer helt opak och komponeras med en svart låda bakom sig. Den fungerande vägen är gränssnittsbilden: skapa den från PNG:en och läs sedan pixlar genom färgåtkomstaren, med tanke på att dess komponenter är 16-bit och behöver skiftas ner åtta för att bli byte. Den ytan använder också naturlig topp-ner-radordning, så inversionen Height - 1 - Y som VCL-scanline-kod behöver måste tas bort i stället för porteras

Två noteringar om byggsystemet innan du rapporterar en bugg

En fullständig ombyggnad misslyckas ibland med en odefinierad symbol vars namn slutar med ett $crc-suffix och ett hexadecimalt värde. Det suffixet beräknas från parametertyperna, och det misslyckas matcha när ett bygge kompilerar en enhet mot två olika gränssnittsversioner i samma pass. Att köra bygget igen klarar det; signaturen är inte fel

För det andra har Free Pascal 3.2.2 inga anonyma metoder, så varhelst biblioteket använde closures för att koppla ihop en parallell pipeline tar Free Pascal-bygget en deterministisk seriell reservväg i stället. Utdata är identiskt, genomströmningen är det inte; om du är beroende av parallell sidrendering är det en anledning att stanna kvar på Delphi för stunden, och pipelinedesignen beskrivs i artikeln om parallell renderingspipeline. Situationen för bildcodecs är den andra platsen där verktygskedsval ändrar förmåga snarare än bara hastighet, så en Lazarus-distribution bör planera sina bildformat därefter; den aktuella matrisen per verktygskedja finns på produktsidan för HotPDF Delphi PDF component