Artículo técnico

Enlace de objetos OMF a COFF para FPC Win32 en PDFlibPas

PDFlibPas compila con Free Pascal para Windows de 32 bits, y lo difícil nunca fue el Pascal: fueron los archivos de objetos. Los objetos AES y OpenJPEG que enlaza la compilación de Delphi son OMF, el linker interno de Free Pascal exige COFF, y convertir entre ambos formatos produce nombres de sección y símbolos de definición de sección que hacen que el linker falle con internal errors en lugar de diagnósticos útiles

Quien alguna vez haya enlazado objetos C dentro de una biblioteca Pascal conoce este terreno. Win64 es comparativamente civilizado: un solo formato de objetos, una sola convención de llamada y sin decoración de nombres. Win32 conserva cada capa de historia que la plataforma acumuló, y una biblioteca que enlaza estáticamente código C de terceros se topa con todas a la vez

El directorio del compilador no le dice cuál es el target

Empiece por el punto de entrada de la compilación, porque equivocarse aquí quema horas antes de que aparezca cualquier archivo de objetos. El nombre de un directorio de instalación de Free Pascal identifica dónde vive el compilador principal, no qué produce. Un compilador anfitrión de 32 bits puede invocar un cross-compiler que tenga al lado y emitir código de 64 bits cuando se pasan los switches de target correctos, así que inferir el target desde una ruta es adivinar, y la apuesta funciona justo hasta que alguien reorganiza su toolchain

Lo confiable es preguntarle al compilador. Consulte el procesador y el sistema operativo del target real a través de los switches de información del propio compilador, y acepte los dos layouts de instalación comunes, el directorio binario plano y el anidado por versión, porque distintos instaladores y gestores de toolchain producen formas distintas. Un script de compilación que hard-codea cualquiera de los dos layouts funciona en exactamente una máquina

¿Por qué un archivo de objetos convertido rompe el linker interno?

Porque la conversión preserva la convención de nombres de sección de OMF y sintetiza símbolos de definición de sección que no coinciden con lo que el linker COFF espera. Convertir los objetos OMF a COFF es necesario pero no suficiente: los archivos resultantes arrastran los nombres de sección clásicos _TEXT, _DATA y _BSS, más los nombres de símbolos de definición de sección derivados de ellos, y alimentar eso al linker interno de Free Pascal produce internal compiler errors en lugar de un mensaje sobre el nombre de las secciones

Un internal error es el peor modo de fallo para un problema de compilación, porque no dice nada sobre qué estaba mal en la entrada. La solución es una pasada de normalización posterior a la conversión sobre el archivo COFF: reescribir los nombres de sección a la forma esperada y reescribir los símbolos de definición de sección correspondientes para que coincidan, sin tocar el índice de símbolos, los bytes de código ni las relocations. Esa última restricción es toda la dificultad: una reescritura que renumere símbolos o desplace offsets produce un objeto que enlaza bien y luego se cae en tiempo de ejecución

Hay un paso previo para uno de los dos sets de objetos. Los objetos OpenJPEG compilados con el clásico compilador de C++ de 32 bits dependen de rutinas helper privadas de Delphi para enteros de 64 bits, que Free Pascal no provee, así que ninguna conversión de formato los hace utilizables. Esos objetos se recompilan primero con el compilador basado en Clang, que no emite esas dependencias, y se convierten después

