Artigo Técnico

HotXLS no Free Pascal: Unicode, slots COM e zlib

O HotXLS compila sob o Free Pascal e o Lazarus em Windows, e o port assentou em quatro decisões que não têm nada a ver com a sintaxe do Object Pascal: manter o núcleo em modo DELPHIUNICODE, declarar as interfaces de structured-storage OLE como interfaces CORBA com contagem de referências gerida à mão, substituir os ficheiros de objeto AES Win32 por uma implementação em Pascal, e corrigir um ciclo de inflate que podia aceitar um ZIP truncado como completo

Quem já portou uma biblioteca Delphi madura conhece a forma deste trabalho. O compilador aceita quase tudo à primeira passagem. O que se segue é uma longa cauda de diferenças comportamentais que compilam limpo e produzem resultados errados, e um motor de folhas de cálculo está invulgarmente exposto a elas porque toca na codificação de texto, structured storage COM, compressão e criptografia num único caminho de código

Porque é que o núcleo insiste em DELPHIUNICODE e não em DELPHI simples?

Porque o motor de fórmulas depende de String e Char transportarem semântica UTF-16, e a alternativa ANSI perde caracteres antes de qualquer coisa chegar ao ficheiro. É tentador compilar o núcleo em modo DELPHI do FPC, já que é o switch de compatibilidade a que a maioria dos ports recorre, e o código compila. Depois um livro com nomes de folhas em chinês ou rótulos em cirílico faz uma viagem de ida e volta pelo caminho de cálculo e os caracteres desapareceram quando o escritor os vê, sem erro nenhum em lado nenhum

O modo não é uniforme em toda a biblioteca, e isso é deliberado e não desarrumado. O descodificador de bytes PNG e as overrides da LCL genuinamente precisam de assinaturas ANSI, porque lidam com bytes e com o que o widgetset lhes entrega. Essas unidades ativam um switch separado LX_FPC_ANSI. Dois modos numa biblioteca soa a smell até reparar que a alternativa é um descodificador de bytes que trata o input como texto

Há um detalhe companheiro que apanha as pessoas mais tarde. DELPHIUNICODE não torna TFormatSettings.DecimalSeparator um WideChar no runtime do FPC. Input que traga um separador decimal Unicode tem de ser normalizado para um separador ASCII dentro da string Unicode primeiro, e qualquer input cujo separador não case com o esperado deve ser rejeitado em vez de silenciosamente truncado no caráter que o parser não reconheceu

program ExportReport;
{$MODE DELPHI}
uses
  Interfaces,          // tem de 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 unidade Interfaces não é opcional e tem de vir primeiro. É ela que inicializa o widgetset da LCL e a camada de conversão UTF-8, e o HotXLS depende de ambos mal as fontes, os caminhos de ficheiros ou o texto cruzam a fronteira entre a RTL e a LCL. Um programa de consola que a salte vai compilar e vai comportar-se mal em qualquer caminho não ASCII. Esta é também a razão de uma compilação com sucesso provar tão pouco aqui: o port só estava demonstravelmente a funcionar quando documentos reais com nomes de fontes reais e caminhos reais fizeram uma viagem completa de ida e volta

Uma VMT de classe não é uma vtable COM

O Free Pascal não deixa entregar uma VMT de classe ao Windows como vtable de interface COM, mesmo quando a declaração parece idêntica à que o Delphi aceita. Os layouts diferem de formas que produzem uma chamada para o slot errado, o que se manifesta como uma falha algures não relacionada com o call site. O structured storage importa aqui porque o formato clássico de livro binário é um ficheiro composto OLE, e ler ou escrever um significa implementar ILockBytes para o 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 geridos à mão. Isso significa abrir mão da contagem de referências automática para estes tipos e assumir a responsabilidade pelo tempo de vida, que é uma troca justa para um punhado de interfaces que vivem dentro de uma unidade. A armadilha específica dentro desse trabalho é o QueryInterface: tem de devolver um ponteiro de interface, não o ponteiro do objeto. Ambos compilam. Um deles entrega ao Windows um endereço cuja primeira palavra de máquina não é uma vtable

Diagrama que compara a VMT de classe do Free Pascal com a vtable de interface COM que o HotXLS tem de apresentar à API de structured storage do Windows: ordens de slots diferentes para a mesma declaração Pascal, mais a armadilha do QueryInterface em que devolver o ponteiro do objeto em vez de um ponteiro de interface manda uma chamada ILockBytes para um slot de classe e falha longe do call site
O Free Pascal recusa-se a servir uma VMT de classe como vtable COM, por isso o HotXLS declara interfaces CORBA com slots COM explícitos e AddRef e Release geridos à 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 código-fonte FPC, por isso as escolhas específicas do compilador ficam num sítio só em vez de espalhadas pelo motor. O formato em si e como a biblioteca o navega estão descritos em ler ficheiros compostos OLE2 em Pascal

