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 automáticamente, mientras que public name exporta la cadena que usted escribió, carácter a carácter. HotPDF tiene que satisfacer ambas convenciones en el mismo árbol fuente, porque la compilación de Delphi ya incluye declaraciones de importación que deletrean el guion bajo a mano. Equivocarse con esta asimetría produce errores de enlace que mencionan un símbolo que nadie escribió

Extender una biblioteca Delphi a Free Pascal suele describirse como un problema de portabilidad, y en Win64 lo es en su mayoría. Win32 es distinto. La ABI de Windows x86 de 32 bits carga treinta años de convenciones acumuladas sobre cómo se deletrean los símbolos C, quién limpia la pila y qué helpers privados del compilador puede asumir una unidad de traducción, y cada uno de esos puntos es un sitio donde dos compiladores Pascal que concuerdan en el lenguaje pueden seguir discrepando sobre el fichero 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 fichero de objetos en Win32, y deflate en Win64. Ese comportamiento es correcto y coincide con lo que emite un compilador C. La trampa está al otro lado del puente: una rutina marcada public name 'deflate' exporta exactamente deflate en ambos objetivos, sin añadir prefijo

Añada ahora 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 ficheros de objetos. Dele la misma declaración a FPC en Win32 y el compilador la antepone de nuevo diligentemente, así que el enlazador persigue __deflate, un símbolo que nadie exporta. El arreglo intuitivo, añadir un guion bajo en todas partes, 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 llanas y otro para las importaciones que ya llevan un prefijo del lado Delphi, y en Win64 ambas constantes están vacías para que los nombres de enlace existentes sobrevivan intactos. Dos constantes en lugar de una es todo el arreglo, y solo resulta obvio cuando ha separado 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 objetivo de 32 bits, una declaración Delphi ya deletreada con guion bajo se convierte en __deflate y falla al enlazar, mientras que las exportaciones public name se mantienen literales en ambas arquitecturas
Una constante de prefijo no puede servir a ambas reglas: las importaciones cdecl llanas y las que ya llevan 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 llanas y las que ya llevan
// un prefijo Delphi escrito a mano se decoran distinto bajo FPC/Win32
const
{$IF DEFINED(FPC) and DEFINED(CPU32)}
  CPrefix     = '_';   // FPC lo añade él solo para cdecl external
  DelphiCName = '';    // ya deletreado con el guion bajo en el fuente
{$ELSE}
  CPrefix     = '';
  DelphiCName = '';
{$IFEND}

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

WIN32 le dice la arquitectura, no la ABI

Este es el error de compilación condicional con la cola de depuración más larga, y merece enunciarse sin rodeos: WIN32 y WIN64 describen la arquitectura objetivo y no dicen nada sobre qué helpers de runtime privados del compilador existen. Free Pascal define ambos símbolos en los objetivos Windows correspondientes, exactamente igual que Delphi. Un guard escrito como {$IFDEF WIN32} alrededor de código que llama a un helper del runtime de Delphi, por tanto, compila bajo FPC y falla en el momento del enlace

En concreto, tres familias de código caen en esta trampa. Los trampolines de enteros de 64 bits de Delphi alcanzados a través de los helpers System.@_ll, las rutinas de soporte en ensamblador Win32 de MSVC, y las ranuras de importación que los acompañan existen todos para servir objetos C precompilados que la compilación de Delphi enlaza. Free Pascal no enlaza esos objetos, así que no necesita nada 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 informa de algo poco útil sobre un identificador que no puede emparejar con nada

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

Acotar declaraciones e implementaciones juntas

Un bloque condicional en la sección de interfaz es fácil de pisar sin darse cuenta, y el mensaje de error resultante señala a cualquier parte menos a la causa. Añada una declaración de método a la interfaz de una clase y el sitio natural para ponerla es junto a los métodos relacionados, lo cual va bien hasta el momento en que esos vecinos resultan estar dentro de un bloque {$IFDEF} existente. Las directivas condicionales no se indentan, así que un bloque que se abrió cuarenta líneas arriba es esencialmente invisible mientras lee las declaraciones de alrededor

