Tehnični članak

FPC napaka v zanki for: klik »Prekliči« ne ustavi tiskanja

Klik na Prekliči med tiskanjem v pregledovalniku PDFium Component včasih ni naredil ničesar: zanka je še naprej izrisovala vse preostale strani in kopije, opravilo pa je vseeno prišlo do tiskalnika. Vzrok je bila zanka for v Free Pascalu, ki zgornjo mejo določi enkrat ob vstopu v zanko, zato nastavitev spremenljivke za CopyCount ali ToPage na nič sredi zanke ni vplivala na že tekoče izvajanje. Napaka z mejami zanke for je peta past iz iste revizije združljivosti PDFiumPas za Delphi/FPC, ki je odkrila štiri druge pasti med prevajalniki, vendar je v nasprotju z njimi v celoti skrita v rutini za tiskanje pregledovalnika — opaziti jo je bilo mogoče šele, ko je preizkuševalec med dolgim opravilom držal Prekliči in videl, da se tiskalnik nikoli ni ustavil

Zakaj se tiskalno opravilo nadaljuje po kliku na Prekliči?

Tiskalno opravilo se je nadaljevalo, ker je preverjanje preklica ponastavilo samo spremenljivke, ki so določale meje zank, ne pa samih zank, saj jih je Free Pascal ob vsakem začetku že zaklenil. Obdelovalnik SpeedButtonPrintClick za gumb Print v predstavitvah PDFViewer in MultiPageViewer sestavi vsako opravilo iz treh ugnezdenih zank: zunanje zanke čez združene sklope kopij, srednje zanke čez strani in notranje zanke čez neposamezno združene kopije iste strani. PrintDialog.Collate določi, kateri števec, CollateCopyCount ali CopyCount, dejansko vsebuje zahtevano število kopij, drugi pa ostane enak ena. Preklic mora doseči vse tri ravni hkrati, prva različica te kode pa je to poskušala doseči tako, da je ob nastavitvi Cancel na true ponastavila kar spremenljivke mej

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

Namen je na prvi pogled dovolj jasen: če se števci, ki določajo preostalo število združevanj, strani in kopij, znižajo na nič, naj bi zankam zmanjkalo dela in bi se same končale. Printer.EndDoc se je nato brezpogojno izvedel za zanko, ne glede na način konca, zato je bilo tudi opravilo, za katero je uporabnik menil, da ga je ustavil, poslano v tiskalno vrsto, skupaj z vsemi stranmi, izrisanimi do trenutka, ko je bil klik opažen

Kaj Free Pascal zaklene ob začetku zanke for

Free Pascal končno vrednost zanke for izračuna natanko enkrat, v trenutku začetka zanke, nato pa je v času njenega trajanja nikoli več ne preveri. for Page := FromPage to ToPage do enkrat prebere ToPage, da izračuna število ponovitev, potem pa zanke ne zanima več spremenljivka z imenom ToPage, temveč samo že zajeto število ponovitev. Nastavitev ToPage := 0 znotraj telesa zanke spremeni spremenljivko, ki je tekoča zanka ne preverja več, zato je bil Cancel lahko true, tiskalnik pa je še naprej prejemal strani več ponovitev, včasih kar vse

Tak vzorec je povsem razumljiva navada, prenesena iz C ali C++, predstavitev pregledovalnika PDFium Component za C++Builder pa je dovolj podobna Pascalovi, da je primerjava neposredna. Njen for (Copy = 1; Copy <= CopyCount; Copy++) pri vsakem prehodu znova preveri Copy <= CopyCount glede na trenutno vrednost CopyCount, zato nastavitev števca na nič tam res konča zanko pri naslednjem preverjanju. Predstavitev C++Builder je vsebovala isto kodo za ponastavitev števcev in je delovala, zaradi česar se je ista zamisel zdela varna za ponovno uporabo v Pascalovi gradnji tik ob njej v istem repozitoriju

Zakaj enaka napaka ni prizadela gradnje za Delphi?

Predstavitev PDFViewer za Delphi ni nikoli temeljila na tem, da bi zanka opazila spremenjeno mejo, saj pot preklica izstopi z izjemo namesto s primerjavo. Njena notranja zanka ob spremembi Cancel na true pokliče proceduro Abort iz RTL, ki sproži tiho izjemo EAbort in se po vseh treh ugnezdenih zankah for razširi do obdelovalnika okoli celotnega bloka za tiskanje

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;

