Artículo técnico

HotXLS en Free Pascal: Unicode, slots COM y zlib

HotXLS compila bajo Free Pascal y Lazarus en Windows, y el porte se jugó en cuatro decisiones que no tienen nada que ver con la sintaxis de Object Pascal: mantener el núcleo en modo DELPHIUNICODE, declarar las interfaces de almacenamiento estructurado OLE como interfaces CORBA con recuento de referencias gestionado a mano, sustituir los ficheros de objetos AES de Win32 por una implementación en Pascal, y arreglar un bucle de inflate que podía aceptar un ZIP truncado como completo

Cualquiera que haya porteado una biblioteca Delphi madura conoce la forma de este trabajo. El compilador acepta casi todo a la primera. Lo que viene después es una cola larga de diferencias de comportamiento que compilan limpias y producen resultados incorrectos, y un motor de hojas de cálculo está expuesto a ellas como pocos, porque toca codificación de texto, almacenamiento estructurado COM, compresión y criptografía en una sola ruta de código

¿Por qué el núcleo insiste en DELPHIUNICODE y no en DELPHI a secas?

Porque el motor de fórmulas depende de que String y Char lleven semántica UTF-16, y la alternativa ANSI pierde caracteres antes de que nada llegue al fichero. Es tentador construir el núcleo en modo DELPHI de FPC, porque es el conmutador de compatibilidad al que recurre la mayoría de los portes, y el código compila. Entonces un libro con nombres de hoja en chino o etiquetas en cirílico hace un viaje de ida y vuelta por la ruta de cálculo y los caracteres ya no están cuando el escritor los ve, sin ningún error por ningún sitio

El modo no es uniforme en toda la biblioteca, y es deliberado más que desorden. El decodificador de bytes PNG y las sobrecargas de LCL necesitan de verdad firmas ANSI, porque trabajan con bytes y con lo que el widgetset les entrega. Esas unidades activan un conmutador aparte, LX_FPC_ANSI. Dos modos en una biblioteca suena a code smell hasta que uno se da cuenta de que la alternativa es un decodificador de bytes que trata su entrada como texto

Hay un detalle compañero que luego pilla a más de uno. DELPHIUNICODE no convierte TFormatSettings.DecimalSeparator en un WideChar en el runtime de FPC. Una entrada que traiga un separador decimal Unicode hay que normalizarla a un separador ASCII dentro de la propia cadena Unicode, y cualquier entrada cuyo separador no coincida con el esperado debe rechazarse en lugar de truncarse en silencio en el primer carácter que el parser no reconoció

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // debe ir el primero: inicializa el widgetset de 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 la primera. Es la que inicializa el widgetset de LCL y la capa de conversión UTF-8, y HotXLS depende de ambas en cuanto fuentes, rutas de fichero o texto cruzan la frontera entre RTL y LCL. Un programa de consola que se la salte compilará y se portará mal con cualquier ruta no ASCII. Esta es también la razón de que aquí una compilación correcta demuestre tan poco: el porte solo estuvo demostrablemente funcionando cuando documentos reales, con nombres de fuente y rutas reales, hicieron un viaje completo de ida y vuelta

Una VMT de clase no es una vtable COM

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

La disposición que funciona es una interfaz CORBA con los slots COM declarados explícitamente y AddRef y Release gestionados a mano. Eso significa renunciar al recuento de referencias automático para estos tipos y hacerse cargo de la vida útil, que es un trato justo para un puñado de interfaces que viven dentro de una unidad. La trampa concreta dentro de ese trabajo es QueryInterface: debe devolver un puntero a interfaz, no el puntero al objeto. Ambas cosas compilan. Una de ellas le entrega a Windows una dirección cuya primera palabra de máquina no es una vtable

Diagrama que compara la VMT de clase de Free Pascal con la vtable de interfaz COM que HotXLS debe presentar a la API de almacenamiento estructurado 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 de un puntero a interfaz manda una llamada ILockBytes a un slot de clase y provoca un crash lejos del sitio de la llamada
Free Pascal se niega a servir una VMT de clase como vtable COM, así que HotXLS declara interfaces CORBA con slots COM explícitos y AddRef y Release gestionados 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, de modo que las decisiones específicas del compilador quedan en un solo sitio en vez de estar dispersas por el motor. El formato en sí y cómo lo recorre la biblioteca se describen en leer ficheros compuestos OLE2 en Pascal

Otro detalle de tipos pertenece a la misma familia. LargeInt debe resolverse a Int64 en la rama FPC, y la clasificación que hace el compilador de Comp difiere lo suficiente entre las dos toolchains como para que la resolución de sobrecargas escoja otro candidato. 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 su cuenta en seeks que pasan de 4 GiB, así que un test en verde ahí no dice nada de su propia aritmética

