Når en Delphi constructor udløser en exception, kalder RTL'en straks destructoren på den halvbyggede instans, så hver destructor skal kunne håndtere felter, der aldrig blev tildelt. HotXLS anvender den regel på sine destructors, TXLSXRichText og TXLSXCells inkluderet: en allokeringsfejl inde i de constructors når din kode som den oprindelige EOutOfMemory, ikke som en sekundær access violation udløst under oprydningen
Fejlklassen er gammel og let at overse. En service konverterer en 300 MB workbook på en travl server, hukommelsen løber tør, og loggen viser en access violation inde i en destructor. Intet ved den besked siger "out of memory", intet antyder, at et forsøg senere ville lykkes, og stack trace'en peger på en destructor, der ser helt korrekt ud. Den oprindelige exception eksisterede i et par mikrosekunder og blev så erstattet. Denne artikel gennemgår, hvorfor det sker, hvordan HotXLS strukturerer sine destructors for at undgå det, og hvordan du skriver dine egne klasser, så de aldrig gør det
Hvad sker der, når en Delphi constructor udløser en fejl?
Objektet destrueres for dig, før exceptionen når den kaldende, og din destructor køres mod den tilstand, constructoren nåede. Sekvensen er fastlagt af compileren og RTL'en. NewInstance allokerer instansen og InitInstance nulfylder den, så hver objektreference starter som nil, hvert heltal som 0 og hver streng som tom. Constructor-body'en køres derefter inde i en implicit exception-handler. Udløser noget i den body en fejl, kalder den genererede kode den virtuelle destructor: BeforeDestruction køres, så din Destroy, så FreeInstance frigiver hukommelsen, og til sidst udløses exceptionen igen. AfterConstruction køres aldrig, og variablen på venstre side af X := TFoo.Create tildeles aldrig
Destructoren ser altså et objekt, der stoppede ved en vilkårlig linje i sin constructor. Felter tildelt før fejlen holder rigtige objekter. Felter efter den er stadig nil. En destructor skrev ud fra den antagelse, at konstruktionen blev færdig, vil dereferere et af de nil-felter og udløse sin egen access violation
Den anden exception er den reelle skade. Delphi kæder ikke exceptions, der udløses under denne oprydning: access violationen erstatter EOutOfMemory, den oprindelige exception kasseres, og den kaldende ser kun AV'en. Den nyttige diagnose, "en allokering fejlede, prøv igen med mere hukommelse", er væk, og i stedet er der noget, der ligner hukommelseskorruption
Hvor kommer den sekundære access violation fra?
Den kommer fra en destructor, der antager, at dens constructor blev færdig, og rører et listfelt, der er nil, når den allerførste interne allokering fejler. To HotXLS-klasser viser formen tydeligt. Både den selvstændige TXLSXRichText.Create og den comment-ejede overload TXLSXRichText.Create(AOwner: TXLSXComment) kalder inherited Create og allokerer derefter FRuns := TList.Create. TXLSXCells.Create(AOwnerSheet: TXLSXWorksheet) gør det samme med FList := TList.Create, før den bygger sine packed-string-tabeller og packed cellelager. I Lib/lxHandleX.pas sikrer de to destructors de felter:
procedure TXLSXRichText.ClearCore;
var
i: Integer;
begin
if FRuns=nil then Exit; // constructoren kan være stoppet, før FRuns
for i:= 0 to FRuns.Count- 1 do
TXLSXRichTextRun(FRuns[i]).Free;
FRuns.Clear;
end;
destructor TXLSXCells.Destroy;
begin
FPackedStorageEnabled:= False;
if FList<>nil then Clear; // FList er nil, hvis TList.Create fejlede
// ... packed storage, strengtabeller og FList frigives med Free
inherited Destroy;
end;
Fejlpunktet er præcist. Tæller du heap-allokeringer under konstruktionen, er den første instansen selv; fejler NewInstance, køres ingen constructor-body, og ingen destructor kaldes, så det tilfælde er altid rent. Den anden allokering er TList. Fejler den, forbliver FRuns eller FList nil, RTL'en kalder Destroy, og en usikret Destroy læser Count gennem en nil-reference, hvilket er præcis den sekundære access violation. Senere fejl inde i TXLSXCells.Create er harmløse, for når FList findes, tjekker Clear FPackedStorage <> nil, før det packede lager bruges
Disse klasser betyder noget, for de oprettes i bulk. Hvert loadet worksheet bygger en TXLSXCells-samling, og hver celle eller comment med formaterede runs bygger en TXLSXRichText. En stor workbook med tung rich text opretter tusindvis af dem, så under hukommelsestryk har den allokering, der til sidst fejler, en reel chance for at lande på én af disse linjer
Free er nil-sikker, feltadgang er ikke
TObject.Free tjekker Self <> nil, før den kalder Destroy, så FRuns.Free på et nil-felt er en no-op. At læse FRuns.Count er ikke: Count læser et felt på en offset fra instanspointeren, og et virtuelt kald som FRuns.Clear læser først VMT-pointeren fra instansen. Begge fejler på nil. Den asymmetri er hele fælden. Frigivelseskaldene i en typisk destructor er allerede sikre; løkkerne og oprydningskaldene placeret før dem er der, hvor problemerne bor. Her er mønsteret i en lille klasse af din egen:
type
TStyledText = class
private
FRuns: TList;
FIndex: TStringList;
public
constructor Create;
destructor Destroy; override;
end;
constructor TStyledText.Create;
begin
inherited Create;
FRuns := TList.Create; // EOutOfMemory her efterlader FRuns = nil
FIndex := TStringList.Create; // ... og her efterlades FIndex = nil
end;
destructor TStyledText.Destroy; // DÅRLIGT: antager, at constructoren blev færdig
var
I: Integer;
begin
for I := 0 to FRuns.Count - 1 do // access violation når FRuns = nil
TObject(FRuns[I]).Free;
FIndex.Free;
FRuns.Free;
inherited Destroy;
end;
Den korrigerede destructor sikrer hver dereference og frigiver felter kun gennem nil-sikre kald:
destructor TStyledText.Destroy;
var
I: Integer;
begin
if FRuns <> nil then
for I := 0 to FRuns.Count - 1 do
TObject(FRuns[I]).Free;
FreeAndNil(FIndex);
FreeAndNil(FRuns);
inherited Destroy;
end;
De regler, der falder ud af det, er korte nok til at bruge i kode review:
- Enhver brug af et ejet felt i en destructor, ud over
FreeellerFreeAndNil, behøver etnil- ellerAssigned-tjek, inklusive kald ind i hjælpere somClearellerClearCore, der itererer feltet - Frigiv ejede objekter med
FreeellerFreeAndNil, aldrig ved at kaldeDestroydirekte, hvilket springernil-tjekket over - Hold
inherited Destroysom den sidste sætning, så forfaderen river sine egne felter ned efter dine er væk - Resonnér ikke ud fra feltrækkefølgen i constructoren; den næste vedligeholder indsætter en allokering over din og flytter lydløst fejlpunktet
Hvor inherited Create hører hjemme
Kald inherited Create først, som begge HotXLS-constructors gør. Forfaderens felter findes så, før din kode kører, og destructorens spejlbillede, inherited Destroy sidst, vikles op i omvendt rækkefølge. Udløser forfader-constructoren selv en fejl, kører din destructor stadig med alle dine felter nil, hvilket en nil-sikker destructor håndterer uden specialtilfælde. Eksplicitte tildelinger som FOwner := nil i en constructor er overflødige efter InitInstance, men harmløse og nogle gange værd at beholde som dokumentation
Fang ikke og frigiv ikke inde i constructoren
To velmente mønstre gør det værre. Det første er at pakke constructor-body'en ind i try ... except Free; raise; end: RTL'en kommer allerede til at kalde destructoren, så objektet destrueres to gange. Det andet er at synke exceptionen for at "komme videre": den kaldende modtager så en plausibel reference til et objekt med nil-felter, og fejlen flytter sig til et helt urelateret metodekald meget senere. Opretter en constructor en midlertidig, den ikke beholder, så frigiv den midlertidige i en lokal try ... finally og lad exceptionen fortsætte opad urørt
Hvordan tester man constructor-fejlveje?
Fejl hvert allokeringsspunkt i en constructor med vilje, ét ad gangen, og kræv EOutOfMemory hver gang, efterfulgt af en succesfuld normal konstruktion. Værktøjet til det er en erstattende memory manager, hvis GetMem, AllocMem og ReallocMem tæller anmodninger med positiv størrelse og returnerer nil ved et valgt nummer. Tæl først de allokeringer, en constructor laver på dit target-build, og kør så én injiceret fejl pr. nummer
Efter hver injiceret fejl, konstruér samme klasse igen med allocatoren gendannet og brug den: til en rich text-klasse, tilføj et run, sæt dets tekst og bekræft, at den rene tekst kommer tilbage som forventet. Normale livscyklus-tjek hører ved siden af, som at gemme en workbook med formaterede runs og comments til fil og stream, frigive kilde-workbooken og genåbne og sammenligne begge kopier
Allokeringsantal gælder én compiler og platform; en anden compiler eller RTL kan allokere anderledes, så mål på det build, du udsender. HotXLS anvender samme nil-sikre regel på tværs af biblioteket, fra style-, chart- og pivot-samlinger til hash-tabellerne, zip-writeren og PDF-eksportøren. Hash-tabeller fortjener en specifik bemærkning: en tabel, der skifter maske før en reallokering, der så fejler, efterlader sin destructor til at gå i hukommelsen forbi den gamle tabel, så masken må først skifte, efter det nye lager findes. Ét tilfælde sidder i RTL'en selv: TInterfaceList fejler i sin destructor, når dens interne allokering fejler. Intet af dette udgør rollback af en vilkårlig operation, der fejler halvvejs, og det er ikke en generel out-of-memory-gendannelsesgaranti
Hvad ændrer det for kode, der åbner store workbooks?
I praksis er EOutOfMemory fra disse konstruktionsveje et signal, du kan fange og handle på, i stedet for en access violation, du er nødt til at behandle som et crash. TXLSXWorkbook.Create tager ingen parametre, og TXLSXWorkbook.Open(const FileName: WideString): Integer returnerer 1 ved succes og -1 ved fejl. Exceptions, der udløses under indlæsning, noteres i workbookens Diagnostics-samling som xlsDiagnosticUnhandledException og udløses derefter igen, hvilket er adfærden beskrevet i HotXLS-artiklen om struktureret diagnostik. En retry-wrapper ser sådan ud:
type
EWorkbookOpenFailed = class(Exception);
function OpenWorkbookWithRetry(const FileName: WideString;
MaxAttempts: Integer): TXLSXWorkbook;
var
Attempt: Integer;
Book: TXLSXWorkbook;
begin
Result := nil;
for Attempt := 1 to MaxAttempts do
begin
Book := nil;
try
try
Book := TXLSXWorkbook.Create; // ingen constructor-argumenter
if Book.Open(FileName) <> 1 then // 1 = succes, -1 = fejl
raise EWorkbookOpenFailed.CreateFmt('HotXLS could not open %s',
[string(FileName)]);
Result := Book;
Book := nil; // ejerskabet går til den kaldende
Exit;
finally
Book.Free; // nil-sikker efter en overdragelse
end;
except
on E: EOutOfMemory do
begin
if Attempt = MaxAttempts then
raise;
ReleaseApplicationCaches; // din kode: dropp caches, andre books
end;
end;
end;
end;
Et par detaljer i den wrapper er bevidste. Hvert forsøg bygger en frisk TXLSXWorkbook, for nil-sikre destructors gør konstruktionsfejl rene, men gør ikke en halvfærdig Open genoptagelig, så den fejlede instans frigives simpelthen. Kun EOutOfMemory genprøves; EWorkbookOpenFailed, passwordfejl og alt andet propagerer ved første forsøg. En EAccessViolation skal aldrig genprøves overhovedet, da den signalerer en defekt frem for en ressourcemangel. Og genprøvningen giver kun mening, hvis noget ændrer sig mellem forsøgene, og derfor frigiver handleren hukommelse først. Har du brug for diagnosessporet fra det fejlede forsøg, så læs Book.Diagnostics, før finally-blokken frigiver den
Genprøvning er en afbødning, ikke en kapacitetsplan. En 32-bit proces, der vedvarende rammer EOutOfMemory på store filer, mangler normalt sammenhængende adresserum frem for fysisk hukommelse, og den holdbare løsning er et Win64-build. Samme tankegang gælder på skrivesiden, hvor HotXLS crash-sikre gemninger med stagede temp-filer forhindrer en fejlet gemning i at beskadige den eksisterende fil. Deler flere tråde én workbook, beskriver HotXLS read leases og write guards, hvordan adgangen til den instans koordineres
En tjekliste til dine egne Delphi-klasser
Sikringerne selv er én linje hver, og samme audit tager minutter på din egen kode. Kig efter destructors, der kalder en Clear-, Reset- eller ClearCore-metode, før felter frigives, og tjek derefter, om den metode rører et felt, constructoren måske ikke nåede. Søg efter løkker over ejede lister uden et forudgående nil-tjek. Kig efter constructors, der fanger exceptions, og fjern både synkningen og den manuelle Free. Vil du have bevis i stedet for inspektion, passer den tællende memory manager beskrevet ovenfor på en side kode og gør hver allokering i en constructor til en testcase
Til det fulde featuresæt, understøttede Delphi- og C++Builder-versioner og licensering, se HotXLS Delphi Excel components productsiden