Artículo técnico

Trampas LP64 en Delphi Linux64: Longint, punteros y WIN64

Delphi Linux64 sigue el modelo de datos LP64, así que allí Longint y LongWord ocupan 8 bytes, mientras que Delphi en Windows (Win32 y Win64) y Free Pascal en todos sus objetivos los mantienen en 4. Portar PDFlibPas, la PDF Library para Delphi, a dcclinux64 choca con esa diferencia de tres formas distintas, además de una trampa de ancho de puntero: un test {$IFDEF WIN64} que elige un entero del tamaño de un puntero escoge en silencio el ancho equivocado en todos los objetivos de 64 bits que no son Windows

Ninguna de estas trampas es exótica. Cada una es un hábito que Windows vuelve seguro por accidente: escribir Integer donde la RTL dice Longint, reflejar una estructura de C con campos Longint, o preguntarse «¿esto es Win64?» cuando la pregunta real es «¿un puntero ocupa 8 bytes?». El código puede arrastrar esos hábitos durante años sin consecuencia, porque en Win32, Win64 y FPC las dos grafías significan lo mismo. En Delphi Linux64 dejan de significar lo mismo, y el compilador es el primero en enterarse

Todo lo que sigue va de lo que el compilador caza. PDFlibrary.pas compila con dcc32, dcc64 y dcclinux64 desde un único árbol de fuentes, y estos son los problemas que se interponen entre los hábitos exclusivos de Windows y ese tercer objetivo

¿Qué cambia cuando Delphi se compila para Linux64?

Los tipos enteros que cambian de ancho en Delphi Linux64 son exactamente Longint y LongWord; Integer y Cardinal se quedan en 4 bytes en todas las plataformas de Delphi, y NativeInt sigue al puntero. Windows es LLP64, donde en 64 bits solo crecen los punteros. Los objetivos POSIX de 64 bits de Delphi (Linux64 y los compiladores de 64 bits de macOS e iOS) son LP64, donde el long de C también crece, y Embarcadero hizo que el Longint de Pascal lo acompañara

TipoDelphi Win32Delphi Win64Delphi Linux64Free Pascal
Integer / Cardinal4444
Longint / LongWord4484 en todos los objetivos
NativeInt / Pointer488el tamaño de puntero del objetivo
C long (ABI de la plataforma)448sigue la ABI del SO

La columna de Free Pascal es la trampa para una biblioteca multi-compilador. PDFlibPas ya distribuye una compilación Linux vía FPC, y FPC mantiene Longint en 32 bits en Linux x86-64. O sea que «funciona en Linux» era cierto y no nos decía nada sobre Delphi Linux64. El backend no-Windows de FPC resultó ser la base correcta para el port a Delphi, pero los tamaños de tipo que hay debajo difieren

Matriz de anchos de Integer, Longint, NativeInt y C long en los objetivos Delphi Win32, Win64, Linux64 y Free Pascal de la PDF library PDFlibPas
solo Longint, LongWord y el C long se ensanchan en Delphi Linux64, mientras FPC los mantiene en 4 bytes en todos los objetivos

¿Por qué un check de WIN64 trunca punteros en Linux?

La respuesta corta: WIN64 significa «Windows de 64 bits», no «64 bits», y tratar ambos como sinónimos elige el ancho de entero equivocado. PDFlibDither, un ditherer de difusión de error Floyd-Steinberg que reduce un bitmap de 24 bits a pocos niveles por canal, recorre tres filas de términos de error con aritmética de punteros cruda, y el entero del tamaño de un puntero detrás de esa aritmética tiene que seguir el ancho de la CPU, no el del sistema operativo. El condicional ingenuo se equivoca:

// Ingenuo: el ancho lo elige el SO, así que todos los objetivos de 64 bits no-Windows obtienen Integer
{$IFDEF WIN64}
Type
  OSPtr= IntPtr;
{$ELSE}
Type
  OSPtr= Integer;
{$ENDIF}

// El barrido serpenteante avanza los punteros de fila de error un término cada vez
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm));  // FPC x86-64: reescribe solo los 32 bits bajos
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm));  // dcclinux64: E2064, left side cannot be assigned to

// Correcto: un tipo del tamaño de un puntero en todas partes
Type
  OSPtr= TPLIntPtr;  // PDFlibTypes: NativeInt fuera de Windows, IntPtr / Integer en Win64 / Win32

Con ese condicional, OSPtr se resuelve en un Integer de 4 bytes en FPC para Linux x86-64, macOS x64 y ARM64, y Inc(OSPtr(ErrorR), ...) reinterpreta los primeros 4 bytes de un puntero de 8 bytes como un entero y los incrementa. En una CPU little-endian eso es la mitad baja de la dirección. La mitad alta jamás se toca, y ahí está lo bien que se esconde el error: el resultado es correcto justo hasta que los 32 bits bajos dan la vuelta, momento en el que el puntero salta silenciosamente 4 GiB hacia atrás. FPC compila el typecast de variable sin quejarse. Delphi rechaza un typecast de variable a un tipo de tamaño distinto a la izquierda de una asignación, así que dcclinux64 se para en E2064 en el primer Inc

