Tehnički članak

Greška FPC petlje for: klik na Cancel ne zaustavlja ispis

Klik na Cancel tijekom ispisa u pregledniku PDFium Component ponekad nije radio ništa: petlja je nastavila iscrtavati svaku preostalu stranicu i kopiju, a zadatak je ipak stigao do pisača. Uzrok je bila petlja for u Free Pascalu, koja pri ulasku u petlju jednom zadaje gornju granicu, pa postavljanje varijable iza CopyCount ili ToPage na nulu usred petlje nije promijenilo ono što je već bilo u tijeku. Greška s granicama petlje for peta je zamka pronađena u istoj reviziji kompatibilnosti Delphi/FPC za PDFiumPas koja je otkrila i četiri druge zamke među prevoditeljima, a za razliku od njih živi potpuno unutar rutine ispisa preglednika — tek je ispitivač koji je tijekom dugog zadatka držao Cancel primijetio da se pisač nikad nije zaustavio

Zašto se ispis nastavlja nakon klika na Cancel?

Zadatak ispisa nastavljao se jer je provjera otkazivanja poništavala samo varijable koje određuju granice petlji, a ne same petlje, čije je granice Free Pascal već zaključao pri pokretanju svake petlje. Rukovatelj SpeedButtonPrintClick iza gumba Print u primjerima PDFViewer i MultiPageViewer svaki zadatak gradi od triju ugniježđenih petlji: vanjske petlje kroz skupove složenih kopija, srednje petlje kroz stranice i unutarnje petlje kroz nesložene kopije iste stranice. PrintDialog.Collate određuje koji brojač, CollateCopyCount ili CopyCount, sadrži traženi broj kopija, dok drugi ostaje na jedan. Otkazivanje mora istodobno proći kroz sve tri razine, a prva je inačica ovog koda to pokušala postići poništavanjem samih varijabli granica čim je Cancel postao istinit

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

Namjera je dovoljno jasna: ako se brojači koji određuju koliko je složenih kopija, stranica i kopija preostalo spuste na nulu, petlje bi trebale ostati bez posla i same završiti. Printer.EndDoc zatim se bezuvjetno izvršavao nakon petlje bez obzira na njezin završetak, pa se čak i zadatak za koji je korisnik vjerovao da ga je zaustavio slao u red čekanja sa svim stranicama iscrtanim prije nego što je klik primijećen

Što Free Pascal određuje pri pokretanju petlje for

Free Pascal konačnu vrijednost petlje for izračunava točno jednom, u trenutku pokretanja petlje, i nikad više tijekom njezina trajanja. for Page := FromPage to ToPage do čita ToPage samo jednom kako bi izračunao broj ponavljanja, a nakon toga petlju više ne zanima varijabla nazvana ToPage, nego samo već spremljeni broj ponavljanja. Postavljanje ToPage := 0 iz tijela petlje mijenja varijablu koju pokrenuta petlja više ne provjerava, upravo zato je Cancel mogao biti istinit dok je pisač još primao stranice kroz nekoliko dodatnih ponavljanja, ponekad kroz sva

To je potpuno razumna navika prenesena iz C-a ili C++-a, a demonstracijski preglednik PDFium Component za C++Builder dovoljno je sličan Pascalovu da je usporedba izravna. Njegov for (Copy = 1; Copy <= CopyCount; Copy++) pri svakom prolazu ponovno uspoređuje Copy <= CopyCount sa živom vrijednošću CopyCount, pa postavljanje brojača na nulu ondje doista završava petlju pri sljedećoj provjeri. Demonstracija za C++Builder sadržavala je isti kod za poništavanje brojača i radila je, upravo zbog čega je ista ideja izgledala sigurno za ponovnu uporabu u Pascalovoj izgradnji koja se nalazila odmah uz nju u istom repozitoriju

Zašto Delphi inačica nije imala istu grešku?

Demonstracija Delphi PDFViewer nikad nije ovisila o tome da petlja primijeti promijenjenu granicu, jer se njezin put otkazivanja odvija kroz iznimku, a ne kroz usporedbu. Njezina unutarnja petlja poziva RTL-ov postupak Abort čim Cancel postane istinit, čime se podiže tiha iznimka EAbort koja prolazi kroz sve tri ugniježđene petlje for do rukovatelja omotanog oko cijelog bloka ispisa

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;

