Una vinculación de Pascal sobre una biblioteca C se lee como Pascal ordinario. Llama a un método, obtiene un registro de vuelta, libera lo que asignó. El problema es que PDFium es una biblioteca C y C++ con su propia convención de llamadas, sus propios anchos de enteros y sus propias reglas sobre quién posee la memoria y quién la libera. Nada de eso cruza el límite del lenguaje por sí solo. Cada uno de esos contratos tiene que ser reestablecido a mano en las declaraciones de Pascal, y una sola palabra incorrecta convierte una llamada de apariencia limpia en una corrupción de pila, un desplazamiento truncado o una doble liberación. Una auditoría v1.61.0 de una vinculación PDFium VCL reveló un defecto de cada tipo. Vale la pena recorrerlos porque no son específicos de esta vinculación. Son los peligros permanentes de empaquetar cualquier API de C en Delphi o Lazarus
cdecl es parte del tipo de función, no una decoración
PDFium es C compilado. En Win32, sus exportaciones y, lo que es más importante, las devoluciones de llamada que invoca utilizan la convención de llamadas cdecl. Bajo cdecl, el llamador limpia la pila después de que la llamada regresa. El valor predeterminado nativo de Delphi es register, y el estándar de C de Win32 para devoluciones de llamada es stdcall en algunas bibliotecas, donde en su lugar limpia el destinatario de la llamada. Cuando una estructura entrega a PDFium un puntero de función y usted olvida el cdecl en el tipo de ese puntero, las dos partes no están de acuerdo sobre quién ajusta el puntero de pila. Ambos lo solucionan, o ninguno lo hace, y el puntero de pila se desvía por el tamaño de los argumentos en cada invocación
La razón por la que este defecto es difícil de encontrar es que el daño no es local. La llamada corrupta regresa y parece correcta. La desalineación aparece más tarde, en alguna función no relacionada cuyo marco ahora se encuentra en un puntero de pila que está desviado unos pocos bytes, y se manifiesta como una lectura aleatoria, una dirección de retorno incorrecta o una caída con un rastreo de pila que no apunta a ninguna parte cerca de la devolución de llamada en la que realmente se equivocó. El llenado de formularios es el lugar clásico donde esto afecta, porque la interfaz de llenado de formularios es un registro lleno de devoluciones de llamada a las que PDFium vuelve a llamar. Una de ellas, FFI_OpenFile, entrega a PDFium una función que llamará para abrir un archivo externo, declarada como function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. El cdecl final es el punto que vale la pena copiar. Omítalo y el código seguirá compilándose, enlazándose y ejecutándose hasta que PDFium llame a la función. La convención pertenece al tipo de función en sí. No es un elemento opcional, y el compilador no le advertirá cuando falte porque un tipo de función simple es un tipo Pascal perfectamente legal. La única defensa es tratar la convención de llamadas como un campo obligatorio de cada firma importada y de cada devolución de llamada que pase al exterior
size_t es el ancho del puntero, y en FPC Win64 eso significa 64 bits
El segundo defecto es una falta de coincidencia en el ancho del entero que solo aparece en un destino. El tipo size_t de C se define para ser lo suficientemente ancho como para contener cualquier tamaño de objeto, lo que en una plataforma de 64 bits significa un entero sin signo de 64 bits. Las interfaces de carga progresiva de PDFium hablan en desplazamientos de bytes size_t. El registro FX_FILEAVAIL del proveedor de disponibilidad lleva una devolución de llamada IsDataAvail que PDFium llama con un desplazamiento y un tamaño, y la devolución de llamada AddSegment del registro FX_DOWNLOADHINTS recibe lo mismo. Ambos parámetros son size_t
IsDataAvail = function(
pThis : PFX_FILEAVAIL;
offset, size: size_t): FPDF_BOOL; cdecl;
AddSegment = procedure(
pThis : PFX_DOWNLOADHINTS;
offset, size: size_t); cdecl;
Si declara esos desplazamientos como un tipo de 32 bits, la vinculación funciona en Win32 y en Delphi Win64, y luego se rompe silenciosamente en FPC y Lazarus Win64. La causa es sutil. En FPC Win64, NativeUInt es un tipo genuino de 64 bits con ancho de puntero, y size_t tiene un alias para él. La vinculación tiene un comentario en la sección de tipos que advierte precisamente contra la superposición de NativeUInt en FPC, porque redefinirlo como un alias de 32 bits allí forzaría a size_t a 32 bits y corrompería cada parámetro size_t pasado a o escrito por la biblioteca. Un desplazamiento de 64 bits que llega a un parámetro de 32 bits pierde su mitad superior. Para un archivo pequeño, cada desplazamiento cabe en 32 bits y no hay ningún problema. Para un archivo grande, en el momento en que un desplazamiento cruza la línea de los cuatro gigabytes, el valor truncado apunta a otra parte completamente distinta, PDFium pregunta si el rango de bytes incorrecto está disponible y la carga progresiva se detiene o lee basura. El defecto es invisible hasta que el archivo es lo suficientemente grande y el destino es aquel en el que size_t realmente se amplió
Una excepción de Pascal nunca debe propagarse a través de un marco de C
La tercera clase se refiere al modelo de excepciones, que C no tiene. Cuando PDFium llama a una de sus devoluciones de llamada, su código Pascal se ejecuta dentro de una pila de marcos de C y C++ que no saben nada sobre el mecanismo de excepciones de Delphi. Si su devolución de llamada genera una excepción y permite que esta se propague, se desenrolla a través de marcos que nunca se construyeron para ser desenrollados. La limpieza propia de PDFium no se ejecuta, sus invariantes internas quedan a medio actualizar y el proceso se encuentra ahora en un estado que la biblioteca nunca anticipó. El contrato para estas devoluciones de llamada es un código de retorno, no una excepción
Dos devoluciones de llamada hacen esto concreto. FPDF_FILEWRITE es el receptor en el que PDFium escribe un documento guardado, y FPDF_FILEACCESS es el origen del que lee un documento de entrada. Ambos se implementan aquí sobre un TStream de Delphi, y ambos pueden fallar de la manera en que falla cualquier flujo: el disco se llena, el flujo se cierra debajo de usted, una lectura se realiza más allá del final. La devolución de llamada de escritura empaqueta la escritura del flujo y convierte cualquier fallo en el código de fallo de PDFium en lugar de dejar que escape
function WriteBlock(
pThis: PFPDF_FILEWRITE;
pData: Pointer;
Size : LongWord): Integer; cdecl;
begin
// PDFium treats any non-1 return as a write failure. A Pascal exception
// must not unwind through this cdecl/C++ frame, so trap it and report
// failure instead.
Result := 0;
try
PPdfWrite(pThis).Stream.WriteBuffer(pData^, Size);
Result := 1;
except
end;
end;
El lado de lectura hace lo mismo: una lectura fallida reporta cero para coincidir con el contrato de FPDF_FILEACCESS en lugar de generar una excepción a través del límite. Un except simple sin volver a lanzar la excepción parece incorrecto para un programador de Pascal entrenado para nunca ignorar las excepciones, y en Pascal ordinario lo es. En un límite ABI tiene la forma correcta, porque el único valor seguro para devolver al llamador de C es un código de estado que este sepa interpretar. El fallo sigue propagándose, solo que a través del valor de retorno, y el código de llamada por encima de la biblioteca lo presenta como EPdfError una vez que el control vuelve al lado de Pascal
La doble liberación se esconde en la ruta de error
El cuarto defecto es la propiedad. Un controlador de documento PDFium es abierto por la biblioteca y debe cerrarse exactamente una vez, mediante FPDF_CloseDocument. El peligro es una ruta de error que libera un controlador que también posee una segunda limpieza. Imagine una rutina que crea un objeto contenedor, le asigna un controlador de documento recién abierto y luego realiza más configuraciones que podrían fallar. Si la configuración genera un error, un controlador de retorno temprano que llama a FPDF_CloseDocument en el controlador sin procesar lo cerrará, y luego el propio destructor del objeto contenedor lo cerrará nuevamente cuando el objeto sea liberado. El controlador se libera dos veces, lo que representa un comportamiento indefinido y una probable caída
La auditoría encontró esto en una ruta de importación de estilo de imposición que construye un TPdf alrededor de un controlador ya abierto. La solución es hacer que la transferencia de propiedad sea la única fuente de verdad. Una vez que el controlador se asigna al campo del contenedor, el contenedor lo posee, y la única limpieza en la ruta de error es liberar el contenedor. El destructor del contenedor llama a FPDF_CloseDocument por usted, por lo que un segundo cierre explícito liberaría dos veces el mismo documento. El controlador de errores corregido libera el objeto y vuelve a generar la excepción, y hay exactamente una ruta para el cierre
Result := TPdf.Create(nil);
try
Result.FDocument := NewDoc; // Result now owns the handle
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free closes the handle. A second FPDF_CloseDocument(NewDoc)
// here would double-free the same PDFium document.
Result.Free;
raise;
end;
Tanto los registros gestionados como una biblioteca llena de exportaciones necesitan un desmontaje explícito
La última clase se refiere a la memoria que el compilador gestiona en su nombre, la cual un hábito de C corromperá silenciosamente. Muchas de las funciones auxiliares de esta vinculación devuelven un registro que contiene una cadena WideString o una matriz dinámica. Esos son campos con recuento de referencias, y el compilador emite un registro interno oculto para mantener sus recuentos. El instinto heredado de C es borrar un registro nuevo con FillChar(Result, SizeOf(Result), 0). Eso estampa ceros sobre la referencia gestionada dentro del registro sin decrementarla primero. El compilador reutiliza una variable temporal oculta para el resultado de una función a lo largo de las iteraciones del bucle, por lo que en la segunda iteración FillChar sobrescribe un puntero de cadena activo que nunca se liberó, y la cadena a la que apuntaba se filtra. Llame a la función en un bucle sobre mil anotaciones y filtrará mil cadenas
La solución es dejar que el lenguaje limpie el registro de la manera que sabe hacerlo, con Default(T), lo que libera cualquier campo gestionado antes de ponerlo a cero
// Default() instead of FillChar: the compiler reuses one hidden temp for
// the function result across loop iterations, so FillChar would zero live
// WideString pointers without releasing them.
Result := Default(TPdfAnnotation);
Un problema de propiedad relacionado reside en el límite de carga de la biblioteca. Esta vinculación resuelve varios cientos de punteros de función fuera de la DLL de PDFium con GetProcAddress después de un LoadLibrary. Si falta una exportación requerida, el estado parcialmente vinculado es peligroso: docenas de punteros son válidos, el resto son nil o están obsoletos, y cualquier llamada posterior a través de uno de ellos salta a un módulo que ya puede estar descargado. La vinculación maneja esto descargando la biblioteca y ejecutando un ClearAllBindings completo que restablece cada puntero importado a nil siempre que una exportación requerida no se resuelva. Después de eso, ningún puntero de función queda colgando en un módulo descargado, y una llamada posterior falla limpiamente con una comprobación de puntero nulo en lugar de ramificarse en código liberado
El contenedor es donde se reestablecen a mano cuatro contratos
Ninguno de estos cinco defectos es exótico. Son los modos de fallo previsibles de una delgada capa de Pascal sobre una API de C, y se agrupan porque esa capa es exactamente donde se deben volver a declarar cuatro contratos separados. La convención de llamadas debe deletrearse cdecl en cada devolución de llamada. El ancho del entero debe coincidir con size_t en el único destino donde realmente se amplía. El modelo de excepciones debe convertirse en códigos de retorno en cada devolución de llamada que salga de Pascal. La propiedad de cada controlador y de cada campo gestionado debe declararse una vez y obedecerse en cada ruta, incluidas las rutas de error que nadie ejercita hasta la producción. Si olvida cualquiera de ellos, obtendrá un defecto cuyo síntoma aparece lejos de su causa, que es lo que hace que esta categoría sea costosa. El valor de la auditoría consistió menos en una sola solución que en tratar a cada uno de ellos como su propia disciplina para comprobar en toda la vinculación
Si desea ver la vinculación realizando un trabajo real en lugar de proteger sus bordes, las técnicas de caché de procesamiento y zoom en nuestra nota sobre el rendimiento del zoom y la caché de procesamiento muestran la ruta de renderizado, y la guía práctica del compilador cruzado en la construcción de un visor Lazarus y FPC es el lugar donde el comportamiento de size_t en Win64 descrito aquí realmente importa. Ambos se basan en el mismo trabajo de seguridad de memoria y ABI que se incluye en PDFium Component para Delphi, Lazarus y C++Builder, junto con las API de procesamiento, extracción de texto y formularios cubiertas en otras partes de este blog