Izjema ne skrbi za to, koliko zank for loči mesto sprožitve od obdelovalnika, ki jo ujame, kar je natanko lastnost, ki jo ta težava potrebuje. Ta robustnost ni bila namerna obramba pred zgoraj opisanim vedenjem zaklenjene meje — avtor predstavitve za Delphi je preprosto posegel po drugem orodju. Izstop na podlagi izjeme je vseeno vredno izpostaviti kot trdnejši vzorec: preživi tudi poznejšo dodano četrto raven gnezdenja, medtem ko si je treba verigo ročno postavljenih stavkov Break zapomniti in jo znova dodati ob vsaki spremembi gnezdenja zank

Popravek: Break na vsaki ravni gnezdenja, pod nadzorom zastavice PrintSucceeded

Popravek, izdan v PDFiumPas v2.27.0, ohranja strukturo treh zank v predstavitvah pregledovalnika za Lazarus in C++Builder, vendar preklic izrecno obravnava na vsaki ravni in loči ustavljanje zank od oddaje opravila z zastavico, ki se prebere šele po popolnem koncu zank

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 se namerno izračuna enkrat, takoj po izhodu iz trojno ugnezdenih zank, samo iz izraza not Cancel. Nič v telesu zanke ne odloča samostojno, ali je opravilo uspelo — zanka se lahko konča samo tako, da ji zmanjka združevanj, strani in kopij, ali pa doseže verigo Break, ki jo sproži Cancel, PrintSucceeded pa izid prebere naknadno, namesto da bi ga spremljala med izvajanjem zanke. Prav tak izračun zagotovi, da je izbira med Printer.EndDoc in Printer.Abort v bloku finally zanesljiva, saj se ne izvede, preden se zanka dejansko ne umiri

Pregled lastnih zank za tiskanje, ki jih lahko prekličete

Tri preverjanja so uporabna tudi zunaj te ene rutine za tiskanje. Nikoli ne predpostavite, da bo Pascalova zanka for opazila spremembo spremenljivke meje po začetku izvajanja; če se mora zanka končati predčasno, to povejte neposredno z Break na vsaki ravni gnezdenja, čez katero mora priti preklic, ne samo v notranji zanki. Ko je gnezdenje dovolj globoko, da je izpuščen Break verjeten, raje uporabite mehanizem, ki po zasnovi razplete izvajanje, na primer izjemo — predstavitev za Delphi je to brezplačno dobila s parom Abort/EAbort. Vsak korak oddaje, kot je EndDoc, naj bo pod zastavico, izračunano strogo po koncu zanke, nikoli znotraj nje, da opravila, ki se je predčasno ustavilo, ni mogoče zamenjati za dokončanega

Ista revizija, ki je odpravila to zanko, je zaostrila tudi obravnavo tipkovnice v devetih predstavitvah pregledovalnika, ko je viden gumb za preklic: zdaj pogoltnejo vsako tipko razen Esc, zato naključni Ctrl+P ali Ctrl+F med aktivnim tiskanjem ali iskanjem ne more več zagnati drugega opravila. Ta popravek ponovne vstopnosti je drugačna napaka z drugačnim mehanizmom, vendar je nastal iz istega pregleda tega, kaj Cancel dejansko naredi sredi operacije. Najpomembnejša navada, ki jo je vredno odnesti iz obeh popravkov, je bolj preizkusna kot programska: preverite preklic v vsakem prevajalniku, za katerega se skupna rutina za tiskanje dejansko izda, saj lahko zamisel o brisanju števcev in zaupanju, da bo zanka to opazila, prestane preizkus pod C++Builder, pride v gradnjo za Free Pascal brez preverjanja, je v izvorni kodi videti enaka in odpove tako, da tega nihče ne opazi, dokler nekdo dovolj dolgo ne drži Prekliči in opazuje, kako se opravilo vseeno konča. Za nastavitev tiskanja, na kateri temelji ta logika preklica, preostanek pojasnjuje vodnik za tiskanje dokumentov PDF s komponento PDFium VCL

Ta popravek preklica tiskanja je bil izdan kot del komponente PDFium Component za Delphi, C++Builder in FPC/Lazarus, ki ob vsaki izdaji zažene isti nabor predstavitev v vseh treh prevajalnikih, da takšne vrzeli odkrije matrika gradenj in ne zahtevek za podporo