Tekninen artikkeli

FPC for-silmukkabugi: Peruuta-painikkeen klikkaus ei pysäytä tulostusta

Peruuta-painikkeen klikkaaminen kesken tulostustyön PDFium-komponentin katseluohjelmassa ei joskus tehnyt mitään: silmukka jatkoi jokaisen jäljellä olevan sivun ja kopion renderöintiä, ja työ silti saavutti tulostimen. Syy oli Free Pascalin for-silmukka, joka kiinnittää ylärajansa kerran silmukan sisääntulossa, joten CopyCount- tai ToPage-muuttujan takana olevan muuttujan nollaaminen kesken silmukan ei muuttanut mitään jo käynnissä olevaa. For-silmukan rajabugi on viides sudenkuoppa, joka on tullut esiin samasta PDFiumPas Delphi/FPC-yhteensopivuustarkastuksesta, joka tuotti neljä muuta kääntäjien välistä sudenkuoppaa, ja toisin kuin nuo neljä, se asuu kokonaan katseluohjelman tulostusrutiinin sisällä — vaadittiin testaajaa, joka piti Peruuta-painiketta painettuna pitkän työn läpi, todella huomatakseen, ettei tulostin koskaan pysähtynyt

Miksi tulostustyö jatkuu Peruuta-klikkauksen jälkeen?

Tulostustyö jatkui, koska peruutuksen tarkistus vain nollasi muuttujat, jotka syöttivät silmukan rajoja, ei itse silmukoita, jotka Free Pascal oli jo lukinnut, kun kukin silmukka käynnistyi. SpeedButtonPrintClick-käsittelijä Print-painikkeen takana PDFViewer- ja MultiPageViewer-demoissa rakentaa jokaisen työn kolmesta sisäkkäisestä silmukasta: ulompi silmukka koostettujen kopiosarjojen yli, keskimmäinen silmukka sivujen yli, ja sisin silmukka saman sivun koostamattomien kopioiden yli. PrintDialog.Collate päättää, kumpi laskuri, CollateCopyCount vai CopyCount, todella kantaa pyydettyä kopiomäärää, kun taas toinen pysyy yhdessä. Peruutuksen on ulotuttava kaikkiin kolmeen tasoon kerralla, ja tämän koodin ensimmäinen versio yritti tehdä sen nollaamalla itse rajamuuttujat heti, kun Cancel muuttui todeksi

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

Aikomus lukee riittävän selvästi: jos laskurit, jotka määrittävät, kuinka monta koostetta, sivua ja kopiota on jäljellä, kaikki putoavat nollaan, silmukoiden pitäisi loppua työstä ja pudota itsestään läpi. Printer.EndDoc ajettiin sitten ehdoitta silmukan jälkeen riippumatta siitä, miten se päättyi, joten jopa työ, jonka käyttäjä uskoi pysäyttäneensä, silti lähetettiin taustatulostukseen jokaisen sivun renderöitynä ennen kuin klikkaus huomattiin

Mitä Free Pascal lukitsee, kun for-silmukka käynnistyy

Free Pascal arvioi for-silmukan loppuarvon täsmälleen kerran, sillä hetkellä kun silmukka alkaa, eikä koskaan enää sen silmukan elinajan aikana. for Page := FromPage to ToPage do lukee ToPage-arvon yhden kerran laskeakseen, kuinka monta iteraatiota ajaa, ja sen jälkeen silmukalla ei ole enää mitään kiinnostusta muuttujaan nimeltä ToPage, vain iteraatiomäärään, jonka se on jo napannut. ToPage := 0:n asettaminen silmukan rungon sisältä muuttaa muuttujaa, jota käynnissä oleva silmukka ei enää konsultoi, mikä on juuri se syy, miksi Cancel saattoi olla tosi, samalla kun tulostin jatkoi sivujen vastaanottamista vielä useamman iteraation ajan, joskus kaikkien niiden

Tuo malli on täysin järkevä tapa tuoda mukanaan C:stä tai C++:sta, ja PDFium-komponentin C++Builder-katseluohjelmademo peilaa Pascal-versiota tarpeeksi läheisesti tehdäkseen vertailusta suoran. Sen for (Copy = 1; Copy <= CopyCount; Copy++) testaa uudelleen Copy <= CopyCount-ehdon CopyCount:n elävää arvoa vasten jokaisella kierroksella, joten laskurin nollaaminen siellä todella päättää silmukan seuraavalla tarkistuksella. C++Builder-demo kantoi identtisen nollaa-laskurit-koodin, ja se toimi, mikä on juuri se, mikä sai saman idean näyttämään turvalliselta uudelleenkäytettäväksi Pascal-käännöksessä, joka istui aivan sen vieressä samassa repositoriossa

Miksei Delphi-käännös osunut samaan bugiin?

Delphi PDFViewer -demo ei koskaan riippunut silmukasta, joka huomaisi muuttuneen rajan, koska sen peruutuspolku purkautuu poikkeuksen kautta vertailun sijaan. Sen sisin silmukka kutsuu RTL:n Abort-proseduuria heti, kun Cancel muuttuu todeksi, mikä nostaa hiljaisen EAbort:n, joka etenee suoraan kaikkien kolmen sisäkkäisen for-silmukan läpi käsittelijään, joka on käärittynä koko tulostuslohkon ympärille

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;