Esa es la asimetría útil aquí: un compilador que rechaza el typecast encuentra un error de ancho de puntero que otro compilador acepta en silencio, antes de que el código haya ejecutado una sola línea en el nuevo objetivo. PDFlibPas usa TPLIntPtr de PDFlibTypes para esto, que comprueba OS_WINDOWS primero y solo después distingue Win64 de Win32, de modo que los objetivos no-Windows siempre obtienen NativeInt

La regla que nos llevamos: nunca infieras el ancho del puntero desde un define de sistema operativo. Si necesitas un entero del tamaño de un puntero, usa NativeInt / NativeUInt (los tienen tanto Delphi como FPC) o un alias de proyecto construido sobre ellos, y córreate el condicional entero. Si de verdad necesitas ramificar por ancho en código Delphi, CPU64BITS es el define que responde la pregunta que estás haciendo de verdad

Flujo de decisión para tipos de ancho de puntero en código Delphi de PDFlibPas: un define WIN64 escoge un Integer de 4 bytes en objetivos de 64 bits no-Windows hasta que NativeInt lo evita
el define WIN64 pregunta por el sistema operativo, así que los tipos de ancho de puntero tienen que salir de NativeInt o de un test CPU64BITS

Overrides de TStream: copia la firma de la RTL, carácter por carácter

Un descendiente de TStream en Delphi Linux64 debe declarar e implementar Read, Write y SetSize con parámetros Longint, exactamente como los declara la RTL, porque Integer ya no es el mismo tipo. PDFlibPas tiene su propio TPDFlibWideFileStream para nombres de archivo Unicode, y su declaración de clase sí usaba Longint. La sección de implementation usaba Integer

Type
  TPDFlibWideFileStream= Class(TStream)
  Protected
    Procedure SetSize(NewSize: Longint); Override;
  Public
    Function Read(Var Buffer; Count: Longint): Longint; Override;
    Function Write(Const Buffer; Count: Longint): Longint; Override;
  End;

// Antes: aceptado durante años en Win32, Win64 y FPC, donde Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;

// Después: la cabecera de implementación repite la declaración exactamente
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Longint): Longint;
Begin
  Result:= FileRead(FHandle, Buffer, Count);
  If Result= - 1 Then
    Result:= 0;
End;

En todos los compiladores que la biblioteca había conocido, Integer y Longint eran el mismo tipo de 4 bytes, así que una cabecera descuadrada era indistinguible de una que encajaba. En Delphi Linux64 la implementación ya no encaja con su propia declaración y la unidad deja de compilar. Este duele más de lo que su tamaño sugiere, porque PDFlibExtra es una unidad base de la que depende casi toda la biblioteca, así que una sola cabecera de implementación descuadrada deja sin compilar a la mayoría de las unidades

El mismo razonamiento funciona al revés para código nuevo. Cuando sobreescribes un método de la RTL, copia sus tipos de parámetro literalmente, aunque «sepas» que dos tipos son el mismo. Delphi Linux64 es sencillamente el primer objetivo donde ese saber deja de ser cierto

¿Qué registros necesitan Integer en vez de Longint, y cuáles deben conservarlo?

Un registro que refleja un formato de archivo o una estructura de Windows debe usar tipos de ancho fijo (Integer, Cardinal, Int32), mientras que un registro que refleja una estructura de C declarada con long debería conservar Longint / LongWord. Los dos casos se ven idénticos en el fuente y necesitan respuestas opuestas, porque el LONG de Windows siempre es de 32 bits y el long de C es lo que diga la ABI de la plataforma

Matriz de decisión para campos de registros Delphi en LP64: Integer de ancho fijo para bytes en disco, Longint conservado para bindings de C long en PDFlibPas
los registros que reflejan bytes toman anchos fijos, y solo los bindings de un C long conservan Longint, respaldados por un assert de tamaño de una línea
Type
  // Uniscribe GOFFSET: dos LONG de Windows, siempre 8 bytes en total
  TGoffset= Packed Record
    du: Integer;  // era Longint: el registro creció a 16 bytes en Delphi Linux64
    dv: Integer;
  End;

  // EMF LOGCOLORSPACE: cabecera rellenada con un Move fijo de 68 bytes desde los datos del registro
  TPLEMFLogColorSpace= Packed Record
    Signature, Version, Size, ColorType, Intent: Cardinal;
    Endpoints: Array[0..8] Of Integer;  // era Longint: 36 bytes se convertían en 72
    Gamma: Array[0..2] Of Cardinal;
    Filename: Array[0..259] Of WideChar;
  End;

  // PDFium FPDF_FILEACCESS: m_FileLen es un unsigned long de C, así que LongWord es lo correcto
  FPDF_FILEACCESS= Record
    m_FileLen: LongWord;  // 4 bytes en Windows, 8 en LP64, igual que el lado C
    m_GetBlock: TFPDF_GetBlock;
    m_Param: Pointer;
  End;

