Un binding de Pascal sobre una biblioteca C se lee como Pascal ordinario. Usted llama a un método, obtiene un registro, libera lo que asignó. El problema es que PDFium es una biblioteca de C y C++ con su propia convención de llamadas, sus propios anchos de enteros y sus propias reglas sobre quién es dueño de la memoria y quién la libera. Nada de eso cruza el límite del idioma por sí solo. Cada uno de esos contratos debe reafirmarse 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 (offset) truncado o una doble liberación. Una auditoría de la versión 1.61.0 de un binding de PDFium Component descubrió un defecto de cada tipo. Vale la pena revisarlos porque no son específicos de este binding. Son los riesgos permanentes de envolver 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 (callbacks) que invoca usan 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 las devoluciones de llamada es stdcall en algunas bibliotecas, donde en cambio es el llamado quien limpia. Cuando una estructura le entrega a PDFium un puntero de función y usted olvida el cdecl en el tipo de ese puntero, ambas partes están en desacuerdo sobre quién ajusta el puntero de la pila. Ambos lo arreglan, o ninguno de los dos lo hace, y el puntero de la 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 se ve bien. La desalineación aparece más tarde, en alguna función no relacionada cuyo marco (frame) ahora se asienta sobre un puntero de pila que está a unos pocos bytes de distancia, y se manifiesta como una lectura salvaje (wild read), una dirección de retorno incorrecta o un bloqueo con un seguimiento retrospectivo (backtrace) que no apunta a ningún lugar cerca de la devolución de llamada en la que realmente se equivocó. El llenado de formularios (form-fill) es el lugar clásico donde esto causa problemas, porque la interfaz form-fill es un registro lleno de devoluciones de llamada que PDFium invoca. Una de ellas, FFI_OpenFile, entrega a PDFium una función a la 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 al final es el punto que vale la pena copiar. Omítalo y el código aún se compila, aún se vincula y aún se ejecuta hasta que PDFium llama a la función. La convención pertenece al propio tipo de función. No es azúcar sintáctico opcional, y el compilador no le advertirá cuando falte, porque un tipo de función simple es un tipo de 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 usted pase hacia el exterior
size_t es del ancho del puntero, y en FPC Win64 eso significa 64 bits
El segundo defecto es una falta de coincidencia en el ancho de los enteros que solo aparece en un objetivo (target). El 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 se comunican con desplazamientos de bytes (byte offsets) de tipo size_t. La devolución de llamada IsDataAvail del registro FX_FILEAVAIL del proveedor de disponibilidad es invocada por PDFium 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 usted declara esos desplazamientos como un tipo de 32 bits, el binding 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 genuino tipo de 64 bits con el ancho del puntero, y size_t es un alias para él. El binding tiene un comentario en la sección de tipos advirtiendo precisamente contra la ocultación (shadowing) de NativeUInt en FPC, porque redefinirlo a un alias de 32 bits allí forzaría a size_t a 32 bits y corrompería cada parámetro size_t pasado 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 encaja en 32 bits y nada está mal. Para un archivo grande, en el momento en que un desplazamiento cruza la línea de los cuatro gigabytes, el valor truncado apunta a un lugar completamente diferente, 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 objetivo es aquel en el que size_t realmente se amplió
Una excepción de Pascal nunca debe desenrollarse a través de un marco C
La tercera clase trata sobre el modelo de excepciones, que C no tiene. Cuando PDFium llama a una de sus devoluciones de llamada, su código de Pascal se ejecuta dentro de una pila de marcos de C y C++ que no saben nada sobre la maquinaria de excepciones de Delphi. Si su devolución de llamada genera una excepción y deja que se propague, se desenrolla a través de marcos que nunca fueron construidos para desenrollarse. La limpieza propia de PDFium no se ejecuta, sus invariantes internos se dejan 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 destino donde PDFium escribe un documento guardado, y FPDF_FILEACCESS es la fuente desde donde lee un documento de entrada. Ambas se implementan aquí sobre un TStream de Delphi, y ambas pueden fallar de la forma en que cualquier flujo (stream) falla: el disco se llena, el flujo se cierra debajo de usted, una lectura se ejecuta más allá del final. La devolución de llamada de escritura envuelve su escritura en el flujo y convierte cualquier falla en el código de falla de PDFium en lugar de dejar que escape
function WriteBlock(
pThis: PFPDF_FILEWRITE;
pData: Pointer;
Size : LongWord): Integer; cdecl;
begin
// PDFium trata cualquier retorno que no sea 1 como una falla de escritura. Una excepción
// de Pascal no debe desenrollarse a través de este marco cdecl/C++, así que atrápela y reporte
// falla en su lugar.
Result := 0;
try
PPdfWrite(pThis).Stream.WriteBuffer(pData^, Size);
Result := 1;
except
end;
end;
El lado de la lectura hace lo mismo: una lectura fallida reporta cero para que coincida con el contrato FPDF_FILEACCESS en lugar de generar una excepción cruzando el límite. Un except sin re-lanzar parece incorrecto a un programador de Pascal entrenado para nunca tragarse las excepciones, y en el Pascal ordinario es incorrecto. En un límite ABI, es la forma correcta, porque el único valor seguro para devolver al llamador C es un código de estado que sepa cómo interpretar. La falla aún se propaga, solo que a través del valor de retorno, y el código de llamada por encima de la biblioteca la aflora como EPdfError una vez que el control está de vuelta en el lado Pascal de la valla
La doble liberación se esconde en la ruta de error
El cuarto defecto es la propiedad. El identificador (handle) de un 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 identificador que una segunda limpieza también posee. Imagine una rutina que crea un objeto contenedor, le asigna un identificador de documento recién abierto y luego realiza más configuraciones que podrían fallar. Si la configuración arroja un error, un controlador de retorno temprano que llame a FPDF_CloseDocument en el identificador sin procesar lo cerrará, y luego el propio destructor del objeto contenedor lo cerrará nuevamente cuando se libere el objeto. El identificador se libera dos veces, lo cual es un comportamiento indefinido y un posible bloqueo
La auditoría encontró esto en una ruta de importación estilo imposición que construye un TPdf alrededor de un identificador ya abierto. La solución es hacer que la transferencia de propiedad sea la única fuente de verdad. Una vez que el identificador se asigna al campo del contenedor, el contenedor es el dueño, 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 por partida doble el mismo documento. El manejador de errores corregido libera el objeto y lo relanza (re-raises), y hay exactamente una ruta hacia el cierre
Result := TPdf.Create(nil);
try
Result.FDocument := NewDoc; // Result ahora posee el identificador
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free cierra el identificador. Un segundo FPDF_CloseDocument(NewDoc)
// aquí liberaría dos veces el mismo documento PDFium.
Result.Free;
raise;
end;
Los registros administrados y una biblioteca llena de exportaciones necesitan un desmontaje explícito
La última clase trata sobre la memoria que el compilador administra en su nombre, la cual un hábito de C corromperá silenciosamente. Muchas de las funciones de ayuda de este binding devuelven un registro que contiene un WideString o un array dinámico. Esos son campos contados por referencia (reference-counted), y el compilador emite contabilidad oculta para mantener sus conteos. El instinto transferido de C es limpiar un registro nuevo con FillChar(Result, SizeOf(Result), 0). Eso sella ceros sobre la referencia administrada dentro del registro sin disminuirla primero. El compilador reutiliza un archivo temporal oculto para el resultado de una función a través de las iteraciones del bucle, por lo que en la segunda iteración, FillChar sobrescribe un puntero de cadena en vivo que nunca se liberó, y la cadena a la que apuntaba se filtra (leaks). Llame a la función en un bucle sobre mil anotaciones y usted filtrará mil cadenas
La solución es dejar que el lenguaje limpie el registro de la manera que sabe hacerlo, con Default(T), que libera cualquier campo administrado antes de ponerlo a cero
// Default() en lugar de FillChar: el compilador reutiliza un temporal oculto para
// el resultado de la función a través de las iteraciones del bucle, por lo que FillChar
// pondría a cero los punteros de WideString vivos sin liberarlos.
Result := Default(TPdfAnnotation);
Un problema de propiedad relacionado vive en el límite de carga de la biblioteca. Este binding 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 nulos (nil) o están inactivos, y cualquier llamada posterior a través de uno de ellos salta a un módulo que ya puede estar descargado. El binding maneja esto descargando la biblioteca y ejecutando un ClearAllBindings completo que restablece cada puntero importado de nuevo a nulo siempre que una exportación requerida no se resuelve. Después de eso, ningún puntero de función cuelga hacia un módulo descargado, y una llamada posterior falla limpiamente con una verificación de puntero nulo en lugar de ramificarse hacia un código liberado
El wrapper es donde cuatro contratos se reafirman a mano
Ninguno de estos cinco defectos es exótico. Son los modos de falla predecibles de una fina capa de Pascal sobre una API de C, y se agrupan porque esa capa es exactamente donde se tienen que re-declarar cuatro contratos separados. La convención de llamadas tiene que deletrearse cdecl en cada devolución de llamada. El ancho del entero tiene que coincidir con size_t en el único objetivo donde realmente se amplía. El modelo de excepción tiene que convertirse en códigos de retorno en cada devolución de llamada que cruza fuera de Pascal. La propiedad de cada identificador y cada campo administrado tiene que indicarse una vez y obedecerse en cada ruta, incluidas las rutas de error que nadie ejercita hasta la producción. Pase por alto cualquiera de ellos y obtendrá un defecto cuyo síntoma aparece lejos de su causa, que es lo que hace que esta categoría sea cara. El valor de la auditoría estuvo menos en cualquier corrección única que en tratar cada uno de estos como su propia disciplina a verificar en todo el binding
Si quiere ver el binding haciendo trabajo real en lugar de vigilar sus bordes, las técnicas de render-cache y zoom en nuestra nota sobre el rendimiento de render-cache y zoom muestran la ruta de renderizado, y el recorrido por el compilador cruzado en la construcción de un visor en 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 el PDFium Component para Delphi, Lazarus y C++Builder, junto con las API de renderizado, extracción de texto y formularios que se tratan en otros lugares de este blog