Poikkeus ei välitä siitä, kuinka moni for-silmukka erottaa pisteen, jossa se nostetaan, käsittelijästä, joka nappaa sen, mikä on juuri se ominaisuus, jota tämä ongelma tarvitsee. Tuo vankkuus ei ollut tarkoituksellinen puolustus yllä kuvattua rajanlukituskäytöstä vastaan — Delphi-demon kirjoittaja vain turvautui eri työkaluun. Poikkeuspohjainen ulospääsy kannattaa silti nimetä vankempana mallina: se selviää neljännen sisäkkäisyystason lisäämisestä myöhemmin, kun taas ketju käsin sijoitettuja Break-lauseita on muistettava ja lisättävä uudelleen joka kerta, kun silmukan sisäkkäisyys muuttuu

Korjaus: Break jokaisella sisäkkäisyystasolla, portitettuna PrintSucceeded-lipulla

Korjaus, joka julkaistiin PDFiumPas v2.27.0:ssa, pitää kolmen silmukan rakenteen Lazarus- ja C++Builder-katseluohjelmademoissa, mutta tekee peruutuksesta nimenomaisen jokaisella tasolla, ja erottaa silmukan pysäyttämisen työn lähettämisestä lipuksi, joka luetaan vasta sen jälkeen, kun silmukka on kokonaan päättynyt

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 lasketaan tarkoituksella kerran, välittömästi kolmoissisäkkäisen silmukan poistumisen jälkeen, ei mistään muusta kuin not Cancel:sta. Mikään silmukan rungon sisällä ei saa päättää itsenäisesti, onnistuiko työ — silmukka voi päättyä vain kahdella tavalla, loppumalla koosteista, sivuista ja kopioista, tai osumalla Break-ketjuun, jonka Cancel laukaisee, ja PrintSucceeded lukee lopputuloksen jälkikäteen sen sijaan, että seuraisi sitä silmukan edetessä. Sen laskeminen tällä tavalla on se, mikä tekee finally-lohkon valinnasta Printer.EndDoc:n ja Printer.Abort:n välillä luotettavan: se ei koskaan laukea ennen kuin silmukka on todella asettunut

Omien peruutusohjattujen tulostussilmukoiden tarkastaminen

Kolme tarkistusta kulkevat paljon tätä yhtä tulostusrutiinia pidemmälle. Älä koskaan oleta, että Pascal-for-silmukka huomaa rajamuuttujan muuttuvan sen jälkeen, kun se on käynnistynyt; jos silmukan on päätyttävä aikaisin, sano se suoraan Break-lauseella, jokaisella sisäkkäisyystasolla, jonka peruutuksen on ylitettävä, ei vain sisimmällä. Suosi ulospääsymekanismia, joka purkautuu rakenteellisesti, kuten poikkeusta, heti kun sisäkkäisyys on tarpeeksi syvä, että puuttuva Break on uskottava — Delphi-demon Abort/EAbort-pari sai tämän ilmaiseksi. Portita mikä tahansa sitomisvaihe kuten EndDoc lipun taakse, joka lasketaan tiukasti silmukan jälkeen, ei koskaan sen sisällä, jotta työtä, joka pysähtyi aikaisin, ei koskaan voida sekoittaa sellaiseen, joka valmistui

Sama tarkastus, joka korjasi tämän silmukan, tiukensi myös, miten yhdeksän katseluohjelmademoa käsittelevät näppäimistöä, kun peruutuspainike on näkyvissä: ne nyt nielevät jokaisen näppäimen paitsi Esc:n, joten eksynyt Ctrl+P tai Ctrl+F aktiivisen tulostuksen tai haun aikana ei voi enää käynnistää toista sen päälle. Tämä uudelleenajautuvuuskorjaus on eri bugi eri mekanismilla, mutta se tuli samasta tarkastuksesta siitä, mitä Cancel todella tekee kesken toiminnon. Tapa, joka kannattaa viedä mukaan molemmista korjauksista, on enemmän testaustapa kuin koodaustapa: harjoita peruutusta jokaisella kääntäjällä, jolle jaettu tulostusrutiini todella toimitetaan, koska idea kuten laskureiden tyhjentäminen ja luottaminen siihen, että silmukka huomaa sen, voi selvitä testauksesta C++Builderin alla, saavuttaa Free Pascal -käännöksen vahvistamattomana, lukea identtisenä lähteessä, ja epäonnistua tavalla, jota kukaan ei näe, ennen kuin joku pitää Peruuta-painiketta painettuna tarpeeksi kauan nähdäkseen työn valmistuvan joka tapauksessa. Tulostusasetuksille, joiden päälle tämä peruutuslogiikka istuu, läpikäynti PDF-asiakirjojen tulostamisesta PDFium VCL -komponentilla kattaa loput

Tämä tulostuksen peruutuskorjaus julkaistiin osana PDFium-komponenttia Delphille, C++Builderille ja FPC/Lazarukselle, joka ajaa saman demosarjan kaikkien kolmen kääntäjän yli jokaisella julkaisulla, jotta tällaiset aukot nappaa käännösmatriisi eikä tukipyyntö