El caso EMF muestra lo calladamente que esto falla. El importador copia 68 bytes a TPLEMFLogColorSpace: 20 bytes de cabecera, 36 bytes de endpoints CIE, 12 bytes de gamma. Con endpoints Longint en LP64, esos mismos 68 bytes caerían en un array de endpoints de 72 bytes, los valores de gamma nunca se escribirían y nada lanzaría un error. Ambos registros viven en rutas de código exclusivas de Windows y usan tipos de ancho fijo de todas formas, porque un registro que refleja bytes en disco no debería depender de qué compilador lo compile en cada momento

El binding de PDFium es el contraejemplo. Su m_FileLen refleja un unsigned long de C, que en Windows es de 32 bits y en LP64 de 64 bits, y LongWord sigue eso con exactitud en ambos. Reescribirlo como Cardinal en una pasada de limpieza lo rompería en el momento en que el host de PDFium gane un loader POSIX. El host de PDFium es exclusivo de Windows, así que esto es una nota para el futuro. El lado de convenciones de llamada de los bindings de C tiene trampas propias, cubiertas en el artículo sobre los bindings DLL, ActiveX y dylib de la PDF Library

Una guarda barata para cualquier registro que refleje bytes: verifica su tamaño con un assert al arrancar, por ejemplo Assert(SizeOf(TGoffset) = 8) en una sección de initialization. No cuesta nada y convierte un cambio silencioso de layout en un fallo inmediato en la primera ejecución de un objetivo nuevo

Ajustar los defines condicionales: DELPHI_LINUX, Android y Types

Antes de que pudiera asomar cualquiera de los problemas de tipos, Delphi en Linux tenía que dejar de hacerse pasar por Windows. PDFlibDefines.inc no definía nada para Delphi Linux, así que el compilador caía directo en los valores por defecto de Windows/VCL, y el primer error era que no encontraba Vcl.Graphics. Los defines encaminan Delphi Linux al mismo backend que FPC usa en objetivos no-Windows: renderizado Cairo, fontconfig, OpenSSL y libcurl cargados en tiempo de ejecución

{$IFDEF LINUX}
{$IFNDEF FPC}
{$IFNDEF ANDROID}
{$DEFINE DELPHI_LINUX}
{$DEFINE NOVCL}
{$DEFINE MD5_MAC}
{$DEFINE NOCRYPTAPI}
{$DEFINE USE_Cairo}
{$ENDIF}
{$ENDIF}
{$ENDIF}

Dos detalles de ese bloque se pasan por alto con facilidad. La exclusión de ANDROID está ahí porque Delphi también define LINUX para objetivos Android, y este backend no cubre Android. Y el define nuevo está deliberadamente separado de la rama existente de Delphi para macOS (OS_MAC sin FPC), que depende de unidades Macapi; el define de Linux se construye sobre la ruta no-Windows de FPC

El otro error recurrente era que TPoint y TRect quedaban sin declarar. En Windows esos tipos llegan a casi todas las unidades indirectamente, a través de la unidad Windows. En Delphi Linux no hay unidad Windows en la cláusula uses, así que unidades como PDFlibFont y PDFlibPageTree ahora listan Types explícitamente bajo DELPHI_LINUX. Si tu propio código compila en Windows solo porque Windows arrastra System.Types consigo, espera el mismo muro. El patrón general del código que solo funciona en un compilador por casualidad es el tema de cinco hábitos de Delphi que solo funcionan por accidente hasta que FPC los destapa

Donde el compilador deja de ayudar

Los problemas LP64 de arriba son los que un compilador puede encontrar. Los que no puede encontrar, como un registro del tamaño equivocado que nunca pasa por un typecast, solo aparecen cuando los tests se ejecutan de verdad en el objetivo, que es exactamente para lo que sirven los asserts de tamaño descritos arriba

Si lo que te trajo al código de dithering era la salida a 1 bit, la ruta soportada para páginas renderizadas es otra: la conversión descendente a halftone por GDI fuera del renderer, descrita en renderizar páginas PDF a bitmaps monocromos de 1 bit

El trabajo multi-compilador descrito aquí vive en la PDF Library para Delphi, que hoy se dirige a Delphi y C++Builder en Windows más compilaciones headless con Free Pascal en Linux y macOS; la página del producto detalla la lista completa de compiladores y plataformas soportados