Å klikke Avbryt under en utskriftsjobb i en PDFium-komponent-fremviser gjorde noen ganger ingenting: løkken fortsatte å gjengi hver gjenværende side og kopi, og jobben nådde likevel skriveren. Årsaken var Free Pascals for-løkke, som fikserer sin øvre grense én gang ved løkkeinngang, så å nullstille variabelen bak CopyCount eller ToPage midt i løkken endret ingenting som allerede kjørte. For-løkke-grense-bugen er den femte fallgruven som kom ut av den samme PDFiumPas Delphi/FPC-kompatibilitetsrevisjonen som produserte fire andre kryss-kompilator-fallgruver, og i motsetning til de fire, bor den fullstendig inne i en fremvisers utskriftsrutine — det krevde en tester som holdt Avbryt gjennom en lang jobb, for faktisk å legge merke til at skriveren aldri stoppet
Hvorfor fortsetter utskriftsjobben etter at Avbryt klikkes?
Utskriftsjobben fortsatte fordi avbrytelsessjekken bare tilbakestilte variablene som matet løkkegrensene, ikke selve løkkene, som Free Pascal allerede hadde låst inn da hver løkke startet. SpeedButtonPrintClick-håndtereren bak Print-knappen i PDFViewer- og MultiPageViewer-demoene bygger hver jobb fra tre nøstede løkker: en ytre løkke over sammenstilte kopisett, en midtre løkke over sider, og en indre løkke over usammenstilte kopier av samme side. PrintDialog.Collate avgjør hvilken teller, CollateCopyCount eller CopyCount, som faktisk holder det forespurte antallet kopier, mens den andre forblir på én. Avbrytelse må rekke gjennom alle tre nivåene på én gang, og den første versjonen av denne koden prøvde å gjøre det ved å tilbakestille selve grensevariablene i det øyeblikket Cancel ble sann
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
Hensikten leser klart nok: hvis tellerne som definerer hvor mange sammenstillinger, sider, og kopier som gjenstår, alle faller til null, burde løkkene gå tom for arbeid og falle gjennom av seg selv. Printer.EndDoc kjørte deretter ubetinget etter løkken uansett hvordan den endte, så selv en jobb en bruker trodde de hadde stoppet, ble likevel sendt til spooleren med hver side gjengitt før klikket ble lagt merke til
Hva Free Pascal låser inn når en for-løkke starter
Free Pascal evaluerer en for-løkkes sluttverdi nøyaktig én gang, i det øyeblikket løkken begynner, og aldri igjen gjennom levetiden til den løkken. for Page := FromPage to ToPage do leser ToPage én enkelt gang for å beregne hvor mange iterasjoner som skal kjøres, og etter det har løkken ingen videre interesse i en variabel ved navn ToPage, bare i iterasjonstellingen den allerede fanget. Å sette ToPage := 0 fra inne i løkkekroppen endrer en variabel den kjørende løkken ikke lenger konsulterer, noe som er nøyaktig grunnen til at Cancel kunne være sann mens skriveren fortsatte å motta sider for flere iterasjoner til, noen ganger alle sammen
Det mønsteret er en fullstendig rimelig vane å bringe over fra C eller C++, og PDFium-komponentens C++Builder-fremviser-demo speiler Pascal-en tett nok til å gjøre sammenligningen direkte. Dens for (Copy = 1; Copy <= CopyCount; Copy++) re-tester Copy <= CopyCount mot den levende verdien av CopyCount ved hver passering, så å nullstille telleren der, avslutter faktisk løkken ved neste sjekk. C++Builder-demoen bar den identiske nullstill-tellerne-koden og den fungerte, noe som er nøyaktig det som fikk den samme idéen til å se trygg ut å gjenbruke i Pascal-bygget som sitter rett ved siden av det i det samme repositoryet
Hvorfor traff ikke Delphi-bygget den samme bugen?
Delphi PDFViewer-demoen var aldri avhengig av at en løkke la merke til en endret grense, fordi avbrytelsesveien dens vikler ut gjennom et unntak i stedet for en sammenligning. Den innerste løkken kaller RTL-ens Abort-prosedyre i det øyeblikket Cancel blir sann, noe som kaster en stille EAbort som forplanter seg rett gjennom alle tre nøstede for-løkker til en håndterer pakket rundt hele utskriftsblokken
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;
Et unntak bryr seg ikke om hvor mange for-løkker som skiller punktet der det kastes, fra håndtereren som fanger det, noe som er nøyaktig egenskapen dette problemet trenger. Den robustheten var ikke et bevisst forsvar mot grense-låsings-oppførselen beskrevet ovenfor — forfatteren av Delphi-demoen grep bare til et annet verktøy. Den unntaksbaserte utveien er fortsatt verdt å navngi som det mer robuste mønsteret: den overlever at et fjerde nøstingsnivå legges til senere, mens en kjede av manuelt plasserte Break-setninger må huskes og legges til på nytt hver gang løkkenøstingen endrer seg
Løsningen: Break på hvert nøstingsnivå, styrt av et PrintSucceeded-flagg
Løsningen som ble levert i PDFiumPas v2.27.0, beholder tre-løkke-strukturen i Lazarus- og C++Builder-fremviser-demoene, men gjør avbrytelse eksplisitt på hvert nivå, og den skiller løkkestansen fra at jobben sendes inn, ved å legge det inn i et flagg som bare leses etter at løkken har fullstendig avsluttet
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 beregnes bevisst én gang, umiddelbart etter at den tre-nøstede løkken avslutter, fra ikke noe mer enn not Cancel. Ingenting inne i løkkekroppen får bestemme på egen hånd om jobben lyktes — løkken kan bare avslutte på to måter, ved å gå tom for sammenstillinger, sider, og kopier, eller ved å treffe Break-kjeden Cancel utløser, og PrintSucceeded leser utfallet i etterkant snarere enn å spore det etter hvert som løkken går. Å beregne det på den måten er det som gjør finally-blokkens valg mellom Printer.EndDoc og Printer.Abort pålitelig: det utløses aldri før løkken faktisk har lagt seg
Å revidere dine egne avbrytelses-drevne utskriftsløkker
Tre sjekker reiser godt utover denne ene utskriftsrutinen. Anta aldri at en Pascal for-løkke vil legge merke til en grensevariabel som endres etter at den har startet; hvis en løkke trenger å avslutte tidlig, si det direkte med Break, på hvert nøstingsnivå avbrytelsen trenger å krysse, ikke bare det innerste. Foretrekk en utvei-mekanisme som vikler ut ved konstruksjon, slik som et unntak, når nøstingen er dyp nok til at en glemt Break er plausibel — Delphi-demoens Abort/EAbort-par fikk dette gratis. Sett enhver commit-trinn som EndDoc bak et flagg beregnet strengt etter løkken, aldri inni den, slik at en jobb som stoppet tidlig, aldri kan mistas for en som fullførte
Den samme gjennomgangen som fikset denne løkken, strammet også inn hvordan de ni fremviser-demoene håndterer tastaturet mens en avbryt-knapp er synlig: de svelger nå hver tast unntatt Esc, slik at en villfaren Ctrl+P eller Ctrl+F under en aktiv utskrift eller søk ikke lenger kan starte en andre oppå den. Denne reentrancy-fiksen er en annen bug med en annen mekanisme, men den kom ut av den samme gjennomgangen av hva Cancel faktisk gjør midt i en operasjon. Vanen verdt å ta med seg fra begge fiksene er mer en testevane enn en kodevane: øv avbrytelse på hver kompilator en delt utskriftsrutine faktisk sendes til, fordi en idé som å tømme tellerne og stole på at løkken legger merke til det, kan overleve testing under C++Builder, nå et Free Pascal-bygg uverifisert, lese identisk i kildekoden, og feile på en måte ingen ser før noen holder Avbryt nede lenge nok til å se jobben fullføre likevel. For utskriftsoppsettet denne avbrytelseslogikken sitter oppå, dekker gjennomgangen av å skrive ut PDF-dokumenter med PDFium VCL-komponenten resten
Denne utskrift-avbrytelses-fiksen ble levert som en del av PDFium-komponenten for Delphi, C++Builder, og FPC/Lazarus, som kjører den samme demo-suiten på tvers av alle tre kompilatorer ved hver utgivelse, slik at gap som dette fanges av en build-matrise snarere enn en support-sak