مقاله فنی

باگ حلقه‌ی for در FPC: کلیک روی لغو، چاپ را متوقف نمی‌کند

کلیک روی لغو در طول یک job چاپ در یک نمایشگر PDFium Component گاهی هیچ کاری نمی‌کرد: حلقه هر صفحه و کپی باقی‌مانده را همچنان رندر می‌کرد، و job همچنان به چاپگر می‌رسید. علت حلقه‌ی for در Free Pascal بود، که کران بالایش را یک‌بار در ورود به حلقه ثابت می‌کند، پس صفرکردن متغیر پشت CopyCount یا ToPage در میانه‌ی حلقه هیچ چیزی که از پیش در حال اجرا بود را تغییر نمی‌داد. باگ کران حلقه‌ی for پنجمین تله‌ای است که از همان ممیزی سازگاری Delphi/FPC در PDFiumPas بیرون آمده که چهار عجیبی بین-کامپایلری دیگر را تولید کرد، و برخلاف آن چهار تا، این یکی کاملاً درون روال چاپ یک نمایشگر زندگی می‌کند — طول کشید تا یک تستر که در طول یک job طولانی دکمه‌ی لغو را نگه داشته بود، واقعاً متوجه شود چاپگر هرگز متوقف نشد

چرا job چاپ پس از کلیک روی لغو همچنان ادامه می‌یابد؟

job چاپ ادامه می‌یافت چون بررسی لغو فقط متغیرهایی را بازنشانی می‌کرد که کران حلقه را تغذیه می‌کنند، نه خودِ حلقه‌ها را، که Free Pascal از پیش وقتی هر حلقه شروع شد قفل کرده بود. handler به نام SpeedButtonPrintClick پشت دکمه‌ی Print در دموهای PDFViewer و MultiPageViewer هر job را از سه حلقه‌ی تودرتو می‌سازد: یک حلقه‌ی بیرونی روی مجموعه‌های کپی collated، یک حلقه‌ی میانی روی صفحات، و یک حلقه‌ی درونی روی کپی‌های uncollated همان صفحه. PrintDialog.Collate تصمیم می‌گیرد کدام شمارنده، CollateCopyCount یا CopyCount، واقعاً تعداد درخواستی کپی‌ها را نگه می‌دارد، درحالی‌که دیگری روی یک باقی می‌ماند. لغوکردن باید همزمان از هر سه سطح عبور کند، و نسخه‌ی اول این کد تلاش کرد این کار را با بازنشانی خودِ متغیرهای کران همان لحظه‌ای که Cancel درست می‌شود انجام دهد

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

نیت به‌اندازه‌ی کافی روشن خوانده می‌شود: اگر شمارنده‌هایی که تعیین می‌کنند چند collation، صفحه، و کپی باقی مانده همگی به صفر برسند، حلقه‌ها باید از کار تمام شوند و خودشان بیرون بیایند. سپس Printer.EndDoc بدون قید و شرط پس از حلقه اجرا می‌شد صرف‌نظر از اینکه چطور پایان یافته، پس حتی jobای که یک کاربر باور داشت متوقفش کرده، همچنان با هر صفحه‌ای که پیش از متوجه‌شدن کلیک رندر شده بود به spooler ارسال می‌شد

Free Pascal چه چیزی را وقتی یک حلقه‌ی for شروع می‌شود قفل می‌کند

Free Pascal مقدار نهایی یک حلقه‌ی for را دقیقاً یک‌بار، همان لحظه‌ای که حلقه شروع می‌شود، ارزیابی می‌کند و دیگر هرگز در طول عمر آن حلقه. for Page := FromPage to ToPage do یک‌بار ToPage را می‌خواند تا محاسبه کند چند تکرار اجرا کند، و پس از آن حلقه دیگر هیچ علاقه‌ای به متغیری به نام ToPage ندارد، فقط به شمار تکراری که از پیش گرفته. تنظیم ToPage := 0 از درون بدنه‌ی حلقه متغیری را تغییر می‌دهد که حلقه‌ی در حال اجرا دیگر مشورتش نمی‌کند، که دقیقاً همان دلیلی است که Cancel می‌توانست درست باشد درحالی‌که چاپگر همچنان برای چند تکرار بیشتر، گاهی همه‌ی آن‌ها، صفحات را دریافت می‌کرد