Lo que pasa después 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 informa de una larga lista de quejas sobre identificadores de método que esperaba y no encontró. Ninguno de los mensajes menciona el bloque condicional que lo causó

Dos hábitos previenen toda esta clase de fallos. Antes de insertar en una sección de interfaz, mire hacia arriba buscando el condicional abierto más cercano en lugar de fiarse de 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 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 justo en el código menos dispuesto a cambiar: Free Pascal de 32 bits no acepta un UInt64 como variable de control de un bucle for. En las unidades de curvas elípticas que llevan X25519 y X448, los bucles que recorren arrays de limbs se escribieron con contadores de 64 bits simplemente porque todo lo demás del fichero es de 64 bits

El arreglo tiene que ser quirúrgico, porque en la aritmética de cuerpos el ancho de una variable es parte del argumento de corrección. Los índices de bucle pasan a ser Integer, ya que un array de limbs tiene un puñado de elementos y ningún índice se acerca jamás al rango de 32 bits. Todo lo que participa en la aritmética, los propios limbs, la propagación del acarreo y las máscaras, se queda en UInt64, porque estrechar cualquiera de eso cambia silenciosamente el resultado módulo el primo del cuerpo

// FPC de 32 bits rechaza una variable de bucle UInt64: estreche solo el
// índice; limbs, máscaras y acarreos conservan su ancho o cambia la matemática
var
  I: Integer;                 // 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 así no puede ser un test de ida y vuelta. Cifrar y descifrar con la misma implementación rota se autoconfirma a la perfección, que es por lo que los vectores de respuesta conocida no son negociables aquí: ejecute los vectores de test publicados de X25519 y X448 y compare los bytes exactos de salida. Es la única comprobación que distingue una implementación correcta de una equivocada pero autoconsistente, y aplica igualmente a las primitivas simétricas tratadas en las fronteras de códec deflate y AES de Free Pascal

Los dos puntos de rotura de una compilación Win32 Free Pascal de HotPDF: un guard {$IFDEF WIN32} alrededor de helpers del runtime de Delphi que compila pero falla al enlazar salvo que declaración e implementación se excluyan juntas, y la variable de bucle UInt64 en los recorridos de limbs de X25519 y X448 estrechada a Integer mientras limbs, acarreos y máscaras conservan su ancho
Guarde sobre el compilador cuando la pregunta es ABI o soporte de runtime y sobre la arquitectura cuando es ancho de puntero, y luego demuestre los cambios de aritmética contra vectores de respuesta conocida publicados en lugar de tests de ida y vuelta

Qué vale una compilación Win32 Free Pascal

La recompensa práctica es que una aplicación Lazarus dirigida a Windows de 32 bits obtiene el mismo motor de documentos que su equivalente Delphi, sin un contrato binario separado que mantener. Eso importa más en los despliegues de los que casi nadie habla: controladores industriales, terminales de punto de venta y software de línea de negocio de larga vida 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 se describe en el soporte de Free Pascal y Lazarus en Win64. Win32 no es una repetición de aquella. Win64 tiene una convención de llamada, ninguna decoración de nombres y ningún helper de enteros privado de Delphi que rodear, así que casi todo en este artículo es específico del objetivo de 32 bits. Las unidades de aritmética que necesitaron el cambio de variable de bucle son las mismas descritas en la 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. Ambos compiladores aceptan aquí el mismo Object Pascal. Lo que difiere es el fichero de objetos: cómo se deletrean los símbolos, qué rutinas helper se asume que proporciona el runtime y qué objetos precompilados entran en el enlace. HotPDF envía los paquetes Free Pascal y Lazarus junto a los de Delphi y C++Builder en el componente PDF Delphi HotPDF, de modo que el mismo árbol fuente alimenta todas las toolchains en lugar de bifurcarse por compilador