Een Pascal-binding over een C-bibliotheek leest als gewone Pascal. U roept een methode aan, krijgt een record terug en geeft vrij wat u hebt gealloceerd. Het probleem is echter dat PDFium een C- en C++-bibliotheek is met een eigen calling convention, eigen integer-breedtes en eigen regels over wie het geheugen bezit en wie het vrijgeeft. Niets daarvan overschrijdt uit zichzelf de taalgrens. Elk van deze contracten moet handmatig opnieuw worden geformuleerd in de Pascal-declaraties, en één verkeerd woord verandert een correct ogende aanroep in stack-corruptie, een afgekapte offset of een double-free. Een audit van versie 1.61.0 van een PDFium VCL-binding bracht van elk type één fout aan het licht. Het is de moeite waard om ze door te nemen, omdat ze niet specifiek zijn voor deze binding. Het zijn de constante risico's bij het verpakken van een C-API in Delphi of Lazarus.
cdecl is onderdeel van het functietype, geen versiering
PDFium is gecompileerde C. Op Win32 gebruiken de exports en, belangrijker nog, de callbacks die het aanroept de cdecl calling convention. Onder cdecl schoont de aanroeper de stack op nadat de aanroep retourneert. De standaardmethode van Delphi is register, en de Win32 C-standaard voor callbacks is in sommige bibliotheken stdcall, waarbij de aangeroepene in plaats daarvan de stack opschoont. Wanneer een structuur een functiepointer aan PDFium overhandigt en u de cdecl bij het type van die pointer vergeet, zijn de twee partijen het oneens over wie de stackpointer aanpast. Ofwel beiden doen het, ofwel geen van beiden, waardoor de stackpointer bij elke aanroep verschuift met de grootte van de argumenten.
De reden waarom deze fout moeilijk te vinden is, is dat de schade niet lokaal is. De gecorrumpeerde aanroep keert terug en lijkt in orde. De misuitlijning komt pas later aan het licht, in een ongerelateerde functie waarvan het frame zich nu bevindt op een stackpointer die een paar bytes afwijkt. Dit manifesteert zich als een onjuiste lezing, een ongeldig retouradres of een crash met een backtrace die in de verste verte niet wijst naar de callback die u daadwerkelijk verkeerd hebt geadresseerd. Form-fill is de klassieke plek waar dit problemen geeft, omdat de form-fill-interface een record is vol callbacks die PDFium aanroept. Een daarvan, FFI_OpenFile, geeft PDFium een functie die het aanroept om een extern bestand te openen, gedeclareerd als function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. De afsluitende cdecl is het detail om te onthouden. Laat deze weg en de code compileert, linkt en draait nog steeds correct, totdat PDFium de functie daadwerkelijk aanroept. De conventie hoort bij het functietype zelf. Het is geen optionele decoratie en de compiler waarschuwt niet als deze ontbreekt, omdat een eenvoudig functietype een volkomen legaal Pascal-type is. De enige verdediging is om de calling convention te behandelen als een verplicht veld van elke geïmporteerde signatuur en elke callback die u naar buiten doorgeeft.
size_t is pointer-breedte, en op FPC Win64 betekent dat 64 bits
De tweede fout is een discrepantie in de integer-breedte die zich op slechts één target voordoet. C's size_t is gedefinieerd als breed genoeg om elke objectgrootte te bevatten, wat op een 64-bits platform een 64-bits unsigned integer betekent. De progressief ladende interfaces van PDFium communiceren via byte-offsets in size_t. Het FX_FILEAVAIL-record van de availability provider bevat een IsDataAvail-callback die PDFium aanroept met een offset en een grootte, en de AddSegment-callback van het FX_DOWNLOADHINTS-record ontvangt hetzelfde. Beide parameters zijn size_t.
IsDataAvail = function(
pThis : PFX_FILEAVAIL;
offset, size: size_t): FPDF_BOOL; cdecl;
AddSegment = procedure(
pThis : PFX_DOWNLOADHINTS;
offset, size: size_t); cdecl;
Als u die offsets declareert als een 32-bits type, de binding werkt op Win32 en op Delphi Win64, maar breekt geruisloos op FPC en Lazarus Win64. De oorzaak is subtiel. Op FPC Win64 is NativeUInt een echt 64-bits type met pointer-breedte, en size_t is er een alias van. De binding bevat in de type-sectie een opmerking die waarschuwt tegen het overschrijven van NativeUInt op FPC, omdat het herdefiniëren naar een 32-bits alias daar size_t zou dwingen naar 32 bits en elke size_t-parameter die naar de bibliotheek wordt doorgegeven of erdoor wordt geschreven, zou corrumperen. Een 64-bits offset die aankomt bij een 32-bits parameter verliest de bovenste helft. Bij een klein bestand past elke offset in 32 bits en is er niets aan de hand. Bij een groot bestand wijst de afgekapte waarde, zodra een offset de grens van vier gigabyte passeert, naar een heel andere plek. PDFium vraagt dan of het verkeerde bytebereik beschikbaar is, waardoor het progressieve laden vastloopt of ongeldige gegevens leest. De fout is onzichtbaar totdat het bestand groot genoeg is en het target degene is waar size_t daadwerkelijk is verbreed.
Een Pascal-uitzondering mag nooit afwikkelen door een C-frame
De derde categorie betreft het uitzonderingsmodel, dat C niet kent. Wanneer PDFium een van uw callbacks aanroept, draait uw Pascal-code binnen een stack van C- en C++-frames die niets weten van Delphi's uitzonderingsmechanisme. Als uw callback een uitzondering veroorzaakt en deze laat propageren, wikkelt deze af door frames die daar nooit voor zijn gebouwd. PDFium's eigen opschoning wordt niet uitgevoerd, de interne invarianten blijven half bijgewerkt achter en het proces bevindt zich nu in een status die de bibliotheek nooit had voorzien. Het contract voor deze callbacks is een retourcode, geen uitzondering.
Twee callbacks maken dit concreet. FPDF_FILEWRITE is the sink waarin PDFium een opgeslagen document schrijft, en FPDF_FILEACCESS is de bron waaruit het een invoerdocument leest. Beide zijn hier geïmplementeerd over een Delphi TStream, en beide kunnen falen zoals elke stream faalt: de schijf raakt vol, de stream wordt onder uw voeten gesloten, of een leesactie loopt voorbij het einde. De write-callback verpakt de stream-write en zet elke fout om in PDFium's foutcode in plaats van deze te laten ontsnappen.
function WriteBlock(
pThis: PFPDF_FILEWRITE;
pData: Pointer;
Size : LongWord): Integer; cdecl;
begin
// PDFium treats any non-1 return as a write failure. A Pascal exception
// must not unwind through this cdecl/C++ frame, so trap it and report
// failure instead.
Result := 0;
try
PPdfWrite(pThis).Stream.WriteBuffer(pData^, Size);
Result := 1;
except
end;
end;
De lees-kant doet hetzelfde: een mislukte leesactie rapporteert nul om te voldoen aan het FPDF_FILEACCESS-contract, in plaats van een fout over de grens te genereren. Een kale except zonder re-raise voelt verkeerd voor een Pascal-programmeur die is getraind om nooit uitzonderingen te negeren, en in gewone Pascal is dat ook fout. Op een ABI-grens is dit echter de juiste vorm, omdat de enige veilige waarde om terug te geven aan de C-aanroeper een statuscode is die hij kan interpreteren. De fout propageert nog steeds, maar dan via de retourwaarde, en de aanroepende code boven de bibliotheek brengt deze naar de oppervlakte als EPdfError zodra de controle weer aan de Pascal-kant van de grens ligt.
Double-free verbergt zich in het foutpad
De vierde fout betreft eigendom. Een PDFium-documenthandle wordt geopend door de bibliotheek en moet exact één keer worden gesloten door FPDF_CloseDocument. Het gevaar is een foutpad dat een handle vrijgeeft die ook door een tweede opschoning wordt beheerd. Stel u een routine voor die een wrapper-object maakt, er een net geopende documenthandle aan toewijst en vervolgens verdere configuratie uitvoert die kan falen. Als die configuratie faalt, zal een early-return-handler die FPDF_CloseDocument aanroept op de rauwe handle deze sluiten, waarna de destructor van het wrapper-object deze opnieuw sluit wanneer het object wordt vrijgegeven. De handle wordt dan tweemaal vrijgegeven, wat ongedefinieerd gedrag is en waarschijnlijk leidt tot een crash.
De audit vond dit in een importpad in imposition-stijl dat een TPdf bouwt rond een reeds geopende handle. De oplossing is om van de eigendomsoverdracht de enige bron van waarheid te maken. Zodra de handle is toegewezen aan het veld van de wrapper, bezit de wrapper deze, en is de enige opschoning op het foutpad het vrijgeven van de wrapper. De destructor van de wrapper roept FPDF_CloseDocument voor u aan, dus een second expliciete sluiting zou hetzelfde document dubbel vrijgeven. De gecorrigeerde error-handler geeft het object vrij en genereert de fout opnieuw, waardoor er exact één pad naar de sluiting is.
Result := TPdf.Create(nil);
try
Result.FDocument := NewDoc; // Result now owns the handle
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free closes the handle. A second FPDF_CloseDocument(NewDoc)
// here would double-free the same PDFium document.
Result.Free;
raise;
end;
Beheerde records en een bibliotheek vol exports vereisen beide expliciete teardown
De laatste categorie gaat over geheugen dat de compiler namens u beheert, wat door een C-gewoonte geruisloos kan worden gecorrumpeerd. Veel helperfuncties van deze binding retourneren een record dat een WideString of een dynamische array bevat. Dit zijn velden met referentietelling, en de compiler genereert verborgen boekhouding om die tellingen bij te houden. Het instinct dat uit C is overgenomen, is om een nieuw record te wissen met FillChar(Result, SizeOf(Result), 0). Dat overschrijft de beheerde referentie in het record met nullen zonder deze eerst te verlagen. De compiler hergebruikt één verborgen tijdelijke variabele voor een functieresultaat over lus-iteraties heen, waardoor FillChar bij de tweede iteratie een actieve stringpointer overschrijft die nooit is vrijgegeven, met een stringlek als gevolg. Roept u de functie aan in een lus over duizend annotaties, dan lekt u duizend strings.
De oplossing is om de taal het record te laten wissen zoals deze dat kan, met Default(T), wat elk beheerd veld vrijgeeft alvorens het te zeroen.
// Default() instead of FillChar: the compiler reuses one hidden temp for
// the function result across loop iterations, so FillChar would zero live
// WideString pointers without releasing them.
Result := Default(TPdfAnnotation);
Een gerelateerd eigendomsprobleem bevindt zich op de grens van het laden van de bibliotheek. Deze binding resolvet honderden functiepointers uit de PDFium DLL met GetProcAddress na een LoadLibrary. Als één vereiste export ontbreekt, is de gedeeltelijk gebonden status gevaarlijk: tientallen pointers zijn geldig, the rest is nil of verouderd, en elke latere aanroep via een daarvan springt naar een module die mogelijk al is ontladen. De binding lost dit op door de bibliotheek te ontladen en een volledige ClearAllBindings uit te voeren die elke geïmporteerde pointer terugzet naar nil zodra een vereiste export niet kan worden geresolved. Daarna hangt er geen enkele functiepointer meer in een ontladen module, en mislukt een latere aanroep netjes met een nil-pointer-controle in plaats van te vertakken naar vrijgegeven code.
De wrapper is waar vier contracten handmatig opnieuw worden geformuleerd
Geen van deze vijf fouten is exotisch. Het zijn de voorspelbare storingsmodi van een dunne Pascal-laag over een C-API. Ze adviseren dat juist in die laag vier afzonderlijke contracten opnieuw moeten worden gedeclareerd. De calling convention moet worden gespeld als cdecl bij elke callback. De integer-breedte moet overeenkomen met size_t op het target waar deze daadwerkelijk verbreedt. Het uitzonderingsmodel moet bij elke callback die buiten Pascal treedt worden omgezet in retourcodes. Het eigendom van elke handle en elk beheerd veld moet eenmalig worden gedefinieerd en op elk pad worden gerespecteerd, inclusief de foutpaden die pas in productie worden doorlopen. Mist u er één, dan krijgt u een fout waarvan het symptoom zich ver van de oorzaak openbaart, wat deze categorie kopers waard is. De waarde van de audit lag minder in een specifieke oplossing, dan in het behandelen van elk van deze punten als een eigen discipline die over de hele binding moet worden gecontroleerd.
Als u de binding daadwerkelijk aan het werk wilt zien in plaats van alleen de randen te bewaken, tonen de render-cache- en zoomtechnieken in onze notitie over render-cache en zoomprestaties het renderingpad, en is de cross-compiler-handleiding over het bouwen van een Lazarus- en FPC-viewer de plek waar het hier beschreven Win64 size_t-gedrag daadwerkelijk van belang is. Beide bouwen voort op hetzelfde geheugenveiligheids- en ABI-werk dat wordt geleverd in de PDFium Component voor Delphi, Lazarus en C++Builder, naast de rendering-, tekstextractie- en formulier-API's die elders op deze blog worden behandeld.