Artículo técnico

Fallos silenciosos de stubs en una biblioteca PDF Pascal

Cuando una biblioteca Delphi gana una configuración de compilación sin el marco visual, las clases sustitutas son donde viven los errores. No la plataforma, no el compilador: los stand-ins. PDFlibPas tiene una capa de gráficos que proporciona equivalentes de bitmap, canvas, fuente, metarchivo e impresora para compilaciones sin la VCL, y portarla a Free Pascal sacó a la luz todos los modos de fallo que un stand-in puede tener. Se ordenan con claridad por costo de diagnóstico, y el orden es el opuesto de lo que sugiere la intuición

Un stand-in que lanza una excepción es barato de encontrar; la excepción nombra el método. Un stand-in que devuelve datos vacíos es costoso, porque el fallo aparece varias capas más allá de su causa. Un stand-in que devuelve éxito es el peor de todos, porque el código de retorno es válido, el código de error es cero, no se lanza ninguna excepción y la única evidencia de que algo salió mal está en los bytes que salieron

Tres formas de fallo silencioso de stub en una biblioteca PDF Pascal ordenadas por costo de diagnóstico, desde un stub que lanza excepciones hasta éxito sobre salida vacía
Un stand-in que lanza excepciones es barato de diagnosticar, los datos vacíos son costosos y un retorno de éxito sobre un artefacto vacío es lo peor de encontrar

Forma tres: un identificador de imagen válido sobre un XObject vacío

El conversor vectorial de metarchivos era un cuerpo de procedimiento vacío en la configuración sin VCL. Todo lo que está por encima siguió funcionando. Los puntos de entrada de importación EMF y el punto de entrada de captura de canvas corrieron hasta el final y devolvieron un identificador de imagen legal, que quien llamó colocó después en una página. Lo que aterrizó en el archivo fue un form XObject con una longitud de contenido de cero. La página se renderizó en blanco

Nada reportó un problema, y eso incluye el propio programa de demostración de la biblioteca para esta función, que dibujó una página en blanco y no lo notó. No había ningún valor de retorno fallido que comprobar, porque la secuencia de llamadas genuinamente tuvo éxito por completo; lo único que estaba mal era el tamaño del flujo producido. Diagnosticar esta clase de defecto significa hacer una pregunta distinta: no "falló la llamada" sino "el artefacto es plausible". Un form XObject de longitud cero, una imagen de cero píxeles, una página de cero bytes de contenido: esas son las aserciones que lo atrapan

El arreglo tiene dos mitades y la segunda es fácil de olvidar. Primero, hacer que la implementación vacía lance una excepción, para que el fallo tenga algún canal. Segundo, convertir esa excepción en un resultado nulo en la fábrica de imágenes y añadir comprobaciones de nulo en los dos lugares que consumen un identificador de imagen, porque de lo contrario un "fallo limpio" se convierte directo en una violación de acceso cuando el árbol de páginas desreferencia la nada. Un stub que lanza excepciones solo es una mejora si quienes llaman estaban preparados para un fallo que antes nunca habían podido recibir

Forma dos: datos vacíos, a tres capas del fallo

El stand-in de canvas de metarchivo no llenaba sus dimensiones físicas. Ese valor divide en un cálculo de geometría de página, así que el cálculo produjo cero, y el cálculo de la caja delimitadora dividió por cero. Un manejador de excepciones desnudo se tragó eso, la fábrica de imágenes devolvió un resultado nulo, y la violación de acceso ocurrió finalmente en el árbol de páginas cuando se usó el nulo. Tres capas entre causa y síntoma, con un manejador de excepciones en medio borrando la evidencia

Cadena de fallo de un stand-in de canvas de metarchivo vacío en PDFlibPas que llega a una violación de acceso tres capas después de la división por cero
La dimensión vacía reduce a cero un cálculo de geometría, un manejador desnudo borra la excepción y el identificador nulo tumba el árbol de páginas

La misma unidad tenía dos instancias más del patrón. La clase de fuente tenía cuerpos vacíos en Assign y en el constructor, lo que importa más de lo que parece porque la propiedad de fuente del canvas es de solo lectura: asignar dentro de ella es la única forma de entregar una fuente, así que una implementación vacía hace que la selección de fuente sea silenciosamente inefectiva y el texto sale con lo que sea que hubiera por defecto. Y un valor de píxeles por pulgada de cero hizo que quien llamara dimensionando un canvas desde métricas de fuente produjera un canvas de cero por cero, lo que da una página en blanco y un retorno de éxito

// La forma a buscar en una unidad stand-in: un método que ni lanza
// excepciones ni hace nada. Ambos compilan y ambos producen "éxito"
// sin salida alguna
procedure TMetafileCanvasStandIn.Create(...);
begin
  // sin llamada inherited, sin inicialización de campos
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // y el bitmap sigue vacío
end;