Mais um detalhe de tipos pertence à mesma família. LargeInt tem de resolver para Int64 no ramo FPC, e a classificação de Comp pelo compilador difere o suficiente entre as duas toolchains para a resolução de overloads poder escolher um candidato diferente. Teste o comportamento de offsets grandes com um file stream e não com um stream HGLOBAL: o stream de memória global do Windows dá a volta em seeks acima de 4 GiB por conta própria, por isso um teste lá passar não prova nada sobre a sua própria aritmética

O que uma implementação AES autoconsistente esconde

Os ficheiros de objeto AES Win32 que a build Delphi liga são OMF, e o linker do Free Pascal não os consegue consumir, por isso o ramo FPC usa uma implementação AES em Pascal em vez deles. O Delphi continua a ligar os ficheiros de 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 a pena levar para qualquer projeto. Encriptar dados e desencriptá-los de novo com a mesma implementação não prova nada de nada: um algoritmo simétrico com um key schedule errado, ordem de blocos errada ou chaining errado é perfeitamente autoconsistente e faz round-trip do próprio output todas as vezes. Só os vetores known-answer o apanham, verificando a expansão da chave, a ordem dos blocos e o chaining CBC contra valores publicados. Distribua uma implementação errada mas autoconsistente e o sintoma aparece na primeira vez que um cliente abre o ficheiro no Excel

A compressão tinha um defeito de outra natureza. Um backend de inflate em Pascal pode ainda ter output pendente depois de já ter consumido todo o input comprimido, por isso o chamador tem de continuar a chamar até o stream reportar o seu fim. Tratar input esgotado como fim de stream trunca o último bloco. Pior, transforma um arquivo danificado num silenciosamente aceito, que é precisamente o modo de falha que o hardening em validar o registo 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 do sistema de build que custam horas a sério

Os caminhos de pesquisa da LCL têm de preceder os caminhos wildcard de pacotes do FPC, ou a unidade Menus do Free Vision faz sombra à unidade da LCL com o mesmo nome e recebe um desacordo de checksum PPU que não diz nada sobre nenhuma das duas. Uma instalação Lazarus movida depois de instalada também pode deixar caminhos velhos no fpc.cfg, por isso os pontos de entrada de build especificam caminhos de unidades 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 ficheiro batch .cmd escrito com finais de linha LF funciona até o ficheiro crescer para lá do tamanho do buffer de leitura do interpretador, altura em que call :label falha com a alegação de que a etiqueta de batch não existe, e a falha aparece no programa que calhar estar para lá da fronteira. Qualquer ferramenta que reescreva um script batch tem de escrever CRLF de volta. E o lazbuild --build-all limpa o diretório de output de unidades de pacotes antes de compilar, por isso um ficheiro de opções estacionado nesse diretório é apagado antes de poder ser lido: mantenha-o fora, e lembre-se de que o caminho @ é resolvido em relação ao diretório do pacote porque o lazbuild invoca o compilador a partir daí

Mapa das duas camadas de perigo por trás de uma compilação Free Pascal do HotXLS limpa: o modo DELPHIUNICODE que mantém String e Char em UTF-16, o escape LX_FPC_ANSI para o descodificador de bytes PNG e as overrides da LCL, e armadilhas de build desde a sombra do Menus do Free Vision, caminhos velhos no fpc.cfg, ficheiros batch só com LF e o wiping de output do lazbuild
Uma primeira compilação prova pouco: o mapa de modos decide que caracteres sobrevivem até ao escritor, enquanto as armadilhas do sistema de build vêm à superfície como desacordos de checksum, etiquetas fantasmas em falta e ficheiros de opções apagados antes de serem lidos
// Exportação de grelha Lazarus: a TGridToXLS é distribuída no pacote Lazarus, por isso
// o mesmo código de exportação 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;

O que vale um aviso do compilador

O Free Pascal reporta variáveis locais não inicializadas que o Delphi não reporta, e correr a build FPC transformou essa diferença em dois defeitos reais na unidade de cálculo. Uma função lia uma variável de contagem que nunca era atribuída antes do uso, e outra usava duas coordenadas num ramo antes de o código que as calculava correr num ramo diferente. Sob o Delphi ambas comportavam-se segundo o que calhasse estar na stack, que é a definição de um bug que se reproduz numa máquina e não noutra

A conclusão prática é que o segundo compilador vale a pena manter no circuito mesmo para um produto que é distribuído primariamente no primeiro. Passar periodicamente os olhos pelas classes de avisos do FPC é uma passagem de análise estática barata sobre uma base de código Delphi, e encontra uma categoria de defeito que nenhuma suite de testes alcança de forma fiável. A disciplina mais ampla de matriz de versões em que isto se insere está descrita em a matriz de build entre compiladores

O suporte de Free Pascal e Lazarus para Windows é distribuído com o componente de folhas de cálculo Delphi HotXLS como um pacote Lazarus a par dos pacotes Delphi e C++Builder, compilado a partir da mesma árvore de código e não de um fork. Esse é o ponto do exercício: um motor, quatro toolchains, e as decisões específicas do compilador isoladas em ficheiros include que se podem ler numa só sentada