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 coste de diagnóstico, y el orden es el contrario 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 caro, porque el fallo aparece a varias capas de su causa. Un stand-in que devuelve éxito es lo peor de todo, 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 fue mal está en los bytes que salieron
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 había encima siguió funcionando. Los puntos de entrada de importación EMF y el punto de entrada de captura de canvas se ejecutaron hasta el final y devolvieron un identificador de imagen legal, que quien llamó colocó luego en una página. Lo que aterrizó en el fichero fue un form XObject con una longitud de contenido de cero. La página se renderizó blanca
Nada informó de un problema, y eso incluye el programa de demostración de la propia biblioteca para esta característica, que dibujó una página en blanco y no se enteró. No había valor de retorno de fallo que comprobar, porque la secuencia de llamadas genuinamente tuvo éxito por completo; lo único malo era el tamaño del stream producido. Diagnosticar esta clase de defecto significa hacerse otra pregunta: 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 cazan
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 un canal siquiera. 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 directamente en un access violation cuando el árbol de páginas desreferencia la nada. Un stub que lanza 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 rellenaba 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ómputo del bounding box dividió por cero. Un manejador de excepciones desnudo se lo tragó, la fábrica de imágenes devolvió un resultado nulo y el access violation 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
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 en 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 fuera el valor por defecto. Y un valor de píxeles por pulgada de cero hizo que todo 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 de stand-ins: un método que ni
// lanza ni hace nada. Ambos compilan y ambos producen «éxito»
// sin salida
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 wide que conserva solo el primer carácter
Este ni siquiera es un problema de stand-ins, 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 tecleados como punteros a caracteres de un solo byte, mientras que la función que la rellena es la variante de caracteres wide 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 ocurre 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. Todos los nombres de impresora volvieron como exactamente un carácter. Aguas 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
// Equivocado: 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 wide en todo
type
TPrinterInfo2W = record
pServerName: PWideChar;
pPrinterName: PWideChar;
// ... diez más
end;
La regla que sale de ahí es mecánica y merece aplicarse sin pensarlo: para cualquier estructura Win32 cuyo nombre termine en W, verificar que cada miembro de cadena es la variante wide, campo a campo. Mezclar el mundo ANSI y el wide no produce ni diagnóstico del compilador ni fallo, solo truncamiento silencioso, y lo mismo aplica al revés para las variantes ANSI
Un manejador de excepciones desnudo es el adversario real
Todas estas investigaciones se ralentizaron por el mismo constructo: un manejador que captura todo y lo convierte en un valor de retorno falso. Es algo razonable de escribir alrededor de un decodificador de imágenes, ya que una imagen corrupta no debería tumbar un trabajo de documento. También es un dispositivo para borrar la única pieza de información que necesitáis
La respuesta práctica es hacer que el manejador sea temporalmente ruidoso. 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 con 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 un access violation en un método de stand-in cuyo nombre lo decía todo
Lista de comprobación para adoptar una ruta de stand-ins
Cuatro puntos, en el orden en que rinden. Antes de llamar a una clase sustituta, leed los métodos que estáis a punto de usar y confirmad que cada uno tiene un cuerpo real; un cuerpo vacío no es un detalle de implementación, es una función ausente. Preferid stand-ins que lanzan sobre stand-ins que devuelven valores neutros, y emparejadlo con comprobaciones de nulo en los lugares donde una fábrica ahora puede legítimamente no devolver nada. Verificad una función inspeccionando el artefacto, no el código de retorno, porque todo el modo de fallo de aquí es un código de retorno limpio sobre un artefacto vacío; un desglose a nivel de bytes 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 fichero cubre esa herramienta. Y cuando una función no tiene implementación sustituta viable, encaminad las muestras afectadas a la ruta que sí funciona y decid por qué en un comentario, en lugar de dejar una demostración que produce silenciosamente salida en blanco
La idea más amplia se aplica mucho más allá de una biblioteca. Cualquier base de código con una segunda implementación condicional, una capa de mocks, 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 confía normalmente, 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 traiciona. Ese es también el razonamiento detrás de comprobar artefactos en vez de estados al manejar entrada no confiable, descrito en el artículo de parsing de PDF no confiable, y detrás de comparar la salida renderizada entre motores en vez de fiarse de uno, descrito 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 de producto de la losLab PDF Developer Library