Artículo técnico

Matriz de compiladores Delphi: HotXLS desde XE5

HotXLS distribuye una única base de código Object Pascal para cada release de Delphi y C++Builder desde XE5, y build-All-Lib-TRIAL.cmd es el script que lo demuestra: 43 legs de compilación, con 12 versiones de Delphi en Win32 y Win64, además de 10 builds de paquetes C++Builder Win32 y 9 Win64. Desde v2.363 hasta v2.374 ese script nunca llegó al final y la leg de XE5 estuvo rota durante todo ese tiempo

Una vez visto, nada del fallo era sutil. Cinco construcciones distintas que el compilador actual acepta sin comentario son errores duros en RAD Studio XE5, que la matriz etiqueta como 12.0. La release v2.375.0 corrigió las cinco y la matriz volvió a ponerse verde con 43 de 43. Lo que sigue es cada rechazo, por qué el compilador antiguo tiene posiblemente razón en los dos que rechaza por motivos de tipos y la parte más embarazosa: el script de probe escrito para diagnosticar el desorden informó de un falso positivo en su primera ejecución

¿Por qué se quedó oxidada la leg de XE5 sin que nadie lo notara?

La leg de XE5 se quedó oxidada porque el desarrollo diario ejecutaba únicamente el conjunto de cuatro scripts de 37.0, y una compilación local verde no dice nada de un compilador que no se haya invocado. La matriz completa es un script distinto y lento que el instalador de prueba llama antes de que Inno Setup recopile los ficheros, así que se ejecuta al empaquetar en lugar de al hacer commit. Doce releases caben en ese hueco

Conviene detallar la aritmética de las legs porque ahí vive la ilusión de cobertura. DELPHI_TRIAL_VERSIONS enumera de 12.0 a 37.0 y cada una de esas 12 versiones se compila dos veces, Win32 y Win64. CB_TRIAL_WIN32_VERSIONS lista 10 versiones y CB_TRIAL_WIN64_VERSIONS solo 9, porque XE5 tiene un proyecto de paquete C++Builder pero no distribuye el objeto de inicio de paquete Win64 c0pkg64.o. Doce más doce más diez más nueve son 43. Ejecutar cuatro y llamar portable a la base de código es un error de categoría, y es el error concreto que permitió que ocurriera esto

HotXLS ya había sufrido la misma forma de problema desde el lado contrario. Una unidad nueva que es alcanzable mediante una cláusula uses pero está ausente de la lista de ficheros .cbproj compila perfectamente bajo Delphi, porque dcc incorpora implícitamente las unidades no listadas al paquete y como mucho emite un hint W1033. C++Builder solo emite un .obj para las unidades nombradas en <DelphiCompile>, así que el mismo código muere en la fase ilink con un external sin resolver. Una toolchain oculta lo que la otra detecta. Ese es todo el argumento para ejecutar la matriz en lugar de confiar en un compilador representativo

Casts de tipos duros que rechazan los compiladores Win32 antiguos

Dos de los cinco rechazos son el mismo bug con distinta ropa: un cast de tipo duro aplicado a una expresión de coma flotante en lugar de a una variable. En Win32, los compiladores antiguos evalúan la aritmética mediante la pila x87, por lo que una suma que incluya un Double se mantiene con una precisión adicional de 80 bits y su tipo estático se convierte en el Extended de 10 bytes. Convertir de 10 bytes a un TDateTime de 8 bytes no es un typecast legal y el compilador lo dice con E2089 Invalid typecast

El detalle desesperante es que la forma con variable sí funciona. TDateTime(Serial) compila en todas las versiones de la matriz porque Serial ya ocupa 8 bytes y el cast conserva el tamaño. Añada cualquier cosa y la expresión se ensancha por debajo. La corrección no es un cast más ancho ni un define condicional, sino dejar de hacer el cast: una asignación real-a-real implícita convierte correctamente en todos los compiladores compatibles con HotXLS y expresa lo que el código realmente significa

// Rechazado en XE5 (Win32): cada suma se evalúa como un Extended de 10 bytes
// y el cast de 10 a 8 bytes produce E2089
if Dates1904 then
  Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
  Value := TDateTime(Serial + 1)
else
  Value := TDateTime(Serial);        // este se acepta: no hay suma

// Compatible con todas las versiones: dejar que la asignación real-a-real haga la conversión
if Dates1904 then
  Value := Serial + XLSDate1904Offset
else if Serial < 60 then
  Value := Serial + 1
else
  Value := Serial;

// La misma clase de rechazo en el empaquetador de valores de celda: un cast duro
// de Double sobre un entero. Dividir es suficiente: el operador ya devuelve un real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then   // E2089
  ;
