Hacer clic en Cancelar durante un trabajo de impresión en un visor del componente PDFium a veces no hacía nada: el bucle seguía renderizando cada página y copia restante, y el trabajo igual llegaba a la impresora. La causa fue el bucle for de Free Pascal, que fija su límite superior una sola vez al entrar al bucle, así que poner en cero la variable detrás de CopyCount o ToPage a mitad del bucle no cambiaba nada de lo que ya estaba en ejecución. El bug de límites del bucle for es la quinta trampa que salió de la misma auditoría de compatibilidad Delphi/FPC de PDFiumPas que produjo otras cuatro trampas entre compiladores, y a diferencia de esas cuatro, vive enteramente dentro de la rutina de impresión de un visor —se necesitó un probador manteniendo presionado Cancelar durante un trabajo largo para realmente notar que la impresora nunca se detenía
¿Por qué sigue el trabajo de impresión después de hacer clic en Cancelar?
El trabajo de impresión seguía adelante porque la comprobación de cancelación solo reiniciaba las variables que alimentaban los límites del bucle, no los bucles mismos, que Free Pascal ya había fijado cuando cada bucle empezó. El manejador SpeedButtonPrintClick detrás del botón Imprimir en las demostraciones PDFViewer y MultiPageViewer construye cada trabajo a partir de tres bucles anidados: un bucle externo sobre conjuntos de copias intercaladas, un bucle intermedio sobre páginas, y un bucle interno sobre copias no intercaladas de la misma página. PrintDialog.Collate decide cuál contador, CollateCopyCount o CopyCount, realmente contiene el número solicitado de copias, mientras el otro se queda en uno. Cancelar tiene que alcanzar los tres niveles a la vez, y la primera versión de este código intentó hacer eso reiniciando las propias variables de límite en el momento en que Cancel se volvía verdadero
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
La intención se lee lo bastante clara: si los contadores que definen cuántas intercalaciones, páginas, y copias quedan caen todos a cero, los bucles deberían quedarse sin trabajo y terminar por sí solos. Printer.EndDoc luego se ejecutaba incondicionalmente después del bucle sin importar cómo hubiera terminado, así que incluso un trabajo que un usuario creía haber detenido igual se enviaba al spooler con cada página ya renderizada antes de que se notara el clic
Qué fija Free Pascal cuando empieza un bucle for
Free Pascal evalúa el valor final de un bucle for exactamente una vez, en el momento en que empieza el bucle, y nunca más durante la vida de ese bucle. for Page := FromPage to ToPage do lee ToPage una sola vez para calcular cuántas iteraciones ejecutar, y después de eso el bucle ya no tiene ningún interés en una variable llamada ToPage, solo en el conteo de iteraciones que ya capturó. Establecer ToPage := 0 desde dentro del cuerpo del bucle cambia una variable que el bucle en ejecución ya no está consultando, que es exactamente por qué Cancel podía ser verdadero mientras la impresora seguía recibiendo páginas por varias iteraciones más, a veces todas ellas
Ese patrón es un hábito completamente razonable de traer de C o C++, y la demostración de visor C++Builder del componente PDFium refleja la de Pascal lo bastante de cerca como para hacer la comparación directa. Su for (Copy = 1; Copy <= CopyCount; Copy++) vuelve a comprobar Copy <= CopyCount contra el valor vivo de CopyCount en cada pasada, así que poner en cero el contador ahí sí termina genuinamente el bucle en la siguiente comprobación. La demostración de C++Builder llevaba el código idéntico de poner-en-cero-los-contadores y funcionaba, que es exactamente lo que hizo que la misma idea se viera segura de reutilizar en la compilación Pascal sentada justo al lado en el mismo repositorio
¿Por qué la compilación Delphi no se topó con el mismo bug?
La demostración PDFViewer de Delphi nunca dependió de que un bucle notara un límite cambiado, porque su ruta de cancelación se desenrolla mediante una excepción en lugar de una comparación. Su bucle más interno llama al procedimiento Abort del RTL en el momento en que Cancel se vuelve verdadero, que lanza un EAbort silencioso que se propaga directamente a través de los tres bucles for anidados hasta un manejador envuelto alrededor de todo el bloque de impresión
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;
A una excepción no le importa cuántos bucles for separan el punto donde se lanza del manejador que la captura, que es exactamente la propiedad que necesita este problema. Esa robustez no fue una defensa deliberada contra el comportamiento de fijación de límites descrito arriba —el autor de la demostración Delphi simplemente recurrió a una herramienta distinta. La salida basada en excepciones sigue valiendo la pena nombrarla como el patrón más robusto: sobrevive a que se agregue un cuarto nivel de anidamiento más tarde, mientras que una cadena de sentencias Break colocadas manualmente hay que recordarla y volver a agregarla cada vez que cambia el anidamiento del bucle
La corrección: Break en cada nivel de anidamiento, controlado por una bandera PrintSucceeded
La corrección que se incorporó en PDFiumPas v2.27.0 mantiene la estructura de tres bucles en las demostraciones de visor de Lazarus y C++Builder pero hace explícita la cancelación en cada nivel, y separa la detención del bucle del envío del trabajo mediante una bandera que solo se lee después de que el bucle ha terminado por completo
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 calcula deliberadamente una vez, inmediatamente después de que sale el bucle triplemente anidado, a partir de nada más que not Cancel. Nada dentro del cuerpo del bucle decide por su cuenta si el trabajo tuvo éxito —el bucle solo puede terminar de dos maneras, quedándose sin intercalaciones, páginas, y copias, o topándose con la cadena de Break que dispara Cancel, y PrintSucceeded lee el resultado después del hecho en lugar de rastrearlo mientras el bucle avanza. Calcularlo de esa manera es lo que hace confiable la elección del bloque finally entre Printer.EndDoc y Printer.Abort: nunca se dispara antes de que el bucle realmente se haya asentado
Auditar sus propios bucles de impresión controlados por cancelación
Tres comprobaciones viajan mucho más allá de esta única rutina de impresión. Nunca asuma que un bucle for de Pascal notará que una variable de límite cambia después de haber empezado; si un bucle necesita terminar antes, dígalo directamente con Break, en cada nivel de anidamiento que la cancelación necesite cruzar, no solo el más interno. Prefiera un mecanismo de salida que se desenrolle por construcción, como una excepción, una vez que el anidamiento sea lo bastante profundo como para que un Break olvidado sea plausible —el par Abort/EAbort de la demostración Delphi obtuvo esto gratis. Controle cualquier paso de confirmación como EndDoc detrás de una bandera calculada estrictamente después del bucle, nunca dentro de él, así un trabajo que se detuvo antes nunca se puede confundir con uno que terminó
La misma revisión que corrigió este bucle también ajustó cómo manejan el teclado las nueve demostraciones de visor mientras un botón de cancelar está visible: ahora ignoran cada tecla excepto Esc, así que un Ctrl+P o Ctrl+F perdido durante una impresión o búsqueda activa ya no puede iniciar una segunda encima de ella. Esta corrección de reentrancia es un bug distinto con un mecanismo distinto, pero salió de la misma revisión de qué hace realmente Cancel a mitad de una operación. El hábito que vale la pena llevarse de ambas correcciones es más un hábito de pruebas que de código: ejercite la cancelación en cada compilador al que realmente se envíe una rutina de impresión compartida, porque una idea como limpiar los contadores y confiar en que el bucle lo note puede sobrevivir a las pruebas bajo C++Builder, llegar a una compilación Free Pascal sin verificar, leerse idéntica en el código fuente, y fallar de una manera que nadie ve hasta que alguien mantiene presionado Cancelar el tiempo suficiente para ver que el trabajo termina de todos modos. Para la configuración de impresión sobre la que se apoya esta lógica de cancelación, el recorrido sobre impresión de documentos PDF con el componente PDFium VCL cubre el resto
Esta corrección de cancelación de impresión se incorporó como parte del componente PDFium para Delphi, C++Builder, y FPC/Lazarus, que ejecuta la misma suite de demostraciones en los tres compiladores en cada versión, así que brechas como esta las atrapa una matriz de compilación en lugar de un ticket de soporte