Artículo técnico

Free Pascal Win32: decoración de símbolos C en HotPDF

Free Pascal en Win32 antepone un guion bajo a cada importación cdecl; external de forma automática, mientras que public name exporta la cadena que usted escribió, carácter por carácter. HotPDF tiene que satisfacer ambas convenciones en el mismo árbol de fuentes, porque la compilación de Delphi ya trae declaraciones de importación que deletrean el guion bajo a mano. Equivocarse en esta asimetría produce errores de link que nombran un símbolo que nadie escribió

Extender una biblioteca de Delphi a Free Pascal suele describirse como un problema de portabilidad, y en Win64 mayormente lo es. Win32 es distinto. El ABI de Windows x86 de 32 bits carga treinta años de convención acumulada sobre cómo se deletrean los símbolos C, quién limpia el stack, y qué helpers privados del compilador puede asumir una unidad de traducción, y cada uno de esos puntos es un lugar donde dos compiladores Pascal que concuerdan en el lenguaje pueden seguir discrepando sobre el archivo de objetos

¿Por qué el mismo símbolo se resuelve en Win64 y falla en Win32?

Porque el prefijo de guion bajo es una convención de 32 bits que Free Pascal aplica a las importaciones pero no a las exportaciones. Declare function deflate(...): Integer; cdecl; external; y FPC busca _deflate en el archivo de objetos en Win32, y deflate en Win64. Ese es comportamiento correcto y coincide con lo que emite un compilador de C. La trampa está del otro lado del puente: una rutina marcada public name 'deflate' exporta exactamente deflate en ambos targets, sin prefijo agregado

Ahora agregue el detalle histórico que lo hace concreto. La compilación de Delphi ya declara algunos de estos puntos de entrada con el guion bajo escrito en el nombre, porque eso es lo que contienen sus propios archivos de objetos. Pásele la misma declaración a FPC en Win32 y el compilador, obediente, le antepone el prefijo otra vez, así que el linker sale a cazar __deflate, un símbolo que nadie exporta. La solución intuitiva, agregar un guion bajo en todos lados, rompe las importaciones que ya estaban deletreadas correctamente

Lo que funciona es un par de constantes de prefijo en lugar de una sola. HPDFFPCZLib y HPDFFPCCodecStubs usan un prefijo para las importaciones C planas y otro para las importaciones que ya llevan un prefijo del lado Delphi, y en Win64 ambas constantes quedan vacías para que los nombres de link existentes sobrevivan intactos. Dos constantes en lugar de una es toda la solución, y solo es obvia una vez que separó la regla de importación de la regla de exportación

Las mismas declaraciones de símbolos C resueltas por Free Pascal y Delphi en Win64 y Win32: las importaciones cdecl ganan un guion bajo solo en el target de 32 bits, una declaración de Delphi ya escrita con guion bajo se convierte en __deflate y falla el link, mientras que las exportaciones public name quedan literales en ambas arquitecturas
Una sola constante de prefijo no puede servir a ambas reglas: las importaciones cdecl planas y las que ya cargan el guion bajo de Delphi se decoran distinto bajo FPC en Win32, así que HotPDF mantiene dos y deja ambas vacías en Win64
// Dos prefijos, no uno: las importaciones C planas y las que ya cargan
// un prefijo Delphi escrito a mano se decoran distinto bajo FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC lo agrega por su cuenta para cdecl external
  DelphiCName = '';    // ya escrito con el guion bajo en el fuente
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

// Lado de exportación: 'public name' es literal en cada target
procedure hpdf_codec_free(P: Pointer); cdecl;
  public name 'hpdf_codec_free';

WIN32 le dice la arquitectura, no el ABI

Este es el error de compilación condicional con la cola de debugging más larga, y vale la pena enunciarlo sin rodeos: WIN32 y WIN64 describen la arquitectura del target y no dicen nada sobre qué helpers privados del runtime del compilador existen. Free Pascal define ambos símbolos en los targets Windows correspondientes, exactamente como Delphi. Un guard escrito como {$IFDEF WIN32} alrededor de código que llama a un helper del runtime de Delphi compila bajo FPC y falla en tiempo de link

En concreto, tres familias de código caen en esta trampa. Los trampolines de enteros de 64 bits de Delphi alcanzados vía helpers System.@_ll, las rutinas de soporte en ensamblador Win32 de MSVC, y las ranuras de importación que las acompañan existen para servir objetos C precompilados que la compilación de Delphi enlaza. Free Pascal no enlaza esos objetos, así que no necesita ninguna de esa maquinaria, y toda referencia a ella tiene que desaparecer. La sutileza es que la declaración y la implementación tienen que excluirse juntas. Excluya solo una y el compilador reporta algo poco útil sobre un identificador que no puede casar con nada

La regla que se deduce es corta. Ponga el guard en el compilador cuando la pregunta es sobre ABI o soporte del runtime, póngalo en la arquitectura cuando la pregunta es sobre el ancho del puntero o el número de registros, y nunca deje que uno suplante al otro

Proteger declaraciones e implementaciones juntas

