Artículo técnico

HotXLS en Free Pascal: Unicode, slots COM y zlib

HotXLS compila con Free Pascal y Lazarus en Windows, y el port se decidió en cuatro puntos que no tienen nada que ver con la sintaxis de Object Pascal: mantener el core en modo DELPHIUNICODE, declarar las interfaces de structured-storage OLE como interfaces CORBA con conteo de referencias administrado a mano, reemplazar los archivos de objetos AES de Win32 por una implementación en Pascal, y arreglar un loop de inflate que podía aceptar un ZIP truncado como completo

Quien haya porteado una biblioteca Delphi madura conoce la forma de este trabajo. El compilador acepta casi todo en la primera pasada. Lo que sigue es una cola larga de diferencias de comportamiento que compilan limpio y producen resultados equivocados, y un engine de hojas de cálculo está inusualmente expuesto a ellas porque toca codificación de texto, structured storage COM, compresión y criptografía en una sola ruta de código

¿Por qué el core insiste en DELPHIUNICODE y no en DELPHI a secas?

Porque el engine de fórmulas depende de que String y Char carguen semántica UTF-16, y la alternativa ANSI pierde caracteres antes de que nada llegue al archivo. Es tentador compilar el core en modo FPC DELPHI, ya que es el switch de compatibilidad al que la mayoría de los ports echan mano, y el código compila. Luego un workbook con nombres de hoja en chino o etiquetas en cirílico hace un round-trip por la ruta de cálculo y los caracteres ya no están para cuando el writer los ve, sin error alguno en ningún lado

El modo no es uniforme en toda la biblioteca, y eso es deliberado, no desorden. El decoder de bytes PNG y los overrides de LCL genuinamente necesitan firmas ANSI, porque trabajan con bytes y con lo que el widgetset les entrega. Esas unidades activan un switch aparte, LX_FPC_ANSI. Dos modos en una biblioteca suena a code smell hasta que se nota que la alternativa es un decoder de bytes que trata su entrada como texto

Hay un detalle compañero que atrapa a la gente después. DELPHIUNICODE no convierte TFormatSettings.DecimalSeparator en un WideChar en el runtime de FPC. Una entrada que traiga un separador decimal Unicode tiene que normalizarse a un separador ASCII dentro del string Unicode primero, y cualquier entrada cuyo separador no coincida con el esperado debe rechazarse en lugar de truncarse silenciosamente en el carácter que el parser no reconoció

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // va primero: inicializa el widgetset de la LCL
  SysUtils, lxHandle;  // y la capa de conversión UTF-8

var
  Book: TXLSWorkbook;
begin
  Book := TXLSWorkbook.Create(nil);
  try
    Book.LoadFromFile('input.xls');
    Book.Sheets[0].AsString[1, 1] := 'Quarterly summary';
    Book.SaveToFile('output.xls');
  finally
    Book.Free;
  end;
end.

La unidad Interfaces no es opcional y tiene que ir primero. Es lo que inicializa el widgetset de la LCL y la capa de conversión UTF-8, y HotXLS depende de ambas en cuanto fuentes, rutas de archivo o texto cruzan la frontera entre RTL y LCL. Un programa de consola que la omita compilará y se portará mal en cualquier ruta no ASCII. Esta es también la razón por la que una compilación exitosa prueba tan poco aquí: el port solo estuvo demostrablemente funcionando cuando documentos reales con nombres de fuente reales y rutas reales hicieron un round-trip completo

El VMT de una clase no es un vtable COM

Free Pascal no le va a dejar entregar el VMT de una clase a Windows como vtable de interfaz COM, ni siquiera cuando la declaración se ve idéntica a la que Delphi acepta. Los layouts difieren de maneras que producen una llamada al slot equivocado, lo que se manifiesta como un crash en un lugar sin relación con el punto de llamada. El structured storage importa aquí porque el formato clásico de workbook binario es un archivo compuesto OLE, y leerlo o escribirlo significa implementar ILockBytes al que la API de storage de Windows va a hacer callbacks

El arreglo que funciona es una interfaz CORBA con los slots COM declarados explícitamente y AddRef y Release administrados a mano. Eso significa renunciar al conteo automático de referencias para estos tipos y hacerse cargo del ciclo de vida, que es un trade justo para un puñado de interfaces que viven dentro de una unidad. La trampa específica dentro de ese trabajo es QueryInterface: debe devolver un puntero a interfaz, no el puntero al objeto. Ambas compilan. Una de ellas le entrega a Windows una dirección cuyo primer machine word no es un vtable

Diagrama que compara el VMT de clase de Free Pascal con el vtable de interfaz COM que HotXLS debe presentar a la API de structured storage de Windows: órdenes de slots distintos para la misma declaración Pascal, más la trampa de QueryInterface donde devolver el puntero al objeto en lugar del puntero a interfaz manda una llamada ILockBytes a un slot de clase y truena lejos del punto de llamada
Free Pascal se niega a servir un VMT de clase como vtable COM, así que HotXLS declara interfaces CORBA con slots COM explícitos y AddRef y Release administrados a mano, y QueryInterface devuelve el puntero a interfaz que Windows puede desreferenciar

Las declaraciones específicas de FPC viven en lxOleInterfaces.inc, junto a lxAESBackend.inc y lxZlibBackend.inc en el directorio de fuentes FPC, así que las decisiones específicas del compilador quedan en un solo lugar en vez de dispersarse por el engine. El formato en sí y cómo lo navega la biblioteca están descritos en leer archivos compuestos OLE2 en Pascal