La estructura ancha que conserva solo el primer carácter

Este no es un problema de stand-in en absoluto, pero pertenece al mismo catálogo porque el síntoma está igual de lejos de la causa. La estructura de enumeración de impresoras se declaró con sus doce miembros de cadena tipados como punteros a caracteres de un solo byte, mientras que la función que la llena es la variante de caracteres anchos de la API de enumeración

Los tamaños de puntero son idénticos, así que la disposición de la estructura es correcta y nada se cuelga. Lo que pasa en cambio es que leer una cadena UTF-16 como cadena de un solo byte se detiene en el primer byte cero, que para cualquier nombre de impresora ASCII es la mitad alta del segundo carácter. Cada nombre de impresora volvió con exactamente un carácter. Más abajo, la validación de nombres falló, la creación de impresoras falló y la impresión falló para cada impresora real de la máquina, y ninguno de esos síntomas apunta a una declaración de estructura

Nombre de impresora UTF-16 truncado a un carácter tras declarar una estructura ancha Win32 con miembros PAnsiChar en lugar de PWideChar
Los miembros de un solo byte leen un nombre UTF-16 solo hasta su primer byte cero, así que cada nombre de impresora vuelve con exactamente un carácter
// Incorrecto: tamaño correcto, tipo de elemento equivocado. Sin error
// de compilación, sin fallo, cada cadena truncada a un carácter
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... diez más
  end;

// Correcto: una estructura *W tiene miembros anchos en todo
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... diez más
  end;

La regla que sale de ahí es mecánica y vale aplicarla sin pensarlo: para toda estructura Win32 cuyo nombre termina en W, verificar que cada miembro de cadena sea la variante ancha, campo por campo. Mezclar el mundo ANSI y el ancho no produce ni diagnóstico del compilador ni fallo, solo truncamiento silencioso, y lo mismo aplica en sentido inverso a las variantes ANSI

El verdadero adversario es el manejador de excepciones desnudo

Cada una de estas investigaciones se atrasó por el mismo constructo: un manejador que atrapa todo y lo convierte en un valor de retorno falso. Es razonable escribirlo alrededor de un decodificador de imágenes, ya que una imagen corrupta no debe tumbar un trabajo de documentos. También es un dispositivo para borrar el único dato que necesitan

La respuesta práctica es hacer que el manejador grite temporalmente. Volcar la clase de excepción, el mensaje y el backtrace desde dentro del manejador desnudo, bajo un condicional de depuración, convierte un retorno nulo inexplicable en una excepción con nombre y ubicación. En dos de los tres casos anteriores ese único paso terminó la investigación, porque la excepción era una división por cero o una violación de acceso en un método stand-in cuyo nombre lo decía todo

Lista de verificación para adoptar una vía stand-in

Cuatro puntos, en el orden en que rinden. Antes de llamar a una clase sustituta, lean los métodos que van a usar y confirmen que cada uno tiene un cuerpo real; un cuerpo vacío no es un detalle de implementación, es una función ausente. Prefieran stand-ins que lanzan excepciones sobre stand-ins que devuelven valores neutros, y combínenlo con comprobaciones de nulo en los lugares donde una fábrica ahora puede legítimamente no devolver nada. Verifiquen una función inspeccionando el artefacto, no el código de retorno, ya que todo el modo de fallo aquí es un código de retorno limpio sobre un artefacto vacío; un desglose byte a byte de lo que un documento contiene realmente es la forma más rápida de verlo, y el artículo de auditoría de tamaño de archivo cubre esa herramienta. Y cuando una función no tiene implementación sustituta viable, enruten las muestras afectadas a la vía que sí funciona y digan por qué en un comentario, en lugar de dejar una demostración que produce salida en blanco en silencio

El punto más amplio aplica mucho más allá de una biblioteca. Cualquier base de código con una segunda implementación condicional, una capa de mock, un modo headless, un shim de plataforma, está expuesta a la forma tres. La razón de que se esconda tan bien es que cada control de calidad en el que un equipo normalmente confía, códigos de retorno, códigos de error, excepciones, estados de salida, es un canal de estado, y la forma tres los mantiene todos limpios. Solo la salida lo delata. De ahí también la lógica de comprobar artefactos en vez de estados al manejar entrada no confiable, descrita en el artículo de análisis de PDF no confiable, y de comparar la salida renderizada entre motores en vez de confiar en uno, descrita en renderizado multi-motor

PDFlibPas es una biblioteca PDF nativa en Object Pascal para Delphi, C++Builder y Free Pascal, y su configuración sin VCL es lo que hace posibles las compilaciones headless y entre toolchains; la cobertura de configuraciones actual está en la página del producto losLab PDF Developer Library