El reporte de error decía "funciona desde C# en Windows, se bloquea desde Python en macOS". El equipo había construido su enlace para macOS copiando el archivo de declaraciones de Windows y cambiando el nombre del binario. Todos los símbolos resolvían, la primera llamada devolvía basura y la segunda se caía. No había nada mal en su lógica PDF: las exportaciones de Windows usan la convención Stdcall, mientras que la dylib de macOS exporta las mismas funciones como Cdecl con un guion bajo inicial, y un enlace de funciones externas que ignora cualquiera de esos detalles corrompe la pila antes de que siquiera se abra un documento.
PDFlibPas, el motor PDF de losLab con código fuente disponible para Delphi y C++Builder, envuelve todo su modelo de objetos en una sola clase fachada plana, TPDFlib, y luego entrega esa fachada en tres formas binarias: 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 las tres. Lo que cambia — y lo que este artículo mapea — es la ABI: convenciones de llamada, codificaciones de cadenas, propiedad de handles y quién tiene permiso de liberar cada búfer.
Una fachada, tres formas binarias
Cada función pública de TPDFlib tiene una contraparte plana llamada DL más el nombre del método: LoadFromFile se convierte en DLLoadFromFile, Encrypt en DLEncrypt, NewSignProcessFromFile en DLNewSignProcessFromFile. El primer parámetro de casi todas las exportaciones es un InstanceID devuelto por DLCreateLibrary, que sustituye la referencia de objeto que tendría un llamador Delphi. Vale la pena interiorizar temprano esta asignación uno a uno, porque significa que la referencia de API Delphi también documenta todos los demás lenguajes — lo que la clase puede hacer, la DLL puede hacerlo bajo un nombre predecible.
La compilación de Windows produce PDFlibDLL32.dll y PDFlibDLL64.dll; elijan la que coincida con la cantidad de bits del proceso host, ya que un proceso Java o .NET de 64 bits no puede cargar la biblioteca de 32 bits sin importar cómo se vea la declaración.
Windows: instancias Stdcall y pares de funciones W/A
Cada exportación que acepta cadenas existe dos veces: una versión wide que toma PWideChar (UTF-16, el ajuste natural para .NET, Java y c_wchar_p de Python) y una versión con sufijo A que toma PAnsiChar. Ambas son semánticamente idénticas y solo difieren en codificación, justo por eso mezclarlas es tan doloroso de depurar: nada falla, simplemente obtienen mojibake en metadatos o "archivo no encontrado" para rutas con cualquier carácter fuera de ASCII.
// Windows binding (PDFlibDLL64.dll): Stdcall, plain export names
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';
// macOS binding: same function, Cdecl, and an underscore prefix on the export
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Elijan un ancho de caracteres por host y codifíquenlo en el generador de enlaces. Una regla práctica: si el lenguaje host tiene cadenas UTF-16 nativas, enlacen las versiones W en todas partes y no vuelvan a tocar la familia A.
macOS: mismos nombres, ABI diferente
La dylib exporta el mismo conjunto de funciones DL, pero con dos cambios sistemáticos: la convención de llamada es Cdecl, y cada nombre exportado lleva un guion bajo inicial — _DLCreateLibrary, _DLLoadFromFile, y así sucesivamente. Ambos cambios son mecánicos, lo que los vuelve candidatos perfectos para un enlace generado y pésimos candidatos para copias editadas a mano del archivo de Windows. Si la capa de enlace lo permite, mantengan una lista canónica de funciones y emitan declaraciones por plataforma; el modo de fallo de no hacerlo es la historia de corrupción de pila con la que abrió este artículo, y solo se reproduce en la plataforma que menos prueban.
Hosts COM y ActiveX: Safecall y cargas Olevariant
Para VB.NET, C#, VBScript y hosts de automatización heredados, la compilación OCX envuelve la misma fachada en un objeto de automatización IDispatch, IPDFlibrary, con cada método declarado Safecall. Esa elección de convención importa para el manejo de errores: Safecall traduce fallos internos a valores COM HRESULT, de modo que un llamador C# ve una excepción capturable en lugar de un código de error silencioso — lo opuesto a la DLL plana, donde ustedes deben comprobar los valores de retorno.
La segunda regla específica de COM tiene que ver con datos binarios. No hay parámetros puntero en la interfaz de automatización; todo lo binario — bytes de imagen que entran, bytes PDF que salen — cruza el límite como Olevariant, mediante métodos como AddImageFromVariant y AppendToVariant. Pasar un arreglo de bytes a un variant es una línea en .NET, pero si intentan pasar un puntero crudo porque "de todos modos es el mismo proceso", la capa dispatch lo rechazará o lo deformará. Por último, recuerden que el registro COM depende de la cantidad de bits: un OCX registrado con regsvr32 de 32 bits es invisible para un host de 64 bits, lo que aparece en sitio del cliente como el famoso y poco útil "clase no registrada".
Disciplina de handles: las instancias son dueñas de los documentos
La API plana es una economía de handles. DLCreateLibrary devuelve una instancia; cargar devuelve un ID de documento dentro de esa instancia; los procesos de firma, listas de cadenas y archivos de acceso directo devuelven más handles enteros acotados a la misma instancia. El ciclo de vida canónico se ve así desde cualquier host FFI, escrito aquí en Pascal para legibilidad:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // one instance per worker thread
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); // frees every document the instance owns
end;
end;
De ahí se desprenden dos consecuencias. Primero, DLReleaseLibrary es la única limpieza estrictamente necesaria — destruye todos los documentos y handles de proceso bajo la instancia — pero apoyarse en eso dentro de un servicio de larga duración es una fuga lenta con pasos extra; liberen los documentos que ya no usen. Segundo, la instancia es la unidad natural de aislamiento por hilo: den a cada worker thread su propio InstanceID y nunca compartan uno entre hilos sin bloqueo externo, exactamente igual que no compartirían un objeto TPDFlib.
Las cadenas devueltas son prestadas, no propias
Las funciones que devuelven texto, como DLGetPageText, entregan un PWideChar o PAnsiChar que apunta a un búfer propiedad de la instancia de biblioteca y reciclado por ella. El contrato es: copiar de inmediato, nunca liberar.
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointer into a library-owned buffer
PageText := P; // copy now; a later call may reuse the buffer
end;
En C# esto significa convertir el IntPtr a una cadena administrada antes de la siguiente llamada a la biblioteca; en Python ctypes, extraer de inmediato la cadena wide desde el puntero. Conservar el puntero crudo entre llamadas es el tipo de bug que pasa todas las pruebas unitarias y falla bajo concurrencia de producción. La misma regla de propiedad se aplica en sentido inverso a los callbacks registrados con DLSetProgressCallback: cualquier puntero que la biblioteca entregue a su callback solo es válido durante ese callback, y el callback mismo debe permanecer vivo — fijado, en hosts con garbage collector — mientras la instancia pueda invocarlo. Un delegate recolectado a mitad de trabajo es la causa canónica de violaciones de acceso "aleatorias" en enlaces .NET que funcionaron durante meses.
Por último, incorporen la prueba de humo dentro del enlace. Antes de entregar un conjunto de declaraciones generado, ejecuten una llamada de cada categoría: una función sin parámetros (DLCreateLibrary), una función con cadena de entrada usando una ruta no ASCII, una función con cadena de salida y una operación que falle a propósito para ver cómo aparecen los códigos de error en el host. Quince minutos de esto capturan errores de convención y codificación que de otro modo llegan como volcados de caída del cliente.
Preguntas de enlace que aparecen en soporte
¿Qué funciones debe usar un enlace Python ctypes en Windows? Carguen la DLL con WinDLL (Stdcall), enlacen las funciones W sin sufijo y declaren los parámetros de cadena como c_wchar_p. En macOS, cambien a CDLL, mantengan la misma lista de funciones y resuelvan nombres sin el guion bajo — el loader de macOS maneja la convención de prefijo por ustedes en la mayoría de capas FFI, pero verifiquen con una llamada antes de generar cientos.
¿Necesito registrar algo para usar la DLL simple? No. El registro con regsvr32 aplica solo a la compilación ActiveX. La DLL se despliega copiando archivos, una de las razones para preferirla en servicios y cargas de trabajo Windows en contenedores.
¿La DLL es thread-safe? El patrón seguro es una instancia por hilo. El handle de instancia contiene todo el estado mutable — documento seleccionado, opciones de renderizado, configuraciones de extracción — por lo que dos hilos que compartan una instancia entrelazarán cambios de estado de forma silenciosa incluso cuando sus llamadas tengan éxito.
Lecturas relacionadas
Una vez que el enlace está en su lugar, las operaciones que expone son las mismas que los artículos de Delphi cubren en profundidad — por ejemplo aplicar y auditar cifrado PDF, o extraer texto e imágenes de documentos existentes.
Las descargas binarias para las tres capas de integración se entregan con la biblioteca; consulten la página del producto PDFlibPas para ediciones y licenciamiento.