Un detalle de tipos más pertenece a la misma familia. LargeInt debe resolverse a Int64 en la rama FPC, y la clasificación que el compilador hace de Comp difiere lo suficiente entre las dos toolchains que la resolución de overloads puede elegir un candidato distinto. Pruebe el comportamiento con offsets grandes usando un file stream y no un stream HGLOBAL: el stream de memoria global de Windows da la vuelta por sí mismo en seeks más allá de 4 GiB, así que un test en verde ahí no prueba nada sobre su propia aritmética

Lo que esconde una implementación AES autoconsistente

Los archivos de objetos AES de Win32 que la compilación de Delphi enlaza son OMF, y el linker de Free Pascal no puede consumirlos, así que la rama FPC usa una implementación AES en Pascal. Delphi sigue enlazando los archivos de objetos de siempre, lo que mantiene el binario publicado sin cambios para los clientes existentes

El requisito de verificación es la parte que vale llevar a cualquier proyecto. Cifrar datos y descifrarlos de nuevo con la misma implementación no prueba nada: un algoritmo simétrico con un key schedule equivocado, un orden de bloques equivocado o un chaining equivocado es perfectamente autoconsistente y hará round-trip de su propia salida todas las veces. Solo los known-answer vectors lo cazan, comprobando la expansión de clave, el orden de bloques y el chaining CBC contra valores publicados. Distribuya una implementación equivocada pero autoconsistente y el síntoma aparecerá la primera vez que un cliente abra el archivo en Excel

La compresión tenía un defecto de otro carácter. Un backend inflate en Pascal puede seguir con salida pendiente después de consumir toda su entrada comprimida, así que quien llama debe seguir llamando hasta que el stream reporte su fin. Tratar la entrada agotada como fin de stream trunca el último bloque. Peor aún, convierte un archivo dañado en uno aceptado en silencio, que es exactamente el modo de falla que el hardening de validar el record ZIP end-of-central-directory existe para prevenir. La regla es que sin progreso más no terminado es un error de truncamiento, jamás un EOF

Dos trampas del build system que costaron horas reales

Las rutas de búsqueda de la LCL tienen que preceder a las rutas wildcard de paquetes de FPC, o la unidad Menus de Free Vision le hace sombra a la unidad de la LCL del mismo nombre y usted recibe un mismatch de checksum PPU que no dice nada de ninguno de los dos. Una instalación de Lazarus que se movió después de instalarse también puede dejar rutas viejas en fpc.cfg, así que los puntos de entrada de compilación especifican rutas de unidades y binarios explícitamente en lugar de heredar lo que el ambiente ofrezca

La segunda trampa no tiene nada que ver con Pascal. Un archivo batch .cmd escrito con finales de línea LF funciona hasta que el archivo crece más allá del tamaño del buffer de lectura del intérprete, momento en el que call :label falla con la afirmación de que la etiqueta batch no existe, y la falla aparece en el programa que casualmente quede pasado el límite. Cualquier herramienta que reescriba un script batch tiene que escribir CRLF de vuelta. Y lazbuild --build-all limpia el directorio de output de unidades del paquete antes de compilar, así que un archivo de opciones estacionado en ese directorio se borra antes de poder leerse: guárdelo fuera, y recuerde que la ruta @ se resuelve relativa al directorio del paquete porque lazbuild invoca al compilador desde ahí

Mapa de las dos capas de peligro detrás de una compilación Free Pascal de HotXLS aparentemente limpia: el modo DELPHIUNICODE que mantiene String y Char en UTF-16, el escape LX_FPC_ANSI para el decoder de bytes PNG y los overrides de LCL, y trampas de build desde la sombra de Menus de Free Vision, rutas viejas en fpc.cfg, archivos batch solo LF y el borrado de output de lazbuild
Una primera compilación prueba poco: el mapa de modos decide qué caracteres sobreviven hasta el writer, mientras que las trampas del build system aparecen como mismatches de checksum, etiquetas fantasma inexistentes y archivos de opciones borrados antes de leerse
// Export de grids Lazarus: TGridToXLS viene en el paquete Lazarus, así
// que el mismo código de export de DB-grid funciona en una app LCL
var
  Exporter: TGridToXLS;
begin
  Exporter := TGridToXLS.Create(nil);
  try
    Exporter.DBGrid := GridOrders;
    Exporter.WorksheetName := 'Orders';
    Exporter.ExportHeader := True;
    Exporter.SetColumnsWidth := True;
    Exporter.ExportDBGrid;
    Exporter.SaveAs('orders.xls');
  finally
    Exporter.Free;
  end;
end;

Cuánto vale un warning del compilador

Free Pascal reporta variables locales sin inicializar que Delphi no reporta, y correr la compilación FPC convirtió esa diferencia en dos defectos reales en la unidad de cálculo. Una función leía una variable de conteo que nunca se asignó antes de usarse, y otra usaba dos coordenadas en una rama antes de que el código que las calculaba corriera en otra rama. Bajo Delphi ambas se comportaban según lo que el stack casualmente tuviera, que es la definición de un bug que se reproduce en una máquina y no en otra

La conclusión práctica es que vale la pena mantener el segundo compilador en el loop incluso para un producto que se distribuye principalmente con el primero. Escanear periódicamente las clases de warnings de FPC es una pasada barata de análisis estático sobre un código Delphi, y encuentra una categoría de defecto que ninguna suite de tests alcanza de forma confiable. La disciplina más amplia de matriz de versiones donde esto se inscribe está descrita en la matriz de compilación cruzada entre compiladores

El soporte de Free Pascal y Lazarus para Windows viene con el HotXLS Delphi spreadsheet component como un paquete Lazarus junto a los paquetes de Delphi y C++Builder, compilado desde el mismo árbol de fuentes y no desde un fork. Ese es el punto del ejercicio: un engine, cuatro toolchains, y las decisiones específicas del compilador aisladas en include files que se pueden leer de una sentada