if (Scaled = intVal) and (intVal / 100 = AValue) then           // portable
  ;

La rama Serial < 60 es la ficción del año bisiesto 1900, no un off-by-one: el número de serie 60 es el inexistente 1900-02-29, así que los seriales inferiores necesitan el día adicional antes de que DecodeDate los vea. El trabajo de portabilidad nunca debe cambiar silenciosamente este tipo de lógica, y por eso la edición segura elimina el cast y deja intacta la aritmética

¿Qué se rompe cuando nil es un argumento procedural?

Un nil desnudo pasado donde se espera un tipo procedural no se puede asociar durante la resolución de overloads en los compiladores antiguos. El punto de llamada en HotXLS es ResolveIndexedColor, que está sobrecargado y recibe un callback TXLSTryResolveSystemColor que la mayoría de los callers no necesitan. Los compiladores nuevos resuelven nil contra el parámetro procedural y eligen el overload correcto. XE5 no lo hace, y el diagnóstico apunta al conjunto de overloads en lugar de al argumento, que es como se pierden veinte minutos

La respuesta portable es dar un tipo al callback nulo. Una variable de nivel de unidad del tipo procedural se inicializa a cero por el lenguaje, así que ya es nil sin inicializador y lleva la información de tipo que necesita el resolver antiguo. Cuando una variable de unidad sería excesiva, una local tipada asignada a nil consigue lo mismo

var
  // Un literal procedural nil no se asocia durante la resolución de overloads
  // de los compiladores antiguos; una variable tipada e inicializada a cero sí
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
  NilSystemColorResolver, Resolution);

// La misma corrección con una local tipada, en el workbook XLSX
function TXLSXWorkbook.ResolveIndexedColor(AIndex: Int64;
  ASpace: TXLSIndexedColorSpace;
  out AResolution: TXLSIndexedColorResolution): Boolean;
var
  NoResolver: TXLSTryResolveSystemColor;
begin
  NoResolver := nil;
  Result := ResolveIndexedColor(AIndex, ASpace, xicrGeneral, NoResolver,
    AResolution);
end;

Tenga en cuenta que esto es una diferencia real a nivel de lenguaje y no un bug del compilador que merezca un workaround mediante defines. La variable inicializada a cero es correcta en todas las versiones de la matriz y cuesta una línea, así que aquí no hay compilación condicional. Recurra a {$IF CompilerVersion} solo cuando la plataforma difiera realmente entre releases, que en este batch ocurre exactamente una vez

Los métodos VCL protected cambian entre releases

TPicture.LoadFromStream es public en la VCL actual y protected en las versiones antiguas que admite HotXLS, por lo que una llamada directa compila ahora y falla entonces. HotXLS lo utiliza para validar que el payload de imagen de fondo de un worksheet realmente se decodifica, una comprobación de firma que se ejecuta antes de que el exportador HTML se comprometa a incrustar los bytes. Se aplica la respuesta clásica de Pascal: declarar un descendiente en la misma unidad solo para ampliar la visibilidad y hacer el cast mediante él en el punto de llamada

type
  // TPicture.LoadFromStream es protected en las versiones VCL antiguas que
  // admite la biblioteca; un descendiente de la misma unidad lo expone
  TXlsxPictureAccess = class(TPicture);

// ...

Stream.WriteBuffer(AData[1], Length(AData));
Stream.Position := 0;
TXlsxPictureAccess(Picture).LoadFromStream(Stream);
Result := (Picture.Graphic <> nil) and not Picture.Graphic.Empty and
  (Picture.Graphic.Width > 0) and (Picture.Graphic.Height > 0);

El truco de la clase accessor es seguro aquí porque el descendiente no añade campos y nunca se instancia; el cast solo cambia lo que el compilador permite nombrar. Aun así merece un comentario en la declaración, porque quien solo compile en un IDE actual verá en otro caso un tipo aparentemente inútil. La gestión de imágenes de fondo vuelve a aparecer en la ruta de renderizado de rejilla VCL personalizada, donde el mismo payload decodificado alimenta la hoja en pantalla

El tipo del token GdiplusStartup cambió dos veces

