Este es un problema que aparece en cuanto una biblioteca PDF sale de su lenguaje de origen. Tiene un enlace que funciona a la perfección desde C# en Windows. Necesita las mismas llamadas desde Python en macOS, así que copia el archivo de declaraciones de Windows, cambia el nombre del binario y lo ejecuta. Todos los símbolos se resuelven. La primera llamada devuelve datos sin sentido, la segunda se bloquea con una infracción de acceso y nada de su código PDF ha cambiado. El fallo está una capa por debajo del PDF: las exportaciones de Windows usan la convención Stdcall, la dylib de macOS exporta las mismas funciones como Cdecl con un guion bajo inicial, y una declaración de función externa que se equivoca en cualquiera de esos detalles corrompe la pila antes de abrir un solo documento
Toda esa clase de fallos procede de una decisión de diseño que conviene entender desde el principio. PDF Library for Delphi, el motor PDF de código disponible de losLab para Delphi y C++Builder, envuelve todo su modelo de objetos en una única clase de fachada plana, TPDFlib, y después distribuye esa fachada en tres formatos binarios: una DLL de Windows con unas 1.250 funciones exportadas, un objeto de automatización COM/ActiveX y una dylib de macOS. La semántica PDF es idéntica en los tres. La parte que puede causarle problemas vive en la ABI subyacente: convenciones de llamada, codificaciones de cadenas, propiedad de los identificadores y qué lado puede liberar cada búfer
Una fachada, tres formatos binarios
Cada función pública de TPDFlib tiene una equivalente plana llamada DL más el nombre del método. LoadFromFile pasa a ser DLLoadFromFile, Encrypt pasa a ser DLEncrypt y NewSignProcessFromFile pasa a ser DLNewSignProcessFromFile. El primer parámetro de casi todas las exportaciones es un InstanceID devuelto por DLCreateLibrary, que sustituye la referencia al objeto que de otro modo mantendría un llamador Delphi. Interiorice pronto esa correspondencia. Significa que la referencia de la API de Delphi también documenta cualquier otro lenguaje: todo lo que puede hacer la clase lo puede hacer la DLL bajo un nombre predecible, y puede leer una firma de método Pascal para saber qué llamada necesita desde Python o C#
La compilación para Windows produce PDFlibDLL32.dll y PDFlibDLL64.dll; elija la que coincida con la arquitectura de bits de su proceso anfitrión, ya que un proceso Java o .NET de 64 bits no puede cargar la biblioteca de 32 bits por muy correcta que parezca la declaración
Windows: instancias Stdcall y pares de funciones W/A
Cada exportación que acepta cadenas existe dos veces. Una versión ancha recibe PWideChar (UTF-16, el ajuste natural para .NET, Java y c_wchar_p de Python), y una versión con el sufijo A recibe PAnsiChar. Ambas tienen semántica idéntica y solo difieren en la codificación, que es precisamente lo que hace tan difícil localizar una mezcla entre ellas: no se lanza ninguna excepción, no se devuelve ningún código de error; simplemente obtiene texto ilegible en los metadatos o un falso «archivo no encontrado» para cualquier ruta que contenga un carácter fuera del ASCII básico. El primer error de codificación que un equipo encuentra de este modo suele costar una tarde entera, porque el síntoma señala los datos y la causa está en la declaración
// Binding de Windows (PDFlibDLL64.dll): Stdcall y nombres de export sin decorar
function DLCreateLibrary: Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLCreateLibrary';
function DLReleaseLibrary(InstanceID: Integer): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLReleaseLibrary';
function DLLoadFromFile(InstanceID: Integer;
FileName, Password: PWideChar): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLLoadFromFile';
// Binding de macOS: la misma función, Cdecl y guion bajo delante del export
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Elija una anchura de caracteres por anfitrión y fíjela en el generador de enlaces. Una regla práctica: si el lenguaje anfitrión tiene cadenas UTF-16 nativas, enlace siempre las versiones W y no vuelva a utilizar la familia A
macOS: mismos nombres, ABI distinta
La dylib exporta el mismo conjunto de funciones DL con dos cambios sistemáticos. La convención de llamada es Cdecl en vez de Stdcall y todos los nombres de exportación llevan un guion bajo inicial (_DLCreateLibrary, _DLLoadFromFile, etcétera). Ambos cambios son puramente mecánicos, lo que los hace idóneos para un enlace generado y peligrosos para una copia del archivo de Windows editada a mano. Mantenga una lista canónica de funciones y genere desde ella las declaraciones para cada plataforma si sus herramientas lo permiten. Si omite ese paso, obtendrá exactamente la corrupción de pila descrita al principio de esta página, que se reproducirá solo en la plataforma que su CI pruebe con menor frecuencia
Anfitriones COM y ActiveX: Safecall y cargas útiles Olevariant
Para VB.NET, C#, VBScript y anfitriones de automatización heredados, la compilación OCX envuelve la misma fachada en un objeto de automatización IDispatch, IPDFlibrary, con todos los métodos declarados como Safecall. Esa convención cambia cómo le llegan los errores. Safecall traduce un fallo interno a un HRESULT COM, de modo que un llamador C# captura una excepción allí donde la DLL plana habría devuelto un entero silencioso que el llamador debía recordar comprobar. La misma operación, dos formas de comunicar errores, según el binario que haya cargado
Los datos binarios siguen una segunda regla específica de COM. La interfaz de automatización no tiene parámetros de puntero en absoluto. Todo lo binario, ya sean bytes de imagen de entrada o bytes PDF de salida, cruza el límite como un Olevariant mediante métodos como AddImageFromVariant y AppendToVariant. Serializar una matriz de bytes en una variante es una sola línea en .NET. Si intenta pasarle un puntero sin procesar, razonando que al fin y al cabo es el mismo proceso, la capa de despacho rechaza o altera la llamada. Un detalle más de registro complica los despliegues: el registro COM es por arquitectura de bits, por lo que un OCX registrado con el regsvr32 de 32 bits es invisible para un anfitrión de 64 bits. Esa discrepancia aparece como el célebre e inútil «clase no registrada» en la máquina del cliente, mucho después de salir de la suya
Disciplina de identificadores: las instancias son propietarias de los documentos
La API plana funciona con identificadores enteros. DLCreateLibrary devuelve una instancia. Cargar un archivo devuelve un identificador de documento dentro de esa instancia. Los procesos de firma, las listas de cadenas y los archivos de acceso directo devuelven cada uno sus propios identificadores enteros, todos asociados a la misma instancia. El ciclo de vida se ve igual desde cualquier anfitrión FFI; aquí se muestra en Pascal porque se lee con claridad:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // una instancia por hilo worker
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // returns a DocumentID, 0 on failure
if Doc <> 0 then
begin
DLEncrypt(Inst, 'owner-secret', 'user-secret', 3,
DLEncodePermissions(Inst, 1, 0, 0, 0, 0, 0, 0, 1));
DLSaveToFile(Inst, 'out.pdf');
end;
finally
DLReleaseLibrary(Inst); // libera todos los documentos que posee la instancia
end;
end;
De ese árbol de propiedad se derivan dos cosas. DLReleaseLibrary es la única llamada de limpieza que necesita estrictamente, ya que elimina de una vez todos los identificadores de documentos y procesos de la instancia. En un script breve es suficiente. En un servicio de larga ejecución se convierte en una fuga lenta con trámites adicionales, así que libere los documentos cuando termine con ellos en lugar de acumularlos hasta que muera la instancia. La instancia también es la unidad natural de aislamiento entre hilos. Dé a cada hilo de trabajo su propio InstanceID y no comparta uno entre hilos sin bloqueo externo, por la misma razón por la que nunca compartiría un único objeto TPDFlib entre hilos
Las cadenas devueltas están prestadas, no son propiedad suya
Las funciones que devuelven texto, como DLGetPageText, entregan un PWideChar o un PAnsiChar que apunta a un búfer propiedad de la instancia de la biblioteca y reutilizado por ella. El contrato es: copie de inmediato; no libere nunca
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointer into a library-owned buffer
PageText := P; // copia ya; una llamada posterior puede reutilizar el buffer
end;
En C# eso significa convertir el IntPtr en una cadena administrada antes de la siguiente llamada a la biblioteca. En ctypes de Python, significa extraer enseguida la cadena ancha del puntero. Mantenga el puntero sin procesar entre llamadas y habrá escrito un error que supera todas las pruebas unitarias pero falla la primera vez que dos solicitudes se solapan en producción, porque la segunda llamada reutilizó el búfer que la primera seguía leyendo. La misma regla de propiedad se aplica en sentido contrario a las devoluciones de llamada registradas mediante DLSetProgressCallback. Cualquier puntero que la biblioteca entregue a su devolución de llamada solo es válido durante el cuerpo de esa devolución, y el propio objeto de devolución debe permanecer vivo (anclado, en un anfitrión con recolección de basura) mientras la instancia pueda seguir invocándolo. Un delegado recogido a mitad de trabajo es la fuente clásica de la infracción de acceso «aleatoria» que aparece en un enlace .NET que funcionó sin problemas durante meses
Incorpore una prueba de humo en el propio enlace y ejecútela antes de distribuir cualquier conjunto de declaraciones generado. Pruebe una llamada de cada categoría que suele revelar errores de ABI: una función sin parámetros como DLCreateLibrary para demostrar que la convención es correcta, una función que recibe una cadena a la que se pase una ruta con caracteres no ASCII para demostrar que la codificación es correcta, una función que devuelve una cadena para demostrar que el tratamiento del búfer prestado es correcto y una operación que falle a propósito para observar cómo llega un error a su anfitrión. Son quince minutos de trabajo y detectan los fallos de convención de llamada y codificación que, de otro modo, aparecerían meses después como un volcado de fallo de un cliente
El caso de Python ctypes, en concreto
Python ctypes es el enlace que más a menudo veo hecho a mano y permite mostrar fácilmente la diferencia entre plataformas. En Windows, cargue la biblioteca con ctypes.WinDLL para que ctypes aplique Stdcall, enlace las funciones W sin sufijo y declare todos los parámetros de cadena como c_wchar_p. En macOS, cárguela con ctypes.CDLL para Cdecl, conserve la lista de funciones idéntica y resuelva los nombres sin el guion bajo inicial. La mayoría de las capas FFI, incluido ctypes, restauran por usted la convención del guion bajo en macOS, pero esa es la única suposición que debe confirmar con una llamada resuelta antes de generar cientos de declaraciones sobre ella
Dos cuestiones de despliegue acompañan al trabajo de enlace y tienen respuestas claras. La DLL normal no necesita registro: regsvr32 solo se aplica a la compilación ActiveX y la DLL se distribuye copiando el archivo, lo que es la principal razón para preferirla en servicios y contenedores Windows donde prefiere no tocar en absoluto el registro. La seguridad entre hilos se reduce a la regla ya indicada, una instancia por hilo. El identificador de instancia contiene todo el estado mutable que controla el motor, el documento seleccionado, las opciones de representación y la configuración de extracción, por lo que dos hilos que comparten una instancia intercalan el estado del otro incluso cuando cada llamada individual devuelve éxito
Una vez que el enlace es sólido, las operaciones al otro lado son exactamente las que los artículos de Delphi explican en detalle, incluida la aplicación y auditoría del cifrado PDF y la extracción de texto e imágenes de documentos existentes
Las descargas binarias de las tres capas de integración se distribuyen con la biblioteca; consulte la página de producto de PDF Library for Delphi para ver las ediciones y las licencias