Artigo Técnico

HotXLS no Free Pascal: Unicode, slots COM e zlib

O HotXLS compila sob Free Pascal e Lazarus no Windows, e o port girou em torno de quatro decisões que não têm nada a ver com sintaxe de Object Pascal: manter o core em modo DELPHIUNICODE, declarar as interfaces de structured-storage OLE como interfaces CORBA com contagem de referências gerenciada à mão, trocar os arquivos objeto AES do Win32 por uma implementação em Pascal, e consertar um loop de inflate que podia aceitar um ZIP truncado como completo

Quem já portou uma biblioteca Delphi madura conhece o formato desse trabalho. O compilador aceita quase tudo na primeira passada. O que vem depois é uma cauda longa de diferenças de comportamento que compilam limpas e produzem resultados errados, e um motor de planilhas fica incomumente exposto a elas porque toca codificação de texto, structured storage COM, compressão e criptografia num único caminho de código

Por que o core insiste em DELPHIUNICODE e não no DELPHI simples?

Porque o motor de fórmulas depende de String e Char carregarem semântica UTF-16, e a alternativa ANSI perde caracteres antes de qualquer coisa chegar ao arquivo. É tentador compilar o core em modo DELPHI do FPC, já que esse é o switch de compatibilidade que a maioria dos ports busca, e o código compila. Aí uma workbook com nomes de planilha em chinês ou rótulos em cirílico faz round-trip pelo caminho de cálculo e os caracteres já se foram quando o writer os vê, sem erro nenhum em lugar algum

O modo não é uniforme pela biblioteca, e isso é deliberado, não desleixo. O decodificador de bytes do PNG e os overrides da LCL genuinamente precisam de assinaturas ANSI, porque lidam com bytes e com o que o widgetset entrega a eles. Essas units ligam um switch separado, LX_FPC_ANSI. Dois modos numa biblioteca soa como code smell até você notar que a alternativa é um decodificador de bytes que trata a própria entrada como texto

Há um detalhe companheiro que pega as pessoas depois. O DELPHIUNICODE não torna o TFormatSettings.DecimalSeparator um WideChar na runtime do FPC. Entrada carregando um separador decimal Unicode precisa ser normalizada para um separador ASCII dentro da string Unicode primeiro, e qualquer entrada cujo separador não bate com o esperado deve ser rejeitada, e não truncada em silêncio no caractere que o parser não reconheceu

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // precisa vir primeiro: inicializa o widgetset da LCL
  SysUtils, lxHandle;  // e a camada de conversão 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.

A unit Interfaces não é opcional e precisa vir primeiro. É ela que inicializa o widgetset da LCL e a camada de conversão UTF-8, e o HotXLS depende dos dois assim que fontes, caminhos de arquivo ou texto cruzam a fronteira entre RTL e LCL. Um programa de console que a pular vai compilar e vai se comportar mal em qualquer caminho não ASCII. Essa é também a razão de uma compilação bem-sucedida provar tão pouco aqui: o port só ficou demonstrativamente funcional quando documentos reais, com nomes de fonte e caminhos reais, fizeram um round-trip completo

Uma VMT de classe não é uma vtable COM

O Free Pascal não deixa você entregar uma VMT de classe ao Windows como uma vtable de interface COM, mesmo quando a declaração parece idêntica à que o Delphi aceita. Os layouts diferem de um jeito que produz uma chamada para o slot errado, o que se manifesta como um crash em algum lugar sem relação com o call site. Structured storage importa aqui porque o formato binário clássico de workbook é um compound file OLE, e ler ou escrever um significa implementar ILockBytes no qual a API de storage do Windows vai fazer callback

O arranjo que funciona é uma interface CORBA com os slots COM declarados explicitamente e AddRef e Release gerenciados à mão. Isso significa abrir mão da contagem automática de referências para esses tipos e assumir a responsabilidade pelo tempo de vida, o que é uma troca justa para um punhado de interfaces que vivem dentro de uma unit. A armadilha específica dentro desse trabalho é o QueryInterface: ele deve devolver um ponteiro de interface, não o ponteiro do objeto. Os dois compilam. Um deles entrega ao Windows um endereço cuja primeira palavra de máquina não é uma vtable

Diagrama comparando a VMT de classe do Free Pascal com a vtable de interface COM que o HotXLS precisa apresentar à API de structured storage do Windows: ordens de slot diferentes para a mesma declaração Pascal, mais a armadilha do QueryInterface, em que devolver o ponteiro do objeto em vez do ponteiro de interface manda uma chamada ILockBytes para um slot de classe e crasha longe do call site
O Free Pascal se recusa a servir uma VMT de classe como vtable COM, então o HotXLS declara interfaces CORBA com slots COM explícitos e AddRef e Release gerenciados à mão, e o QueryInterface devolve o ponteiro de interface que o Windows consegue desreferenciar

As declarações específicas do FPC vivem em lxOleInterfaces.inc, ao lado de lxAESBackend.inc e lxZlibBackend.inc no diretório de fontes do FPC, então as escolhas específicas de compilador ficam num lugar só em vez de se espalhar pelo engine. O formato em si e como a biblioteca o navega estão descritos em leitura de compound files OLE2 em Pascal

