Apăsarea Anulare în timpul unui job de tipărire într-un vizualizator al componentei PDFium uneori nu făcea nimic: bucla continua să randeze fiecare pagină și copie rămasă, iar job-ul tot ajungea la imprimantă. Cauza a fost bucla for a Free Pascal, care fixează limita sa superioară o singură dată la intrarea în buclă, așa că zerorizarea variabilei din spatele CopyCount sau ToPage la mijlocul buclei nu schimba nimic din ce rula deja. Bug-ul de limite ale buclei for este a cincea capcană rezultată din același audit de compatibilitate Delphi/FPC al PDFiumPas care a produs alte patru capcane cross-compiler, și spre deosebire de acele patru, aceasta trăiește în întregime în interiorul rutinei de tipărire a unui vizualizator — a fost nevoie ca un tester să țină apăsat Anulare pe parcursul unui job lung pentru a observa efectiv că imprimanta nu se oprea niciodată
De ce continuă job-ul de tipărire după ce se face clic pe Anulare?
Job-ul de tipărire continua pentru că verificarea de anulare doar reseta variabilele care alimentau limitele buclei, nu buclele însele, pe care Free Pascal le fixase deja de la începutul fiecărei bucle. Handler-ul SpeedButtonPrintClick din spatele butonului Print din demo-urile PDFViewer și MultiPageViewer construiește fiecare job din trei bucle imbricate: o buclă exterioară peste seturi de copii colaționate, o buclă mijlocie peste pagini, și o buclă interioară peste copii necolaționate ale aceleiași pagini. PrintDialog.Collate decide care contor, CollateCopyCount sau CopyCount, ține efectiv numărul cerut de copii, în timp ce celălalt rămâne la unu. Anularea trebuie să ajungă prin toate cele trei niveluri deodată, iar prima versiune a acestui cod a încercat să facă asta resetând variabilele de limită înseși în momentul în care Cancel devenea true
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
Intenția se citește destul de clar: dacă contoarele care definesc câte colaționări, pagini și copii rămân scad toate la zero, buclele ar trebui să rămână fără lucru și să treacă mai departe de la sine. Printer.EndDoc apoi rula necondiționat după buclă indiferent de cum s-a terminat, așa că chiar și un job pe care un utilizator credea că l-a oprit tot ajungea trimis către spooler cu fiecare pagină randată înainte ca clicul să fie observat
Ce fixează Free Pascal când începe o buclă for
Free Pascal evaluează valoarea finală a unei bucle for exact o dată, în momentul în care bucla începe, și niciodată din nou pe durata de viață a acelei bucle. for Page := FromPage to ToPage do citește ToPage o singură dată pentru a calcula câte iterații să ruleze, iar după asta bucla nu mai are niciun interes ulterior într-o variabilă numită ToPage, doar în numărul de iterații pe care l-a capturat deja. Setarea ToPage := 0 din interiorul corpului buclei schimbă o variabilă pe care bucla care rulează nu o mai consultă, ceea ce este exact motivul pentru care Cancel putea fi true în timp ce imprimanta continua să primească pagini pentru încă câteva iterații, uneori toate
Acel tipar este un obicei complet rezonabil de purtat din C sau C++, iar demo-ul vizualizator C++Builder al componentei PDFium oglindește pe cel Pascal suficient de aproape încât să facă comparația directă. for (Copy = 1; Copy <= CopyCount; Copy++) al său retestează Copy <= CopyCount față de valoarea vie a CopyCount la fiecare trecere, așa că zerorizarea contorului acolo chiar termină bucla la următoarea verificare. Demo-ul C++Builder purta codul identic de zerorizare-a-contoarelor și funcționa, ceea ce este exact ce a făcut ca aceeași idee să pară sigur de refolosit în build-ul Pascal așezat chiar alături în același depozit
De ce nu a lovit build-ul Delphi același bug?
Demo-ul PDFViewer Delphi nu a depins niciodată de o buclă care observă o limită schimbată, pentru că calea sa de anulare se deșiră printr-o excepție, nu printr-o comparație. Cea mai interioară buclă a sa apelează procedura Abort a RTL în momentul în care Cancel devine true, ceea ce ridică un EAbort silențios care se propagă direct prin toate cele trei bucle for imbricate spre un handler înfășurat în jurul întregului bloc de tipărire
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;
O excepție nu-i pasă de câte bucle for separă punctul unde este ridicată de handler-ul care o prinde, ceea ce este exact proprietatea de care are nevoie această problemă. Acea robustețe nu a fost o apărare deliberată împotriva comportamentului de fixare a limitei descris mai sus — autorul demo-ului Delphi pur și simplu a apelat la o altă unealtă. Ieșirea bazată pe excepție merită totuși numită drept tiparul mai solid: supraviețuiește adăugării unui al patrulea nivel de imbricare mai târziu, în timp ce un lanț de instrucțiuni Break plasate manual trebuie amintit și readăugat de fiecare dată când imbricarea buclei se schimbă
Soluția: Break la fiecare nivel de imbricare, controlat de un steag PrintSucceeded
Soluția livrată în PDFiumPas v2.27.0 păstrează structura cu trei bucle în demo-urile vizualizator Lazarus și C++Builder, dar face anularea explicită la fiecare nivel, și separă oprirea buclei de trimiterea job-ului într-un steag care este citit doar după ce bucla s-a terminat complet
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 este calculat deliberat o singură dată, imediat după ce bucla triplu-imbricată iese, din nimic mai mult decât not Cancel. Nimic din interiorul corpului buclei nu ajunge să decidă singur dacă job-ul a reușit — bucla se poate termina doar în două moduri, rămânând fără colaționări, pagini și copii, sau lovind lanțul Break pe care Cancel îl declanșează, iar PrintSucceeded citește rezultatul după fapt, în loc să-l urmărească pe măsură ce bucla avansează. Calcularea lui în acest fel este ceea ce face alegerea blocului finally între Printer.EndDoc și Printer.Abort de încredere: nu se declanșează niciodată înainte ca bucla să se fi stabilizat efectiv
Auditarea propriilor bucle de tipărire conduse de anulare
Trei verificări călătoresc mult dincolo de această singură rutină de tipărire. Nu presupuneți niciodată că o buclă for Pascal va observa o variabilă de limită schimbându-se după ce a început; dacă o buclă trebuie să se termine devreme, spuneți asta direct cu Break, la fiecare nivel de imbricare pe care anularea trebuie să îl traverseze, nu doar cel mai interior. Preferați un mecanism de ieșire care se deșiră prin construcție, precum o excepție, odată ce imbricarea este suficient de adâncă încât un Break ratat este plauzibil — perechea Abort/EAbort a demo-ului Delphi a obținut asta gratuit. Puneți poartă la orice pas de confirmare precum EndDoc în spatele unui steag calculat strict după buclă, niciodată în interiorul ei, astfel încât un job care s-a oprit devreme să nu poată fi niciodată confundat cu unul care s-a terminat
Aceeași revizuire care a reparat această buclă a strâns de asemenea modul în care cele nouă demo-uri de vizualizator gestionează tastatura în timp ce un buton de anulare este vizibil: acum înghit fiecare tastă în afară de Esc, astfel încât un Ctrl+P sau Ctrl+F rătăcit în timpul unei tipăriri sau căutări active nu mai poate începe una a doua peste ea. Această corecție de reentranță este un bug diferit cu un mecanism diferit, dar a rezultat din aceeași revizuire a ce face efectiv Cancel la mijlocul unei operații. Obiceiul care merită dus de la ambele soluții este mai mult un obicei de testare decât unul de scriere de cod: exercitați anularea pe fiecare compilator pe care o rutină de tipărire partajată efectiv este livrată, pentru că o idee precum golirea contoarelor și încrederea că bucla va observa poate supraviețui testării sub C++Builder, poate ajunge la un build Free Pascal neverificat, se poate citi identic în sursă, și poate eșua într-un mod pe care nimeni nu îl vede până când cineva ține apăsat Anulare suficient de mult pentru a vedea job-ul terminându-se oricum. Pentru configurarea de tipărire pe care stă această logică de anulare, ghidul despre tipărirea documentelor PDF cu componenta VCL PDFium acoperă restul
Această corecție de anulare a tipăririi a fost livrată ca parte a componentei PDFium pentru Delphi, C++Builder și FPC/Lazarus, care rulează aceeași suită de demo-uri pe toate cele trei compilatoare la fiecare lansare, astfel încât goluri ca acesta sunt prinse de o matrice de build, nu de un tichet de suport