Mengklik Cancel selama sebuah job cetak di sebuah viewer PDFium Component terkadang tidak melakukan apa-apa: loop tersebut terus merender setiap halaman dan salinan yang tersisa, dan job itu tetap sampai ke printer. Penyebabnya adalah loop for milik Free Pascal, yang menetapkan batas atasnya sekali pada saat masuk loop, sehingga menolkan variabel di balik CopyCount atau ToPage di tengah loop tidak mengubah apa pun yang sudah berjalan. Bug batas for-loop ini adalah jebakan kelima yang muncul dari audit kompatibilitas Delphi/FPC PDFiumPas yang sama yang menghasilkan empat jebakan lintas-compiler lainnya, dan tidak seperti keempatnya, bug ini sepenuhnya hidup di dalam rutin cetak sebuah viewer — dibutuhkan seorang tester yang menahan Cancel sepanjang sebuah job panjang untuk benar-benar memperhatikan printer tersebut tidak pernah berhenti
Mengapa job cetak terus berjalan setelah Cancel diklik?
Job cetak itu terus berjalan karena pemeriksaan pembatalan hanya mereset variabel yang memberi makan batas loop, bukan loop itu sendiri, yang sudah dikunci Free Pascal saat setiap loop dimulai. Handler SpeedButtonPrintClick di balik tombol Print di demo PDFViewer dan MultiPageViewer membangun setiap job dari tiga loop bersarang: sebuah loop luar atas set salinan collated, sebuah loop tengah atas halaman, dan sebuah loop dalam atas salinan uncollated dari halaman yang sama. PrintDialog.Collate memutuskan counter mana, CollateCopyCount atau CopyCount, yang benar-benar memegang jumlah salinan yang diminta, sementara yang lain tetap di satu. Membatalkan harus menjangkau ketiga level sekaligus, dan versi pertama kode ini mencoba melakukan itu dengan mereset variabel batas itu sendiri begitu Cancel menjadi 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
Maksudnya terbaca cukup jelas: jika counter yang mendefinisikan berapa banyak collation, halaman, dan salinan yang tersisa semuanya turun ke nol, loop tersebut seharusnya kehabisan pekerjaan dan jatuh keluar dengan sendirinya. Printer.EndDoc kemudian berjalan tanpa syarat setelah loop terlepas dari bagaimana loop itu berakhir, sehingga bahkan sebuah job yang diyakini pengguna sudah dihentikan tetap diserahkan ke spooler dengan setiap halaman dirender sebelum klik itu diperhatikan
Apa yang dikunci Free Pascal ketika sebuah for loop dimulai
Free Pascal mengevaluasi nilai akhir sebuah loop for tepat satu kali, pada saat loop itu dimulai, dan tidak pernah lagi selama umur loop tersebut. for Page := FromPage to ToPage do membaca ToPage satu kali tunggal untuk menghitung berapa banyak iterasi yang harus berjalan, dan setelah itu loop tersebut tidak lagi berminat pada sebuah variabel bernama ToPage, hanya pada hitungan iterasi yang sudah ditangkapnya. Mengatur ToPage := 0 dari dalam body loop mengubah sebuah variabel yang tidak lagi dikonsultasikan loop yang berjalan itu, dan itulah persis mengapa Cancel bisa true sementara printer terus menerima halaman untuk beberapa iterasi lagi, terkadang semuanya
Pola itu adalah sebuah kebiasaan yang sepenuhnya masuk akal untuk dibawa dari C atau C++, dan demo viewer C++Builder milik PDFium Component mencerminkan yang Pascal cukup dekat untuk membuat perbandingan langsung. for (Copy = 1; Copy <= CopyCount; Copy++) miliknya menguji ulang Copy <= CopyCount terhadap nilai hidup CopyCount pada setiap langkah, sehingga menolkan counter di sana benar-benar mengakhiri loop pada pemeriksaan berikutnya. Demo C++Builder membawa kode nolkan-counter yang identik dan itu bekerja, yang persis membuat ide yang sama terlihat aman digunakan kembali dalam build Pascal yang duduk tepat di sebelahnya dalam repository yang sama
Mengapa build Delphi tidak menabrak bug yang sama?
Demo PDFViewer Delphi tidak pernah bergantung pada sebuah loop yang memperhatikan sebuah batas yang berubah, karena jalur pembatalannya membatalkan lewat sebuah exception alih-alih sebuah perbandingan. Loop terdalamnya memanggil prosedur Abort milik RTL begitu Cancel menjadi true, yang memunculkan sebuah EAbort diam-diam yang merambat langsung lewat ketiga loop for bersarang ke sebuah handler yang membungkus seluruh blok cetak
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;
Sebuah exception tidak peduli berapa banyak loop for memisahkan titik di mana ia dimunculkan dari handler yang menangkapnya, dan itulah persis sifat yang dibutuhkan masalah ini. Ketangguhan itu bukan sebuah pertahanan yang disengaja terhadap perilaku pengunciam-batas yang dijelaskan di atas — penulis demo Delphi itu sekadar menggunakan tool yang berbeda. Jalan keluar berbasis-exception itu tetap layak disebutkan sebagai pola yang lebih kokoh: ia bertahan dari sebuah level nesting keempat yang ditambahkan belakangan, sementara sebuah rantai statement Break yang ditempatkan manual harus diingat dan ditambahkan ulang setiap kali nesting loop berubah
Perbaikannya: Break di setiap level nesting, dijaga sebuah flag PrintSucceeded
Perbaikan yang dirilis di PDFiumPas v2.27.0 mempertahankan struktur tiga-loop di demo viewer Lazarus dan C++Builder tetapi membuat pembatalan eksplisit di setiap level, dan ia memisahkan penghentian loop dari job yang dikirim menjadi sebuah flag yang hanya dibaca setelah loop itu sepenuhnya selesai
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 secara sengaja dihitung sekali, segera setelah loop tiga-lapis keluar, dari tidak lebih dari not Cancel. Tidak ada apa pun di dalam body loop yang boleh memutuskan sendiri apakah job itu berhasil — loop tersebut hanya bisa berakhir dengan dua cara, kehabisan collation, halaman, dan salinan, atau menabrak rantai Break yang dipicu Cancel, dan PrintSucceeded membaca hasilnya setelah fakta alih-alih melacaknya selama loop berjalan. Menghitungnya dengan cara itu adalah yang membuat pilihan blok finally antara Printer.EndDoc dan Printer.Abort bisa dipercaya: ia tidak pernah terpicu sebelum loop benar-benar mengendap
Mengaudit loop cetak berbasis-cancel Anda sendiri
Tiga pemeriksaan berlaku jauh melampaui satu rutin cetak ini. Jangan pernah mengasumsikan sebuah loop for Pascal akan memperhatikan sebuah variabel batas yang berubah setelah dimulai; jika sebuah loop perlu berakhir lebih awal, nyatakan itu secara langsung dengan Break, di setiap level nesting yang perlu diseberangi pembatalan, bukan hanya yang terdalam. Lebih pilih sebuah mekanisme jalan-keluar yang membatalkan berdasarkan konstruksi, seperti sebuah exception, begitu nesting-nya cukup dalam sehingga sebuah Break yang terlewat masuk akal — pasangan Abort/EAbort milik demo Delphi mendapatkan ini secara gratis. Jaga langkah commit apa pun seperti EndDoc di balik sebuah flag yang dihitung secara ketat setelah loop, tidak pernah di dalamnya, sehingga sebuah job yang berhenti lebih awal tidak pernah bisa disalahkirakan sebagai yang selesai
Review yang sama yang memperbaiki loop ini juga memperketat bagaimana kesembilan demo viewer menangani keyboard selagi sebuah tombol cancel terlihat: mereka sekarang menelan setiap tombol kecuali Esc, sehingga sebuah Ctrl+P atau Ctrl+F nyasar selama sebuah cetak atau pencarian aktif tidak lagi bisa memulai yang kedua di atasnya. Perbaikan reentrancy ini adalah bug berbeda dengan mekanisme berbeda, tetapi muncul dari review yang sama tentang apa yang sebenarnya dilakukan Cancel di tengah-operasi. Kebiasaan yang layak dibawa pergi dari kedua perbaikan itu adalah lebih sebuah kebiasaan pengujian daripada pengkodean: jalankan pembatalan pada setiap compiler yang benar-benar dikirimi sebuah rutin cetak bersama, karena sebuah ide seperti membersihkan counter dan memercayai loop untuk memperhatikannya bisa bertahan dari pengujian di bawah C++Builder, mencapai sebuah build Free Pascal tanpa terverifikasi, terbaca identik dalam source, dan gagal dengan cara yang tidak dilihat siapa pun sampai seseorang menahan Cancel cukup lama untuk menyaksikan job itu tetap selesai. Untuk pengaturan cetak yang menjadi landasan logika pembatalan ini, panduan tentang mencetak dokumen PDF dengan komponen PDFium VCL membahas sisanya
Perbaikan pembatalan-cetak ini dirilis sebagai bagian dari PDFium Component untuk Delphi, C++Builder, dan FPC/Lazarus, yang menjalankan suite demo yang sama di ketiga compiler pada setiap rilis sehingga celah seperti ini tertangkap oleh sebuah build matrix alih-alih sebuah tiket dukungan