Iznimku nije briga koliko petlji for dijeli mjesto njezina podizanja od rukovatelja koji je hvata, a upravo je to svojstvo potrebno ovom problemu. Ta robusnost nije bila namjerna obrana od prethodno opisanog ponašanja zaključavanja granice — autor demonstracije Delphi jednostavno je posegnuo za drugim alatom. Izlaz temeljen na iznimci ipak vrijedi navesti kao čvršći obrazac: preživljava naknadno dodavanje četvrte razine ugniježđenosti, dok se lanac ručno postavljenih naredbi Break mora zapamtiti i ponovno dodati svaki put kada se ugniježđenost petlji promijeni

Popravak: Break na svakoj razini ugniježđenosti uz zastavicu PrintSucceeded

Popravak isporučen u PDFiumPas v2.27.0 zadržava strukturu s trima petljama u preglednicima za demonstraciju Lazarusa i C++Buildera, ali otkazivanje čini eksplicitnim na svakoj razini te zaustavljanje petlje odvaja od slanja zadatka pomoću zastavice koja se čita tek nakon potpunog završetka petlje

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 namjerno se izračunava jednom, odmah nakon izlaska iz trostruko ugniježđene petlje, samo iz izraza not Cancel. Ništa unutar tijela petlje ne odlučuje samostalno je li zadatak uspio — petlja može završiti samo na dva načina, tako da ostane bez složenih kopija, stranica i kopija ili tako da dosegne lanac naredbi Break koji pokreće Cancel, a PrintSucceeded ishod čita naknadno umjesto da ga prati tijekom petlje. Takav izračun čini izbor bloka finally između Printer.EndDoc i Printer.Abort pouzdanim: on se nikad ne izvršava prije nego što se petlja stvarno smiri

Provjera vlastitih petlji ispisa kojima upravlja Cancel

Tri se provjere mogu primijeniti daleko izvan ove jedne rutine ispisa. Nikad ne pretpostavljajte da će Pascalova petlja for primijetiti promjenu varijable granice nakon pokretanja; ako petlja mora završiti ranije, recite to izravno pomoću Break na svakoj razini ugniježđenosti kroz koju otkazivanje mora proći, a ne samo u unutarnjoj petlji. Kad je ugniježđenost dovoljno duboka da je propušteni Break moguć, dajte prednost mehanizmu izlaza koji se odvija po konstrukciji, primjerice iznimci — par Abort/EAbort u demonstraciji Delphi to je dobio besplatno. Svaki korak potvrde poput EndDoc uvjetujte zastavicom izračunatom strogo nakon petlje, nikad unutar nje, kako se zadatak koji je ranije zaustavljen ne bi mogao zamijeniti za onaj koji je dovršen

Ista revizija koja je popravila ovu petlju također je pooštrila način na koji devet demonstracijskih preglednika obrađuje tipkovnicu dok je gumb za otkazivanje vidljiv: sada zanemaruju svaku tipku osim Esc, pa zalutali Ctrl+P ili Ctrl+F tijekom aktivnog ispisa ili pretraživanja više ne može pokrenuti drugi zadatak povrh prvoga. Ovaj popravak ponovne ulaznosti drugačija je greška s drugačijim mehanizmom, ali proizašao je iz iste revizije onoga što Cancel doista radi usred operacije. Navika koju vrijedi ponijeti iz oba popravka više je navika testiranja nego kodiranja: provjerite otkazivanje na svakom prevoditelju za koji se zajednička rutina ispisa stvarno isporučuje, jer ideja poput brisanja brojača i povjerenja da će petlja to primijetiti može proći testiranje pod C++Builderom, doći do neprovjerene izgradnje za Free Pascal, izgledati isto u izvornom kodu i zakazati na način koji nitko ne vidi dok netko dovoljno dugo ne drži Cancel i ne promatra kako se zadatak ipak dovršava. Za postavke ispisa na kojima se temelji ova logika otkazivanja, vodič za ispis PDF dokumenata komponentom PDFium VCL pokriva ostatak

Ovaj popravak otkazivanja ispisa isporučen je kao dio komponente PDFium Component za Delphi, C++Builder i FPC/Lazarus, koja pri svakom izdanju pokreće isti skup demonstracija na sva tri prevoditelja kako bi se praznine poput ove otkrile matricom izgradnje, a ne zahtjevom korisničkoj podršci