Att klicka på Avbryt under ett utskriftsjobb i en PDFium-komponentvisare gjorde ibland ingenting: loopen fortsatte rendrera varje återstående sida och kopia, och jobbet nådde ändå skrivaren. Orsaken var Free Pascals for-loop, som fixerar sin övre gräns en gång vid loopingång, så att nollställa variabeln bakom CopyCount eller ToPage mitt i loopen ändrade ingenting som redan kördes. For-loop-gränsbuggen är den femte fällan att komma ut ur samma PDFiumPas Delphi/FPC-kompatibilitetsgranskning som producerade fyra andra korskompilator-fällor, och till skillnad från de fyra lever den helt inuti en visares utskriftsrutin — det krävdes en testare som höll Avbryt genom ett långt jobb för att faktiskt märka att skrivaren aldrig stannade
Varför fortsätter utskriftsjobbet efter att Avbryt klickats?
Utskriftsjobbet fortsatte eftersom avbrottskontrollen bara återställde variablerna som matade loopgränserna, inte loopar na själva, som Free Pascal redan låst fast när varje loop startade. SpeedButtonPrintClick-hanteraren bakom Skriv ut-knappen i PDFViewer- och MultiPageViewer-demona bygger varje jobb från tre kapslade loopar: en yttre loop över sammanhäftade kopiesatser, en mellersta loop över sidor, och en inre loop över osammanhäftade kopior av samma sida. PrintDialog.Collate avgör vilken räknare, CollateCopyCount eller CopyCount, som faktiskt håller det begärda antalet kopior, medan den andra stannar på ett. Att avbryta måste nå genom alla tre nivåer samtidigt, och den första versionen av den här koden försökte göra det genom att återställa själva gränsvariablerna i det ögonblick Cancel blev sant
for CollateCopy:= 1 to CollateCopyCount do
for Page:= FromPage to ToPage do
for Copy:= 1 to CopyCount do
begin
// ... render the page and send it to the printer ...
Application.ProcessMessages;
if Cancel then
begin
CollateCopyCount:= 0;
ToPage:= 0;
CopyCount:= 0;
end;
end;
Printer.EndDoc; // runs whether or not Cancel fired
Avsikten läses tydligt nog: om räknarna som definierar hur många sammanhäftningar, sidor, och kopior som återstår alla faller till noll, borde looparna slut på arbete och falla igenom av sig själva. Printer.EndDoc kördes sedan ovillkorligt efter loopen oavsett hur den slutade, så även ett jobb en användare trodde de hade stoppat skickades ändå till spooler med varje sida rendrerad innan klicket märktes
Vad Free Pascal låser fast när en for-loop startar
Free Pascal utvärderar en for-loops slutvärde exakt en gång, i det ögonblick loopen börjar, och aldrig igen under den loopens livstid. for Page := FromPage to ToPage do läser ToPage en enda gång för att beräkna hur många iterationer som ska köras, och efter det har loopen inget vidare intresse av en variabel vid namn ToPage, bara i iterationsantalet den redan fångat. Att sätta ToPage := 0 inifrån loop-kroppen ändrar en variabel den körande loopen inte längre konsulterar, vilket är precis varför Cancel kunde vara sant medan skrivaren fortsatte ta emot sidor för flera iterationer till, ibland alla
Det mönstret är en helt rimlig vana att ta med sig från C eller C++, och PDFium-komponentens C++Builder-visardemo speglar Pascal-versionen nära nog för att göra jämförelsen direkt. Dess for (Copy = 1; Copy <= CopyCount; Copy++) omtestar Copy <= CopyCount mot det levande värdet av CopyCount vid varje passering, så att nollställa räknaren där avslutar genuint loopen vid nästa kontroll. C++Builder-demot bar den identiska nollställ-räknarna-koden och den fungerade, vilket är precis vad som fick samma idé att se säker ut att återanvända i Pascal-byggnationen sittande precis bredvid den i samma repository
Varför drabbades inte Delphi-byggnationen av samma bugg?
Delphi PDFViewer-demot förlitade sig aldrig på att en loop skulle märka en ändrad gräns, eftersom dess avbrottsväg reder ut sig genom ett undantag istället för en jämförelse. Dess innersta loop anropar RTL:s Abort-procedur i det ögonblick Cancel blir sant, vilket kastar ett tyst EAbort som fortplantar sig rakt igenom alla tre kapslade for-loopar till en hanterare omslutande hela utskriftsblocket
Printer.BeginDoc;
try
for CollateCopy:= 1 to CollateCopyCount do
for Page:= FromPage to ToPage do
for Copy:= 1 to CopyCount do
begin
// ... render the page and send it to the printer ...
Application.ProcessMessages;
if Cancel then
Abort; // raises EAbort, unwinds all three loops at once
end;
Printer.EndDoc;
except
on E: EAbort do
Printer.Abort;
else
begin
Printer.Abort;
raise;
end;
end;
Ett undantag bryr sig inte om hur många for-loopar som skiljer punkten där det kastas från hanteraren som fångar det, vilket är precis egenskapen det här problemet behöver. Den robustheten var inget medvetet försvar mot gränslåsningsbeteendet som beskrivs ovan — Delphi-demots författare grep helt enkelt efter ett annat verktyg. Undantagsbaserade utvägen är fortfarande värd att namnge som det stabilare mönstret: det överlever att en fjärde kapslingsnivå läggs till senare, medan en kedja av manuellt placerade Break-satser måste kommas ihåg och läggas till på nytt varje gång loopkapslingen ändras
Fixen: Break på varje kapslingsnivå, grindad av en PrintSucceeded-flagga
Fixen som levererades i PDFiumPas v2.27.0 behåller de tre looparnas struktur i Lazarus- och C++Builder-visardemona men gör avbrytandet explicit på varje nivå, och den separerar loopens stoppning från jobbets inlämning till en flagga som bara läses efter att loopen helt avslutats
Printer.BeginDoc;
try
Cancel:= False;
for CollateCopy:= 1 to CollateCopyCount do
begin
for Page:= FromPage to ToPage do
begin
for Copy:= 1 to CopyCount do
begin
// ... render the page and send it to the printer ...
Application.ProcessMessages;
if Cancel then
Break;
end;
if Cancel then
Break;
end;
if Cancel then
Break;
end;
PrintSucceeded:= not Cancel;
finally
if PrintSucceeded then
Printer.EndDoc
else
Printer.Abort;
end;
PrintSucceeded beräknas medvetet en gång, omedelbart efter att den trippelt kapslade loopen avslutas, från inget mer än not Cancel. Inget inuti loop-kroppen får bestämma på egen hand om jobbet lyckades — loopen kan bara sluta på två sätt, ta slut på sammanhäftningar, sidor, och kopior, eller träffa Break-kedjan som Cancel utlöser, och PrintSucceeded läser utfallet i efterhand snarare än att spåra det medan loopen går. Att beräkna det på det sättet är vad som gör finally-blockets val mellan Printer.EndDoc och Printer.Abort pålitligt: det utlöses aldrig innan loopen faktiskt har lugnat sig
Att granska dina egna avbrottsdrivna utskriftsloopar
Tre kontroller reser långt bortom just den här utskriftsrutinen. Anta aldrig att en Pascal for-loop kommer märka att en gränsvariabel ändras efter att den startat; om en loop behöver sluta tidigt, säg det direkt med Break, på varje kapslingsnivå avbrottet behöver korsa, inte bara den innersta. Föredra en utvägsmekanism som reder ut sig genom konstruktion, som ett undantag, när kapslingen väl är djup nog att en missad Break är trolig — Delphi-demots Abort/EAbort-par fick det gratis. Grinda alla commit-steg som EndDoc bakom en flagga beräknad strikt efter loopen, aldrig inuti den, så att ett jobb som stoppades tidigt aldrig kan misstas för ett som avslutades
Samma granskning som fixade den här loopen stramade också åt hur de nio visardemona hanterar tangentbordet medan en avbryt-knapp är synlig: de sväljer nu varje tangent utom Esc, så ett förlupet Ctrl+P eller Ctrl+F under en aktiv utskrift eller sökning kan inte längre starta en andra ovanpå den. Den här reentrancy-fixen är en annan bugg med en annan mekanism, men den kom ut ur samma granskning av vad Cancel faktiskt gör mitt i en operation. Vanan värd att ta med sig från båda fixarna är mer en testvana än en kodningsvana: utöva avbrytande på varje kompilator en delad utskriftsrutin faktiskt levereras till, eftersom en idé som att rensa räknarna och lita på att loopen märker det kan överleva testning under C++Builder, nå en Free Pascal-byggnation overifierad, läsas identiskt i källkoden, och misslyckas på ett sätt ingen ser förrän någon håller Avbryt nedtryckt tillräckligt länge för att se jobbet slutföras ändå. För utskriftsuppsättningen den här avbrottslogiken sitter ovanpå täcker genomgången om att skriva ut PDF-dokument med PDFium VCL-komponenten resten
Den här utskriftsavbrottsfixen levererades som en del av PDFium-komponenten för Delphi, C++Builder, och FPC/Lazarus, som kör samma demosvit över alla tre kompilatorer vid varje release så luckor som den här fångas av en byggmatris snarare än ett supportärende