Caer dentro de un bloque condicional de la sección interface es fácil sin darse cuenta, y el mensaje de error resultante apunta a cualquier lugar menos a la causa. Agregue una declaración de método a la interfaz de una clase y el lugar natural es junto a los métodos relacionados, lo cual está bien hasta el momento en que esos vecinos casualmente viven dentro de un bloque {$IFDEF} existente. Las directivas condicionales no se indentan, así que un bloque que se abrió cuarenta líneas arriba es prácticamente invisible mientras usted lee las declaraciones de alrededor

Lo que sigue es una compilación que tiene éxito en una toolchain y produce una cascada en otra. Si el guard de alrededor es una comprobación de versión de Delphi que Free Pascal no satisface, la declaración desaparece para FPC mientras la implementación incondicional permanece, y el compilador reporta una lista larga de quejas sobre identificadores de métodos que esperaba y no encontró. Ninguno de los mensajes menciona el bloque condicional que lo causó

Dos hábitos previenen toda esta clase de fallas. Antes de insertar en una sección interface, mire hacia arriba buscando el condicional abierto más cercano en lugar de confiar en la agrupación visual. Y trate una suite de tests de Delphi en verde como evidencia solo sobre Delphi: la compilación de la biblioteca con Free Pascal es una compuerta separada, y la única forma de saber que pasa es ejecutar build-Win32-Lib-FPC.cmd y build-Win64-Lib-FPC.cmd como parte del mismo cambio

Qué se rompe en el código de aritmética de 32 bits

Una restricción del lenguaje aparece exactamente en el código menos dispuesto a cambiar: el Free Pascal de 32 bits no acepta un UInt64 como variable de control de un loop for. En las unidades de curvas elípticas que cargan X25519 y X448, los loops que recorren arreglos de limbs se escribieron con contadores de 64 bits simple y llanamente porque todo lo demás en el archivo es de 64 bits

El arreglo tiene que ser quirúrgico, porque en la aritmética de campos el ancho de una variable es parte del argumento de corrección. Los índices de loop se vuelven Integer, ya que un arreglo de limbs tiene un puñado de elementos y ningún índice se acerca al rango de 32 bits. Todo lo que participa en la aritmética, los limbs en sí, la propagación de carry y las masks, permanece UInt64, porque achicar cualquiera de esos cambia en silencio el resultado módulo el primo del campo

// El FPC de 32 bits rechaza una variable de loop UInt64. Achique solo el
// índice; limbs, masks y carries conservan su ancho o cambia la matemática
var
  I: Integer;                 // antes era UInt64
  Carry, Mask: UInt64;
begin
  Carry := 0;
  for I := 0 to High(Limbs) do
  begin
    Limbs[I] := Limbs[I] + Carry;
    Carry := Limbs[I] shr 51;
    Limbs[I] := Limbs[I] and Mask;
  end;
end;

La verificación de un cambio como este no puede ser un test de ida y vuelta. Cifrar y descifrar con la misma implementación rota concuerda consigo misma a la perfección, y por eso los known-answer vectors son innegociables aquí: ejecute los test vectors publicados de X25519 y X448 y compare los bytes de salida exactos. Esa es la única comprobación que distingue una implementación correcta de una equivocada pero autoconsistente, y aplica igual para las primitivas simétricas tratadas en los límites de los codecs deflate y AES en Free Pascal

Los dos puntos de rotura de una compilación Win32 de Free Pascal de HotPDF: un guard {$IFDEF WIN32} alrededor de helpers del runtime de Delphi que compila pero falla en el link salvo que declaración e implementación se excluyan juntas, y la variable de loop UInt64 en los recorridos de limbs de X25519 y X448 achicada a Integer mientras limbs, carries y masks conservan su ancho
Ponga el guard en el compilador cuando la pregunta es ABI o soporte del runtime y en la arquitectura cuando es ancho de puntero, y luego demuestre los cambios de aritmética contra known-answer vectors publicados en lugar de tests de ida y vuelta

Lo que vale una compilación Win32 con Free Pascal

El pago práctico es que una aplicación Lazarus que apunta a Windows de 32 bits obtiene el mismo motor de documentos que su contraparte Delphi, sin un contrato binario separado que mantener. Eso importa más en los despliegues de los que la gente rara vez habla: controladores industriales, terminales de punto de venta y software de línea de negocio de vida larga donde el runtime de 32 bits no es una elección heredada sino una restricción de hardware

La historia de Win64 vino primero y está descrita en soporte de Free Pascal y Lazarus en Win64. Win32 no es una repetición de esa. Win64 tiene una convención de llamada, sin decoración de nombres y sin helpers de enteros privados de Delphi que sortear, así que casi todo en este artículo es específico del target de 32 bits. Las unidades de aritmética que necesitaron el cambio de variable de loop son las mismas descritas en aritmética de Montgomery sobre las curvas NIST, donde la disciplina de anchos se explica con más profundidad

La lección general es que el trabajo de portabilidad entre compiladores no va principalmente de características del lenguaje. Aquí ambos compiladores aceptan el mismo Object Pascal. Lo que difiere es el archivo de objetos: cómo se deletrean los símbolos, qué rutinas helper se asume que provee el runtime, y qué objetos precompilados entran al link. HotPDF distribuye los paquetes de Free Pascal y Lazarus junto con los de Delphi y C++Builder en el HotPDF Delphi PDF component, de modo que el mismo árbol de fuentes alimenta a todas las toolchains en lugar de bifurcarse por compilador