El único rechazo del batch que requiere realmente compilación condicional es el tipo del parámetro var de GdiplusStartup, que cambió entre generaciones de VCL de una forma que no deja una grafía válida en todas partes. La comprobación por versión fijó el comportamiento real: las legs 12.0 a 20.0 aceptan solo Cardinal, las 21.0 y 22.0 aceptan solo THandle o ULONG_PTR y 23.0 y 37.0 aceptan ambos. En nombres de release, eso significa Cardinal desde XE5 hasta 10.3 Rio y THandle desde 10.4 Sydney. Como los dos rangos de aceptación no se solapan entre 12.0 y 22.0, no funciona ninguna declaración incondicional: la protección se basa en CompilerVersion >= 34, que es Sydney, y la llamada se cualifica por completo como Winapi.GDIPAPI.GdiplusStartup para que el orden de resolución de unidades no pueda sustituir otra declaración en alguna versión intermedia del rango

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // El tipo del parámetro var de GdiplusStartup en GDIPAPI sigue la generación
  // de VCL: Cardinal hasta Rio y THandle desde Sydney
  {$IF CompilerVersion >= 34}
  StartupToken: THandle;
  {$ELSE}
  StartupToken: Cardinal;
  {$IFEND}
  TiffEncoder: TGUID;
begin
  FillChar(StartupInput, SizeOf(StartupInput), 0);
  StartupInput.GdiplusVersion := 1;
  CheckStatus(Winapi.GDIPAPI.GdiplusStartup(StartupToken, @StartupInput,
    nil), 'startup');
  if GetEncoderClsid('image/tiff', TiffEncoder) < 0 then
    raise EInvalidGraphic.Create('GDI+ TIFF encoder is unavailable');
  // ... codificar ...
end;

Esta es la rama TIFF del exportador de imágenes de página, así que el radio de impacto de equivocarse aquí es toda la superficie de exportación raster, incluidas las rutas descritas en la exportación de un rango de celdas como una sola imagen. Tenga en cuenta también lo que la protección no afirma: ULONG_PTR y THandle tienen la misma anchura en ambas plataformas, así que la elección trata de qué identificador nombra la declaración, no de la corrección de 32 frente a 64 bits

¿Por qué la primera ejecución de la probe no informó de nada?

La probe de versiones no informó de nada en su primera ejecución porque las asignaciones res=$(...) se hacían dentro de un subshell, donde no se propagan al padre. dcc32 devuelve 0 cuando tiene éxito, así que el código de salida era la señal correcta que había que capturar, y el script la capturaba en una variable que dejaba de existir una línea después. Todas las legs volvían vacías y la salida parecía la de una probe que no hubiera compilado nada, que era exactamente lo que ocurría

El segundo fallo fue peor porque produjo una respuesta incorrecta en lugar de ninguna. La probe clasificaba una leg contando las líneas que coincidían con Error, y Delphi no antepone esa palabra a todos los fatales. F1026 File not found es fatal y no coincide, así que una probe que ni siquiera podía resolver una unidad se puntuaba como un paso limpio. XE5 no distribuye Winapi.GDIPOPS.dcu, la primera probe encontró exactamente eso y se puso falsamente en verde. La regla que salió de ahí es estrecha y merece decirse con claridad: juzgue una probe del compilador por el artefacto producido o por la línea de resumen del propio compilador, nunca buscando una palabra en su salida. Buscar Error en stderr es una heurística que falla en la dirección que no puede permitirse, informando silenciosamente de éxito

Qué cuesta realmente admitir una década de compiladores

La contabilidad honesta es que los cambios de código de este batch son triviales y los cambios de proceso no. Cuatro de los cinco rechazos se corrigieron escribiendo Pascal más normal, no añadiendo maquinaria de versiones: quitar un cast, dividir en lugar de hacer un cast, dar tipo a nil y declarar una clase accessor. Solo GdiplusStartup se ganó un {$IF}. Una base de código que va de XE5 a la release actual no se convierte en una maraña de defines condicionales salvo que deje acumularse casts duros e idioms del compilador más nuevo desde el principio

Lo que realmente cuesta es tiempo de compilación y disciplina. Cuarenta y tres legs es un script lento, precisamente por eso se desplazó hasta el momento del empaquetado y después hasta nunca. El punto medio defendible es conservar el bucle rápido de cuatro scripts para iterar y ejecutar la matriz completa con una cadencia que no se pueda omitir, porque el modo de fallo no es una compilación rota que uno detecta, sino un IDE admitido que dejó de estar admitido doce releases atrás sin hacer ruido

Esa obligación es la otra cara de distribuir un componente nativo. HotXLS lee y escribe XLS, XLSX y ODS solo mediante Object Pascal, sin instalación de Excel ni dependencia de COM, que es lo que hace posible la automatización de workbooks sin Office en un servidor bloqueado. La misma propiedad significa que el compilador es todo el contrato de plataforma, por lo que cada versión de la matriz es una promesa que hay que volver a verificar y no dar por supuesta

La matriz de compilación cruzada y el código compatible con las versiones descrito aquí forman parte del componente de hojas de cálculo Delphi HotXLS, compatible con Delphi y C++Builder desde XE5 hasta la release actual, con binarios de biblioteca precompilados para cada IDE admitido