Artículo técnico

Trampas LP64 de 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 los destinos los mantienen en 4. Al llevar PDFlibPas, la PDF Library para Delphi, a dcclinux64, esa diferencia aparece de tres formas distintas, más una trampa de ancho de puntero: una prueba {$IFDEF WIN64} usada para elegir un entero del tamaño de un puntero elige en silencio el ancho equivocado en cada destino de 64 bits que no sea 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 cargar con esos hábitos años sin consecuencias, porque en Win32, Win64 y FPC ambas escrituras significan lo mismo. En Delphi Linux64 dejan de significar lo mismo, y el compilador es el primero en notarlo

Todo lo que sigue trata de lo que el compilador detecta. 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 destino

¿Qué cambia cuando Delphi apunta a 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 solo los punteros crecen en 64 bits. Los destinos POSIX de 64 bits de Delphi (Linux64 y los compiladores de 64 bits para macOS e iOS) son LP64, donde el long de C también crece, y Embarcadero hizo que el Longint de Pascal lo siguiera

TipoDelphi Win32Delphi Win64Delphi Linux64Free Pascal
Integer / Cardinal4444
Longint / LongWord4484 en todos los destinos
NativeInt / Pointer488el tamaño de puntero del destino
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. Así 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 tipos por debajo difieren

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

¿Por qué una prueba 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 debe seguir al CPU, no al sistema operativo. El condicional ingenuo se equivoca:

// Ingenuo: ancho elegido por el SO, así que cada destino de 64 bits no-Windows recibe Integer
{$IFDEF WIN64}
Type
  OSPtr= IntPtr;
{$ELSE}
Type
  OSPtr= Integer;
{$ENDIF}

// El barrido serpentina avanza los punteros de fila de error un término a la vez
Inc(OSPtr(ErrorR), SizeOf(TErrorTerm));  // FPC x86-64: solo reescribe los 32 bits bajos
Dec(OSPtr(ErrorG), SizeOf(TErrorTerm));  // dcclinux64: E2064, el lado izquierdo no puede asignarse

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

Con ese condicional, OSPtr se resuelve como 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 un CPU little-endian eso es la mitad baja de la dirección. La mitad alta nunca se toca, y por eso el error se esconde tan bien: el resultado es correcto justo hasta que los 32 bits bajos dan la vuelta, momento en que el puntero salta silenciosamente hacia atrás 4 GiB. FPC compila el typecast de variable sin quejarse. Delphi rechaza un typecast de variable a un tipo de tamaño distinto en el lado izquierdo de una asignación, así que dcclinux64 se detiene con 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 ejecute una sola línea en el nuevo destino. PDFlibPas usa TPLIntPtr de PDFlibTypes para esto, que prueba OS_WINDOWS primero y solo después distingue Win64 de Win32, de modo que los destinos no-Windows siempre reciben NativeInt

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

Flujo de decisión para tipos del ancho de un puntero en código Delphi de PDFlibPas: un define WIN64 elige un Integer de 4 bytes en destinos de 64 bits no-Windows hasta que NativeInt lo evita
el define WIN64 pregunta por el sistema operativo, así que los tipos del ancho de un puntero deben venir de NativeInt o de una prueba CPU64BITS

Overrides de TStream: copie 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 por años en Win32, Win64 y FPC, donde Integer = Longint
Function TPDFlibWideFileStream.Read(Var Buffer; Count: Integer): Longint;

// Después: el encabezado 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 cada compilador que la biblioteca había conocido, Integer y Longint eran el mismo tipo de 4 bytes, así que un encabezado sin coincidencia era indistinguible de uno coincidente. En Delphi Linux64 la implementación ya no coincide 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 fundacional de la que depende la mayor parte de la biblioteca, así que un solo encabezado de implementación sin coincidencia impide compilar la mayoría de las unidades

Para código nuevo el mismo razonamiento corre al revés. Cuando haga override de un método de la RTL, copie sus tipos de parámetros literalmente, incluso cuando "sabe" que dos tipos son iguales. Delphi Linux64 simplemente es el primer destino donde ese conocimiento deja de ser verdad

¿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 código 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 los bytes en disco, Longint conservado para bindings del long de C en PDFlibPas
los registros que reflejan bytes toman anchos fijos, y solo los bindings de un long de C conservan Longint, respaldados por una aserción de tamaño de una línea
Type
  // GOFFSET de Uniscribe: 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;

  // LOGCOLORSPACE de EMF: encabezado llenado 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 volvieron 72
    Gamma: Array[0..2] Of Cardinal;
    Filename: Array[0..259] Of WideChar;
  End;

  // FPDF_FILEACCESS de PDFium: 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 de EMF muestra lo silenciosamente que esto falla. El importador copia 68 bytes en TPLEMFLogColorSpace: 20 bytes de encabezado, 36 bytes de endpoints CIE, 12 bytes de gamma. Con endpoints Longint en LP64, esos mismos 68 bytes caerían en un arreglo de endpoints de 72 bytes, los valores de gamma nunca se escribirían y nada levantaría un error. Ambos registros viven en rutas de código exclusivas de Windows y de todos modos usan tipos de ancho fijo, porque un registro que refleja bytes en disco no debería depender de qué compilador lo construya

El binding de PDFium es el contraejemplo. Su m_FileLen refleja un unsigned long de C, que es de 32 bits en Windows y de 64 bits en LP64, y LongWord sigue eso exactamente en ambos. Reescribirlo como Cardinal en una pasada de limpieza lo rompería en cuanto 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 refleja bytes: afirme su tamaño una vez 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 destino nuevo

Acertar con los defines condicionales: DELPHI_LINUX, Android y Types

Antes de que cualquiera de los problemas de tipos pudiera aparecer, 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 fue que Vcl.Graphics no se encontraba. Los defines enrutan Delphi Linux al mismo backend que FPC usa en destinos 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 son fáciles de pasar por alto. La exclusión ANDROID está ahí porque Delphi define LINUX también para destinos Android, y este backend no cubre Android. Y el define nuevo está deliberadamente separado de la rama existente para Delphi macOS (OS_MAC sin FPC), que depende de unidades Macapi; el define de Linux se apoya en la ruta no-Windows de FPC

El otro error recurrente fue TPoint y TRect sin declarar. En Windows esos tipos llegan a la mayoría de 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 su propio código compila en Windows solo porque Windows arrastra System.Types consigo, espere el mismo muro. El patrón general de código que solo funciona en un compilador por coincidencia es el tema de cinco hábitos de Delphi que solo funcionan por accidente hasta que FPC los expone

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 las pruebas corren de verdad en el destino, que es exactamente para lo que sirven las aserciones de tamaño descritas arriba

Si lo que lo trajo al código de dithering en primer lugar fue la salida de 1 bit, la ruta soportada para páginas renderizadas es otra: la conversión hacia abajo por halftone de GDI fuera del renderizador, descrita en renderizar páginas PDF a bitmaps monocromos de 1 bit

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