Pipeline que lleva los objetos C estáticos de PDFlibPas desde OMF de Delphi en Lib\thirdparty\Win32 hasta COFF enlazable por FPC en Lib\thirdparty\Win32f en Win32: conversión de OMF a COFF, una pasada de normalización que reescribe nombres de sección y símbolos de definición de sección sin tocar el índice de símbolos, los bytes de código ni las relocations, y la recompilación con Clang para los objetos OpenJPEG que llaman helpers de 64 bits de Delphi
Convertir es necesario pero no suficiente: entregue el COFF renombrado pero sin normalizar al linker interno de Free Pascal y responde con internal errors, así que una pasada posterior a la conversión arregla nombres y símbolos sin tocar offsets
// Los objetos del target FPC viven en su propio directorio. No reemplazan
// al set de objetos de Delphi, porque ambas toolchains compilan desde el
// mismo árbol de fuentes y cada una necesita sus propias entradas de link
//
//   Lib\thirdparty\Win32   objetos OMF de Delphi, sin cambios
//   Lib\thirdparty\Win32f  objetos COFF de FPC, convertidos y normalizados
//
// Puntos de entrada de compilación:
//   build-Win32-Lib-FPC.cmd
//   build-Win64-Lib-FPC.cmd

Los helpers privados del compilador no son portables, y sus convenciones tampoco

El runtime de Delphi provee trampolines en ensamblador para operaciones con enteros de 64 bits en x86 de 32 bits, y los objetos C precompilados para Delphi los llaman. Free Pascal tiene su propio arreglo, así que esas referencias hay que satisfacerlas de otra forma, no redirigirlas. El detalle que hace imposible la redirección es la convención de llamada: el helper de timing que usa el código de imaging limpia su argumento de cuatro bytes en el callee, mientras que el helper de división de 64 bits limpia dieciséis bytes y devuelve el resultado en el clásico par de registros. Dos helpers, dos convenciones, y un trampoline escrito para uno corrompe silenciosamente el stack del otro

La decoración de nombres agrega la otra mitad del problema. En Win32, Free Pascal antepone un guion bajo a las importaciones C externas de forma automática, mientras que exporta las declaraciones public name textualmente, así que el lado de importación y el lado de exportación del mismo puente siguen reglas distintas. El puente de C runtime que OpenJPEG necesita tiene que exportar los nombres de símbolos C exactos, y los puntos de entrada variádicos necesitan un salto indirecto de 32 bits en lugar de uno directo. Nada de esto es exótico una vez enunciado, y todo falla como un error de link que menciona un símbolo que nadie escribió

¿Qué hacía que un ejecutable Win32 muriera antes de main?

Una DLL de 64 bits en la ruta de búsqueda, alcanzada porque la unidad zlib de Free Pascal se enlaza dinámicamente en lugar de estáticamente. El síntoma era una salida inmediata con el código de estado de imagen inválida, antes de que corriera cualquier código Pascal del programa, lo que lo manda a revisar el ejecutable que acaba de compilar cuando la falla está en el loader resolviendo una importación contra la arquitectura equivocada

La lección va sobre supuestos, no sobre zlib. Una unidad con nombre de biblioteca de compresión no necesariamente contiene una; puede ser un binding que espera una biblioteca compartida en tiempo de ejecución, y una dependencia dinámica que usted no planeó es un pasivo de despliegue incluso cuando llega a resolverse. Cambiar a la implementación de stream en Pascal puro le da a ambos targets una ruta de compresión incluida estáticamente y sin ninguna dependencia externa, que es lo que una biblioteca embebida en la aplicación de otra persona debería tener desde el principio

El mismo instinto aplica para el backend del encoder JBIG2 externo. En el target de 32 bits el encoder externo no se enlaza, así que las peticiones caen al encoder Pascal incorporado, y el test que verifica esto tiene que revisar el estado de registro del target actual en lugar de tomar una codificación exitosa como prueba de que el backend externo está presente. Un fallback que funciona es justamente lo que esconde una dependencia faltante, que es el patrón de falla examinado en diagnóstico de fallas silenciosas de stubs. El trabajo de enlace estático de 64 bits está cubierto en enlace estático de jbig2enc bajo FPC