Mais um detalhe de tipo pertence à mesma família. O LargeInt precisa resolver para Int64 no branch do FPC, e a classificação do Comp pelo compilador difere o bastante entre as duas toolchains para a resolução de overload escolher um candidato diferente. Teste o comportamento de offsets grandes com um file stream em vez de um stream HGLOBAL: o stream de memória global do Windows dá wrap em seeks além de 4 GiB por conta própria, então um teste passando ali não prova nada sobre a sua aritmética

O que uma implementação AES autoconsistente esconde

Os arquivos objeto AES do Win32 que o build Delphi linka são OMF, e o linker do Free Pascal não os consome, então o branch do FPC usa uma implementação AES em Pascal. O Delphi continua linkando os arquivos objeto de sempre, o que mantém o binário publicado inalterado para os clientes existentes

O requisito de verificação é a parte que vale levar para qualquer projeto. Criptografar dados e descriptografá-los de novo com a mesma implementação não prova absolutamente nada: um algoritmo simétrico com key schedule errado, ordem de blocos errada ou encadeamento errado é perfeitamente autoconsistente e fará round-trip da própria saída toda vez. Só os vetores de known-answer pegam isso, conferindo a expansão de chave, a ordem de blocos e o encadeamento CBC contra valores publicados. Publique uma implementação errada autoconsistente e o sintoma aparece na primeira vez que um cliente abre o arquivo no Excel

A compressão teve um defeito de caráter diferente. Um backend de inflate em Pascal pode ainda ter saída pendente depois de consumir toda a sua entrada comprimida, então o caller precisa continuar chamando até o stream reportar o seu fim. Tratar entrada esgotada como fim de stream trunca o último bloco. Pior: transforma um arquivo danificado num aceito em silêncio, que é exatamente o modo de falha que o hardening em validação do record ZIP end-of-central-directory existe para prevenir. A regra é que nenhum progresso mais não terminado é um erro de truncamento, nunca um EOF

Duas armadilhas de build que custam horas de verdade

Os paths de busca da LCL precisam preceder os paths de wildcard dos pacotes do FPC, ou a unit Menus do Free Vision ofusca a unit homônima da LCL e você recebe um checksum mismatch de PPU que não diz nada sobre nenhum dos dois. Uma instalação do Lazarus movida depois de instalada também pode deixar paths velhos no fpc.cfg, então os entry points de build especificam os paths de units e binários explicitamente em vez de herdar o que o ambiente oferecer

A segunda armadilha não tem nada a ver com Pascal. Um arquivo batch .cmd escrito com line endings LF funciona até o arquivo passar do tamanho do buffer de leitura do interpretador, ponto em que o call :label falha alegando que o label de batch não existe, e a falha aparece no programa que por acaso estiver depois da fronteira. Toda ferramenta que reescreve um script batch precisa gravar CRLF de volta. E o lazbuild --build-all limpa o diretório de output de units do pacote antes de compilar, então um arquivo de opções estacionado nesse diretório é deletado antes que possa ser lido: mantenha-o fora, e lembre que o path do @ é resolvido relativo ao diretório do pacote porque o lazbuild invoca o compilador de lá

Mapa das duas camadas de perigo por trás de uma compilação Free Pascal limpa do HotXLS: o modo DELPHIUNICODE que mantém String e Char em UTF-16, o escape LX_FPC_ANSI para o decodificador de bytes do PNG e os overrides da LCL, e as armadilhas de build vindas da ofuscação do Menus do Free Vision, paths velhos no fpc.cfg, arquivos batch só com LF e a limpeza de output do lazbuild
A primeira compilação prova pouco: o mapa de modos decide quais caracteres sobrevivem até o writer, enquanto as armadilhas de build aparecem como checksum mismatches, labels fantasmagoricamente ausentes e arquivos de opções deletados antes de serem lidos
// Export de grid no Lazarus: o TGridToXLS vem no pacote Lazarus, então
// o mesmo código de export de DB-grid funciona numa aplicação 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;

Quanto vale um warning de compilador

O Free Pascal reporta variáveis locais não inicializadas que o Delphi não reporta, e rodar o build FPC transformou essa diferença em dois defeitos reais na unit de cálculo. Uma função lia uma variável de contagem que nunca recebia atribuição antes do uso, e outra usava duas coordenadas num branch antes de o código que as calculava rodar num branch diferente. Sob o Delphi, as duas se comportavam conforme o que a stack por acaso segurava, que é a definição de um bug que reproduz numa máquina e em outra não

A conclusão prática é que o segundo compilador vale manter no ciclo mesmo para um produto que publica primariamente no primeiro. Varrer as classes de warnings do FPC periodicamente é uma passada barata de análise estática sobre um codebase Delphi, e ela acha uma categoria de defeito que nenhuma suíte de testes alcança de forma confiável. A disciplina mais ampla de matriz de versões em que isso se insere está descrita em a matriz de build cross-compiler

O suporte a Free Pascal e Lazarus para Windows vem com o componente de planilha HotXLS para Delphi como um pacote Lazarus junto dos pacotes Delphi e C++Builder, compilado da mesma árvore de fontes, e não de um fork. Esse é o propósito do exercício: um engine, quatro toolchains, e as decisões específicas de compilador isoladas em include files que dá para ler numa sentada