Lo que esconde una implementación AES autoconsistente

Los ficheros de objetos AES de Win32 que enlaza la build de Delphi son OMF, y el enlazador de Free Pascal no puede consumirlos, así que la rama FPC usa una implementación AES en Pascal. Delphi sigue enlazando los ficheros 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 merece la pena llevarse a cualquier proyecto. Cifrar datos y descifrarlos otra vez con la misma implementación no prueba absolutamente nada: un algoritmo simétrico con un key schedule equivocado, un orden de bloques equivocado o un encadenado equivocado es perfectamente autoconsistente y hará viaje de ida y vuelta a su propia salida cada vez. Solo los vectores de known-answer lo cazan, comprobando la expansión de clave, el orden de bloques y el encadenado CBC contra valores publicados. Envíe una implementación equivocada pero autoconsistente y el síntoma aparecerá la primera vez que un cliente abra el fichero en Excel

La compresión tenía un defecto de otro carácter. Un backend inflate en Pascal puede seguir teniendo salida pendiente después de consumir toda su entrada comprimida, así que quien llama tiene que seguir llamando hasta que el stream anuncie 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 fallo que el endurecimiento de validar el registro end-of-central-directory del ZIP existe para prevenir. La regla es que sin progreso y sin terminar es un error de truncamiento, nunca un EOF

Dos trampas del sistema de build que cuestan horas de verdad

Las rutas de búsqueda de LCL tienen que ir delante de las rutas con comodines de los paquetes FPC, o la unidad Menus de Free Vision eclipsa a la unidad de LCL del mismo nombre y obtiene 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 obsoletas en fpc.cfg, así que los puntos de entrada de la build especifican explícitamente las rutas de unidades y binarios en lugar de heredar lo que ofrezca el entorno

La segunda trampa no tiene nada que ver con Pascal. Un fichero batch .cmd escrito con fines de línea LF funciona hasta que el fichero crece más allá del tamaño del búfer de lectura del intérprete, momento en el que call :label falla con el mensaje de que la etiqueta batch no existe, y el fallo aparece en el programa que por casualidad quede más allá de la frontera. Cualquier herramienta que reescriba un script batch tiene que volver a escribir CRLF. Y lazbuild --build-all vacía el directorio de salida de unidades del paquete antes de compilar, de modo que un fichero de opciones aparcado en ese directorio se borra antes de poder leerse: téngalo 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 limpia de HotXLS en Free Pascal: el modo DELPHIUNICODE que mantiene String y Char en UTF-16, la vía de escape LX_FPC_ANSI para el decodificador de bytes PNG y las sobrecargas de LCL, y las trampas de build procedentes del Menus de Free Vision que eclipsa, las rutas obsoletas de fpc.cfg, los ficheros batch solo LF y el vaciado de salida de lazbuild
Una primera compilación demuestra poco: el mapa de modos decide qué caracteres sobreviven hasta el escritor, mientras que las trampas del sistema de build afloran como mismatch de checksum, etiquetas fantasma que no existen y ficheros de opciones borrados antes de leerse
// Exportación de grid en Lazarus: TGridToXLS viene en el paquete Lazarus, así que
// el mismo código de exportación de DB-grid funciona en una aplicación 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 avisa de variables locales sin inicializar a las que Delphi pasa, y correr la build FPC convirtió esa diferencia en dos defectos reales en la unidad de cálculo. Una función leía una variable de contador que nunca se asignaba antes de usarse, y otra usaba dos coordenadas en una rama antes de que el código que las calculaba se ejecutara en otra rama. Bajo Delphi ambas se comportaban según lo que la pila tuviera a bien contener en cada momento, que es la definición exacta de un bug que se reproduce en una máquina y en otra no

La conclusión práctica es que conviene mantener el segundo compilador en el circuito aunque el producto se envíe sobre todo con el primero. Pasar periódicamente las clases de warnings de FPC es una pasada de análisis estático barata sobre una base de código Delphi, y encuentra una categoría de defecto a la que ninguna suite de tests llega de forma fiable. La disciplina más amplia de matriz de versiones en la que esto encaja se describe en la matriz de builds entre compiladores

El soporte de Free Pascal y Lazarus para Windows se envía con el componente de hoja de cálculo Delphi HotXLS como paquete Lazarus junto a los paquetes Delphi y C++Builder, construido desde el mismo árbol de fuentes y no desde un fork. Ese es el sentido del ejercicio: un motor, cuatro toolchains, y las decisiones específicas del compilador aisladas en include files que se pueden leer de un tirón