آن الگو یک عادت کاملاً معقول است برای انتقال از C یا C++، و دموی نمایشگر C++Builder کامپوننت PDFium نسخه‌ی Pascal را به‌اندازه‌ی کافی نزدیک آینه می‌کند که مقایسه را مستقیم کند. for (Copy = 1; Copy <= CopyCount; Copy++) آن، Copy <= CopyCount را در برابر مقدار زنده‌ی CopyCount در هر پاس دوباره تست می‌کند، پس صفرکردن شمارنده آنجا واقعاً حلقه را در بررسی بعدی تمام می‌کند. دموی C++Builder همان کد صفرکردن-شمارنده‌ها را حمل می‌کرد و کار می‌کرد، که دقیقاً همان چیزی است که همان ایده را برای استفاده‌ی دوباره در ساخت Pascal که درست کنارش در همان مخزن نشسته بود امن نشان می‌داد

چرا ساخت Delphi به همین باگ برخورد نکرد؟

دموی PDFViewer در Delphi هرگز به متوجه‌شدن یک حلقه از تغییر یک کران بستگی نداشت، چون مسیر لغوش از طریق یک استثنا باز می‌شود به‌جای یک مقایسه. درونی‌ترین حلقه‌اش procedure Abort از RTL را همان لحظه‌ای که Cancel درست می‌شود فرا می‌خواند، که یک EAbort خاموش raise می‌کند که مستقیم از هر سه حلقه‌ی تودرتوی for به یک handler که دور کل بلوک چاپ پیچیده شده منتشر می‌شود

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;

یک استثنا اهمیتی نمی‌دهد چند حلقه‌ی for نقطه‌ای که در آن raise می‌شود را از handlerای که آن را می‌گیرد جدا می‌کنند، که دقیقاً همان ویژگی‌ای است که این مسئله نیاز دارد. آن استحکام یک دفاع عمدی در برابر رفتار قفل‌کردن-کران توصیف‌شده در بالا نبود — نویسنده‌ی دموی Delphi صرفاً به‌سراغ یک ابزار متفاوت رفت. راه‌فرار مبتنی‌بر-استثنا همچنان ارزش نام‌بردن به‌عنوان الگوی استوارتر دارد: این از افزوده‌شدن یک سطح تودرتویی چهارم بعداً جان سالم به‌در می‌برد، درحالی‌که یک زنجیره از عبارت‌های Break دستی‌گذاشته‌شده باید هر بار که تودرتویی حلقه تغییر کند به‌خاطر سپرده و دوباره اضافه شود

رفع اشکال: Break در هر سطح تودرتویی، دروازه‌بندی‌شده با یک پرچم PrintSucceeded

رفع اشکالی که در PDFiumPas v2.27.0 عرضه شد ساختار سه-حلقه‌ای را در دموهای نمایشگر Lazarus و C++Builder نگه می‌دارد اما لغوشدن را در هر سطح صریح می‌کند، و توقف حلقه را از ارسال‌شدن job به یک پرچمی جدا می‌کند که فقط پس از تمام‌شدن کامل حلقه خوانده می‌شود

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 عمداً یک‌بار محاسبه می‌شود، بلافاصله پس از خروج حلقه‌ی سه‌گانه‌ی تودرتو، از هیچ چیز بیشتر از not Cancel. هیچ چیزی درون بدنه‌ی حلقه به‌تنهایی تصمیم نمی‌گیرد آیا job موفق بوده — حلقه فقط می‌تواند به دو روش پایان یابد، تمام‌شدن collationها، صفحات، و کپی‌ها، یا برخورد به زنجیره‌ی Breakای که Cancel ماشه می‌کشد، و PrintSucceeded نتیجه را پس از واقعه می‌خواند به‌جای اینکه همان‌طور که حلقه پیش می‌رود آن را پیگیری کند. محاسبه‌کردنش به این روش چیزی است که انتخاب بلوک finally بین Printer.EndDoc و Printer.Abort را قابل‌اعتماد می‌کند: هرگز پیش از اینکه حلقه واقعاً ساکن شده باشد شلیک نمی‌شود