Flujo de diagnóstico para un ejecutable Win32 que sale antes de main bajo Free Pascal: el loader resuelve importaciones mientras corre la inicialización de unidades, el binding de zlib encuentra una DLL de 64 bits en la ruta de búsqueda, y el proceso muere con estado de imagen inválida antes de cualquier sentencia Pascal, lo que empujó a PDFlibPas hacia una ruta de compresión en Pascal puro incluida estáticamente
La falla nunca fue el programa recién compilado: una unidad llamada zlib era un binding de runtime resuelto contra la arquitectura equivocada, y un fallback que funciona, como el encoder JBIG2 incorporado, esconde una dependencia faltante

Aritmética de 32 bits en un memory stream

El código que manipula tamaños de buffer con aritmética sin signo del ancho del puntero es correcto en Win64 y está a una imagen grande de distancia del overflow en Win32. El stream en memoria que alimenta al codec JPEG 2000 crece duplicando y avanza sumando, y en un target de 32 bits ambas operaciones pueden hacer wrap con entradas grandes pero completamente legítimas

Por eso cada escritura, salto, seek y asignación inicial verifica antes de calcular, y el techo de capacidad es el valor con signo máximo del ancho del puntero, elegido para coincidir con lo que la rutina de movimiento de bloques y los valores de retorno del callback pueden expresar. El requisito de comportamiento cuando se rechaza una petición es fácil de equivocar: rechazar no debe cambiar ni la posición del stream ni su longitud. Una mutación parcial seguida de un error deja el stream en un estado que el llamador no puede razonar, y la siguiente operación lo empeora

// Verifique antes de calcular. En Win32 ambas operaciones hacen wrap con
// entradas que una imagen JPEG 2000 grande produce legítimamente
if Needed > NativeUInt(High(NativeInt)) - FPosition then
  Exit(False);                    // rechazar, dejar posición y tamaño intactos

NewCapacity := FCapacity;
while (NewCapacity < FPosition + Needed) do
begin
  if NewCapacity > NativeUInt(High(NativeInt)) shr 1 then
    Exit(False);                  // duplicar haría overflow
  NewCapacity := NewCapacity shl 1;
end;

Dos trampas del output de compilación que sobreviven al port

Separar los ejecutables de test y de ejemplos por arquitectura de target en directorios de output por target es obviamente correcto y rompe de inmediato todo lo que localizaba sus datos de test contando niveles de directorio hacia arriba. La solución es buscar hacia arriba el directorio de assets en lugar de asumir una profundidad fija, con una restricción deliberada: el ejemplo de firma acepta un certificado de fallback solo desde su propio directorio de proyecto, nunca desde un ancestro arbitrario, porque un certificado con el mismo nombre encontrado más arriba en el árbol es una sorpresa de seguridad, no una conveniencia

La segunda trampa sobrevive a cada port y vale la pena llevarla a cualquier proyecto FPC. Después de una actualización del compilador, no basta con que el compilador rechace archivos PPU viejos, porque el linker sigue prefiriendo archivos de objetos sobrantes en la ruta de búsqueda de unidades aunque el PPU que cargó viniera del directorio correcto, y agregar una ruta de output de objetos explícita no invalida esa preferencia. La única respuesta confiable es un directorio temporal de unidades fresco por cada ronda de compilación. Cualquier cosa menos que eso produce un binario enlazado desde dos versiones del compilador, que falla de maneras que parecen bugs del código fuente

Los condicionales de plataforma son la última pieza, y elegir el eje correcto importa más de lo que parece. La pregunta correcta suele ser si el código es específico de Windows, no si una biblioteca de widgets particular está presente, como mostró el trabajo de conversión de metafiles en importación vectorial EMF y condicionales de plataforma: cambiar ese guard de una condición sobre la biblioteca de controles a una condición de plataforma convirtió una supuesta reescritura en el cambio de una directiva. El soporte de Free Pascal y Lazarus para ambos targets de Windows viene con la PDFlibPas Delphi PDF library, compilada desde las mismas fuentes que los paquetes de Delphi y C++Builder