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. Elk contract hieronder komt met de klassieke manier waarop het misgaat en met de aanpak die de PDFium Component-binding daarvoor in de plaats kiest. 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 valkuil 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 het doel waarnaar 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 beschouwt elke niet-1-retourwaarde als een schrijffout. Een Pascal-uitzondering
// mag niet door dit cdecl/C++-frame afwikkelen, dus vang deze af en rapporteer
// in plaats daarvan een fout.
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 loert op het foutpad
De vierde valkuil 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
Het patroon is het belangrijkst op een importpad in imposition-stijl dat een TPdf bouwt rond een al geopende handle. Het middel is om eigendomsoverdracht de enige bron van waarheid te maken. Zodra de handle aan het veld van de wrapper is toegewezen, is de wrapper de eigenaar, en de enige opschoning op het foutpad is het vrijgeven van de wrapper. De destructor van de wrapper roept FPDF_CloseDocument voor u aan, dus een tweede expliciete sluiting zou hetzelfde document dubbel vrijgeven. De foutafhandeling hieronder geeft het object vrij en gooit de uitzondering opnieuw, en er is precies één pad naar de sluiting
Result := TPdf.Create(nil);
try
Result.FDocument := NewDoc; // Result is nu eigenaar van de handle
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free sluit de handle. Een tweede FPDF_CloseDocument(NewDoc)
// zou hier hetzelfde PDFium-document dubbel vrijgeven.
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 op nul te zetten
// Default() in plaats van FillChar: de compiler hergebruikt één verborgen tijdelijke variabele voor
// het functieresultaat over lus-iteraties heen, waardoor FillChar actieve
// WideString-pointers zou nulleren zonder ze vrij te geven.
Result := Default(TPdfAnnotation);
Een gerelateerd eigendomsprobleem bevindt zich op de grens van het laden van de bibliotheek. Deze binding resolveert 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, de 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 valkuilen is exotisch. Het zijn de voorspelbare storingsmodi van een dunne Pascal-laag rond een C-API, en ze stapelen zich op omdat die laag precies de plek is waar vier aparte contracten opnieuw moeten worden gedeclareerd. De calling convention moet bij elke callback als cdecl worden uitgeschreven. De integer-breedte moet overeenkomen met size_t op het ene target waar hij daadwerkelijk verbreedt. Het uitzonderingsmodel moet bij elke callback die Pascal verlaat worden omgezet in retourcodes. Het eigendom van elke handle en elk beheerd veld moet één keer worden vastgelegd en op elk pad worden nageleefd, inclusief de foutpaden die niemand doorloopt totdat het in productie misgaat. Mist u er één, dan krijgt u een fout waarvan het symptoom ver van de oorzaak opduikt, en dat is wat deze categorie duur maakt. De waarde ligt minder in één enkele regel dan in het behandelen van elk van deze punten als een eigen discipline die over de hele binding wordt 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