En Pascal-binding over et C-bibliotek leses som vanlig Pascal. Du kaller en metode, du får en post (record) tilbake, og du frigjør det du tildelte. Problemet er at PDFium er et C- og C++-bibliotek med sin egen kallkonvensjon, sine egne heltallsbredder og sine egne regler om hvem som eier minnet og hvem som frigjør det. Ingenting av dette krysser språkgrensen av seg selv. Hver enkelt av disse kontraktene må gjentas for hånd i Pascal-deklarasjonene, og et enkelt feil ord forvandler et tilsynelatende ryddig kall til en stabelkorrupsjon, en avkortet forskyvning eller en dobbel frigjøring. En v1.61.0-revisjon av en PDFium VCL-binding avdekket én feil av hver type. De er verdt å gå gjennom fordi de ikke er spesifikke for denne bindingen. De er de konstante farene ved å pakke inn ethvert C-API i Delphi eller Lazarus.
cdecl er en del av funksjonstypen, ikke en dekorasjon
PDFium er kompilert C. På Win32 bruker eksportene dens, og enda viktigere, tilbakekallene den kaller, kallkonvensjonen cdecl. Under cdecl rydder kaller opp stakken etter at kallet returnerer. Delphis opprinnelige standard er register, og Win32 C-standarden for tilbakekall er stdcall i noen biblioteker, der den som blir kalt rydder opp i stedet. Når en struktur gir PDFium en funksjonspeker og du glemmer cdecl på pekerens type, er de to sidene uenige om hvem som justerer stakkelpekeren. Enten retter begge det, eller så gjør ingen det, og stakkelpekeren driver med størrelsen på argumentene for hvert kall.
Grunnen til at denne feilen er vanskelig å finne, er at skaden ikke er lokal. Det korrupte kallet returnerer og ser fint ut. Feiljusteringen dukker opp senere, i en helt urelatert funksjon der rammen nå sitter på en stakkelpeker som er noen få byte feil, og det manifesterer seg som en vill lesing, en dårlig returadresse eller et krasj med en backtrace som peker milevis unna tilbakekallet du faktisk gjorde feil. Skjemautfylling er det klassiske stedet dette slår til, fordi grensesnittet for skjemautfylling er en post full av tilbakekall som PDFium kaller tilbake til. Ett av dem, FFI_OpenFile, gir PDFium en funksjon den skal kalle for å åpne en ekstern fil, deklarert som function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. Den avsluttende cdecl er poenget som er verdt å merke seg. Utelat den, og koden kompilerer fortsatt, lenker fortsatt, og kjører helt til PDFium kaller funksjonen. Konvensjonen tilhører selve funksjonstypen. Den er ikke valgfritt sukker, og kompilatoren vil ikke advare deg når den mangler, fordi en vanlig funksjonstype er en helt lovlig Pascal-type. Det eneste forsvaret er å behandle kallkonvensjonen som et obligatorisk felt i enhver importert signatur og ethvert tilbakekall du sender utover.
size_t er pekerbredde, og på FPC Win64 betyr det 64 bits
Den andre feilen er en uoverensstemmelse i heltallsbredde som bare vises på én plattform. C-typens size_t er definert til å være bred nok til å holde enhver objektstørrelse, noe som på en 64-bit plattform betyr et 64-bit unsigned heltall. PDFiums grensesnitt for progressiv lasting snakker i size_t byte-forskyvninger. Tilgjengelighetsleverandørens FX_FILEAVAIL-post bærer et IsDataAvail-tilbakekall som PDFium kaller med en forskyvning og en størrelse, og FX_DOWNLOADHINTS-postens AddSegment-tilbakekall mottar det samme. Begge parametere er size_t.
IsDataAvail = function(
pThis : PFX_FILEAVAIL;
offset, size: size_t): FPDF_BOOL; cdecl;
AddSegment = procedure(
pThis : PFX_DOWNLOADHINTS;
offset, size: size_t); cdecl;
Hvis du deklarerer disse forskyvningene som en 32-bit type, bindingen fungerer på Win32 og på Delphi Win64, for så å feile i det stille på FPC og Lazarus Win64. Årsaken er subtil. På FPC Win64 er NativeUInt en reell pekerbredde 64-bit type, og size_t er aliasert til den. Bindingen har en kommentar i typeseksjonen som advarer spesifikt mot å overskygge NativeUInt på FPC, fordi å redefinere den til et 32-bit alias der ville tvinge size_t to 32 bits og korrumpere alle size_t-parametere som sendes til eller skrives av biblioteket. En 64-bit forskyvning som ankommer en 32-bit parameter, mister sin øvre halvdel. For en liten fil passer hver forskyvning i 32 bits og ingenting er feil. For en stor fil, i det øyeblikket en forskyvning krysser fire gigabyte-grensen, peker den avkortede verdien et helt annet sted, PDFium spør om feil byteområde er tilgjengelig, og progressiv lasting stopper opp eller leser søppel. Feilen er usynlig til filen er stor nok og målet er det der size_t faktisk utvidet seg.
Et Pascal-unntak må aldri rulle tilbake gjennom en C-ramme
Den tredje klassen handler om unntaksmodellen, som C ikke har. Når PDFium kaller et av dine tilbakekall, kjører Pascal-koden din inne i en stakk av C- og C++-rammer som ikke vet noe om Delphis unntakssystem. Hvis tilbakekallet ditt utløser en feil og lar unntaket forplante seg, det ruller tilbake gjennom rammer som aldri ble bygget for å rulles tilbake. PDFiums egen opprydding kjører ikke, dens interne invarianter forblir halvveis oppdatert, og prosessen er nå i en tilstand biblioteket aldri har forutsett. Kontrakten for disse tilbakekallene er en returkode, ikke et unntak.
To tilbakekall gjør dette konkret. FPDF_FILEWRITE er mottaket PDFium skriver et lagret dokument til, og FPDF_FILEACCESS er kilden den leser et inndatadokument fra. Begge er implementert her over en Delphi TStream, og begge kan feile slik alle strømmer feiler: Disken blir full, strømmen lukkes under deg, eller en lesing går forbi slutten. Tilbakekallet for skriving pakker inn strømskrivingen sin og gjør enhver feil om til PDFiums feilkode i stedet for å la den unnslippe.
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;
Lesesiden gjør det samme: En feilet lesing rapporterer null for å samsvare med kontrakten til FPDF_FILEACCESS i stedet for å utløse et unntak over grensen. En tom except uten re-raise ser feil ut for en Pascal-programmerer som er opplært til aldri å svelge unntak, og i vanlig Pascal er det feil. Ved en ABI-grense er det den riktige formen, fordi den eneste sikre verdien å gi tilbake til C-kalleren er en statuskode den vet hvordan den skal tolke. Feilen forplanter seg fortsatt, bare via returverdien, og den kallende koden over biblioteket viser den som EPdfError når kontrollen er tilbake på Pascal-siden av gjerdet.
Dobbel frigjøring gjemmer seg på feilbanen
Den fjerde feilen er eierskap. Et PDFium-dokumenthåndtak åpnes av biblioteket og må lukkes nøyaktig én gang, av FPDF_CloseDocument. Faren er en feilbane som frigjør et håndtak en annen opprydding også eier. Se for deg en rutine som oppretter et innpakningsobjekt, tilordner et nylig åpnet dokumenthåndtak til det, og deretter gjør mer oppsett som kan feile. Hvis oppsettet feiler og kaster et unntak, vil en tidlig returhåndterer som kaller FPDF_CloseDocument på det rå håndtaket lukke det, og deretter vil innpakningsobjektets egen destruktør lukke det igjen når objektet frigjøres. Håndtaket frigjøres to ganger, noe som er udefinert oppførsel og et sannsynlig krasj.
Revisjonen fant dette på en importbane av imposisjonstype som bygger en TPdf rundt et allerede åpent håndtak. Løsningen er å gjøre eierskapsoverdragelsen til den eneste kilden til sannhet. Når håndtaket er tildelt innpakningsobjektets felt, innpakningsobjektet eier det, og den eneste oppryddingen på feilbanen er å frigjøre innpakningsobjektet. Innpakningsobjektets destruktør kaller FPDF_CloseDocument for deg, så en ny eksplisitt lukking ville dobbelt-frigjøre det samme dokumentet. Den korrigerte feilhåndtereren frigjør objektet og sender unntaket videre, og det er nøyaktig én bane til lukkingen.
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;
Administrerte poster og et bibliotek fullt av eksporter trenger begge eksplisitt nedstenging
Den siste klassen handler om minne kompilatoren administrerer på dine vegne, som en vane fra C rolig vil korrumpere. Mange av denne bindingens hjelpefunksjoner returnerer en post som inneholder en WideString eller en dynamisk array. Dette er referansetalte felt, og kompilatoren sender ut skjult bokføring for å opprettholde tellingene deres. Instinktet som overføres fra C, er å tømme en ny post med FillChar(Result, SizeOf(Result), 0). Det stempler nuller over den administrerte referansen i posten uten å dekrementere den først. Kompilatoren gjenbruker én skjult midlertidig variabel for et funksjonsresultat på tvers av løkkeiterasjoner, så på den andre iterasjonen overskriver FillChar en levende strengpeker som aldri ble frigjort, og strengen den pekte på lekker. Hvis du kaller funksjonen i en løkke over tusen annoteringer, lekker du tusen strenger.
Løsningen er å la språket tømme posten slik det vet hvordan, med Default(T), som frigjør ethvert administrert felt før det nullstilles.
// 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);
Et beslektet eierskapsproblem lever ved grensen for lasting av biblioteker. Denne bindingen løser flere hundre funksjonspekere ut av PDFium-DLL-en med GetProcAddress etter en LoadLibrary. Hvis én påkrevd eksport mangler, den delvis bundne tilstanden er farlig: Dusinvis av pekere er gyldige, resten er nil eller foreldet, og ethvert senere kall gjennom en av dem hopper inn i en modul som kanskje allerede er avlastet. Bindingen håndterer dette ved å avlaste biblioteket og kjøre en fullstendig ClearAllBindings som tilbakestiller hver importert peker til nil hver gang en påkrevd eksport ikke kan løses. Etter det dingler ingen funksjonspekere inn i en avlastet modul, og et senere kall feiler rent med en nil-peker-sjekk i stedet for å hoppe inn i frigitt kode.
Innpakningen er der fire kontrakter gjentas for hånd
Ingen av disse fem feilene er eksotiske. De er de forutsigbare feilmodusene til et tynt Pascal-lag over et C-API, og de hoper seg opp fordi dette laget er akkurat der fire separate kontrakter må re-deklareres. Kallkonvensjonen må staves cdecl på hvert tilbakekall. Heltallsbredden må samsvare med size_t på det ene målet der den faktisk utvides. Unntaksmodellen må konverteres til returkoder ved hvert tilbakekall som krysser ut av Pascal. Eierskapet til hvert håndtak og hvert administrert felt må fastslås én gang og følges på alle baner, inkludert feilbanene ingen tester før produksjon. Glemmer du én, får du en feil hvis symptom dukker opp langt unna årsaken, noe som er det som gjør denne kategorien kostbar. Revisjonens verdi lå mindre i en enkelt rettelse enn i å behandle hver av disse som sin egen disiplin som må sjekkes over hele bindingen.
Hvis du vil se bindingen gjøre faktisk arbeid i stedet for bare å vokte kantene sine, teknikkene for rendringsbuffer og zoom i vårt notat om rendringsbuffer og zoom-ytelse viser rendringsbanen, og gjennomgangen av krysskompilering i å bygge en Lazarus- og FPC-viser er stedet der Win64-oppførselen for size_t beskrev her faktisk betyr noe. Begge bygger på det samme arbeidet med minnesikkerhet og ABI som leveres i PDFium-komponenten for Delphi, Lazarus og C++Builder sammen med API-ene for rendring, tekstuttrekking og skjemaer som er dekket andre steder på denne bloggen.