ممیزی حلقه‌های چاپ لغو-محور خودتان

سه بررسی فراتر از این یک روال چاپ خوب سفر می‌کنند. هرگز فرض نکنید یک حلقه‌ی for در Pascal متوجه تغییر یک متغیر کران پس از شروعش می‌شود؛ اگر یک حلقه نیاز دارد زودتر پایان یابد، آن را مستقیماً با Break بگویید، در هر سطح تودرتویی‌ای که لغو نیاز دارد از آن عبور کند، نه فقط درونی‌ترین. یک سازوکار فرار را ترجیح دهید که ساختاری باز می‌شود، مثل یک استثنا، به‌محض اینکه تودرتویی به‌اندازه‌ی کافی عمیق باشد که یک Break از قلم‌افتاده باورپذیر باشد — جفت Abort/EAbort دموی Delphi این را رایگان به‌دست آورد. هر گام commit مثل EndDoc را پشت یک پرچم دروازه‌بندی کنید که دقیقاً پس از حلقه محاسبه شده، هرگز درونش، پس jobای که زودتر متوقف شده هرگز نمی‌تواند با یکی که تمام شده اشتباه گرفته شود

همان بازبینی که این حلقه را رفع اشکال کرد همچنین اینکه نه دموی نمایشگر چطور صفحه‌کلید را در حین قابل‌مشاهده‌بودن یک دکمه‌ی لغو مدیریت می‌کنند سفت کرد: حالا هر کلیدی جز Esc را می‌بلعند، پس یک Ctrl+P یا Ctrl+F سرگردان در طول یک چاپ یا جستجوی فعال دیگر نمی‌تواند یکی دوم را رویش شروع کند. این رفع اشکال reentrancy یک باگ متفاوت با یک سازوکار متفاوت است، اما از همان بازبینی از اینکه Cancel واقعاً در میانه‌ی عملیات چه کاری انجام می‌دهد بیرون آمد. عادتی که ارزش برداشتن از هر دو رفع اشکال دارد بیشتر یک عادت تستی است تا یک عادت کدنویسی: لغوشدن را روی هر کامپایلری که یک روال چاپ مشترک واقعاً به آن عرضه می‌شود اعمال کنید، چون ایده‌ای مثل پاک‌کردن شمارنده‌ها و اعتمادکردن به اینکه حلقه متوجه شود، می‌تواند از تست زیر C++Builder جان سالم به‌در ببرد، به یک ساخت Free Pascal تأییدنشده برسد، یکسان در سورس خوانده شود، و به‌شکلی شکست بخورد که هیچ‌کس تا زمانی که کسی به‌اندازه‌ی کافی طولانی لغو را پایین نگه دارد تا ببیند job در هر صورت تمام می‌شود، نمی‌بیند. برای تنظیم چاپی که این منطق لغو رویش می‌نشیند، راهنمای چاپ اسناد PDF با کامپوننت VCL از PDFium بقیه را پوشش می‌دهد

این رفع اشکال لغو-چاپ به‌عنوان بخشی از کامپوننت PDFium برای Delphi، C++Builder، و FPC/Lazarus عرضه شد، که همان مجموعه دموها را در سراسر هر سه کامپایلر در هر انتشار اجرا می‌کند پس شکاف‌هایی مثل این با یک ماتریس build گرفته می‌شوند نه یک تیکت پشتیبانی