Artigo Técnico

Matriz de build cross-compiler Delphi: HotXLS desde XE5

O HotXLS distribui uma única codebase Object Pascal para cada release de Delphi e C++Builder a partir do XE5, e build-All-Lib-TRIAL.cmd é o script que prova isso: 43 legs de build, cobrindo 12 versões Delphi em Win32 e Win64, além de 10 builds de pacotes C++Builder Win32 e 9 Win64. Da v2.363 à v2.374, esse script nunca foi executado até o fim, e a leg do XE5 ficou quebrada durante todo o período

Nada na falha era sutil depois que ela foi vista. Cinco construções distintas que o compilador atual aceita sem comentário são erros fatais no RAD Studio XE5, que a matriz chama de 12.0. A release v2.375.0 corrigiu as cinco e a matriz voltou a ficar verde em 43 de 43. A seguir estão cada rejeição, por que o compilador antigo está discutivelmente certo sobre as duas que rejeita por motivos de tipo e a parte mais constrangedora: o script de probe escrito para diagnosticar a bagunça informou um falso sucesso na primeira execução

Por que a leg do XE5 apodreceu sem que ninguém percebesse?

A leg do XE5 apodreceu porque o desenvolvimento diário executava apenas o conjunto de quatro scripts da versão 37.0, e um build local verde não diz nada sobre um compilador que você não invocou. A matriz completa é um script separado e lento, chamado pelo instalador trial antes de o Inno Setup coletar os arquivos, então é exercitada no momento do packaging, não no commit. Doze releases cabem nessa lacuna

A aritmética das legs vale ser detalhada porque é onde mora a ilusão de cobertura. DELPHI_TRIAL_VERSIONS enumera de 12.0 a 37.0, e cada uma dessas 12 versões é compilada duas vezes, Win32 e Win64. CB_TRIAL_WIN32_VERSIONS lista 10 versões, e CB_TRIAL_WIN64_VERSIONS apenas 9, porque o XE5 tem um projeto de pacote C++Builder, mas não distribui o objeto de inicialização de pacote Win64 c0pkg64.o. Doze mais doze mais dez mais nove são 43. Executar quatro delas e chamar a codebase de portátil é um erro de categoria, e é o erro específico que permitiu que isso acontecesse

O HotXLS já foi atingido pela mesma forma de problema na direção oposta. Uma unit nova alcançável por uma cláusula uses, mas ausente da lista de arquivos .cbproj, compila perfeitamente no Delphi, porque dcc puxa implicitamente units não listadas para o pacote e, no pior caso, emite uma dica W1033. O C++Builder emite um .obj somente para units nomeadas em <DelphiCompile>, então o mesmo código morre na etapa ilink com um símbolo externo não resolvido. Uma toolchain esconde o que a outra detecta. Esse é todo o argumento para executar a matriz em vez de confiar em um compilador representativo

Type casts rígidos que os compiladores Win32 antigos rejeitam

Duas das cinco rejeições são o mesmo bug com roupas diferentes: um type cast rígido aplicado a uma expressão de ponto flutuante, e não a uma variável. No Win32, os compiladores antigos avaliam a aritmética pela stack x87, então uma adição envolvendo um Double é mantida com precisão extra de 80 bits e seu tipo estático vira o Extended de 10 bytes. Converter 10 bytes para um TDateTime de 8 bytes não é um typecast legal, e o compilador informa isso com E2089 Invalid typecast

O detalhe enlouquecedor é que a forma com variável funciona. TDateTime(Serial) compila em todas as versões da matriz, porque Serial já tem 8 bytes e o cast preserva o tamanho. Acrescente qualquer coisa e a expressão se amplia por baixo de você. A correção não é um cast mais largo nem uma define condicional, mas parar de fazer cast: uma atribuição implícita de real para real converte corretamente em todos os compiladores suportados pelo HotXLS e diz o que o código realmente significa

// Rejeitado no XE5 (Win32): cada adicao e avaliada como um
// Extended de 10 bytes, e o cast de 10 para 8 bytes levanta E2089
if Dates1904 then
  Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
  Value := TDateTime(Serial + 1)
else
  Value := TDateTime(Serial);        // este e aceito: sem adicao

// Seguro entre versoes: deixe a atribuicao real-para-real fazer a conversao
if Dates1904 then
  Value := Serial + XLSDate1904Offset
else if Serial < 60 then
  Value := Serial + 1
else
  Value := Serial;

// Mesma classe de rejeicao no empacotador de valores de celula: um cast Double
// rigido de um inteiro. Divida em vez disso: o operador ja produz um real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then   // E2089
  ;
if (Scaled = intVal) and (intVal / 100 = AValue) then           // portatil
  ;

O branch Serial < 60 é a ficção do ano bissexto de 1900, não um off-by-one: o serial 60 é o inexistente 1900-02-29 do Excel, então os seriais abaixo dele precisam do dia extra antes de DecodeDate recebê-los. Trabalho de portabilidade nunca deve alterar silenciosamente esse tipo de lógica, e é exatamente por isso que a edição segura remove o cast e deixa a aritmética intacta

O que quebra quando nil é um argumento procedural?

Um nil simples passado onde se espera um tipo procedural não consegue ser associado durante a resolução de overload nos compiladores antigos. O ponto de chamada no HotXLS é ResolveIndexedColor, que é overloaded e recebe um callback TXLSTryResolveSystemColor que a maioria dos chamadores não precisa. Compiladores novos resolvem nil contra o parâmetro procedural e escolhem o overload correto. O XE5 não, e o diagnóstico aponta para o conjunto de overloads, não para o argumento, que é como se perdem vinte minutos

A resposta portável é dar um tipo ao callback nulo. Uma variável no nível da unit do tipo procedural é inicializada com zero pela linguagem, então já é nil sem inicializador e carrega a informação de tipo que o resolver antigo quer. Quando uma variável de unit seria exagero, um local tipado ao qual se atribui nil faz o mesmo trabalho

var
  // Um literal procedural nil nao se associa nos compiladores antigos
  // durante a resolucao de overload; uma variavel tipada e inicializada em zero associa
  NilSystemColorResolver: TXLSTryResolveSystemColor;

// ...

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

// A mesma correcao com um local tipado, no 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;

Observe que essa é uma diferença real no nível da linguagem, não um bug de compilador que valha contornar com defines. A variável inicializada com zero está correta em todas as versões da matriz e custa uma linha, então não há compilação condicional aqui. Recorra a {$IF CompilerVersion} somente quando a plataforma realmente diferir entre releases, o que acontece exatamente uma vez neste batch

Métodos VCL protected mudam entre releases

TPicture.LoadFromStream é public na VCL atual e protected nas versões antigas suportadas pelo HotXLS, portanto uma chamada direta compila agora e falha depois. O HotXLS o usa para validar que o payload de imagem de fundo de uma worksheet realmente decodifica, uma checagem de assinatura que roda antes de o exporter HTML assumir o compromisso de incorporar os bytes. A resposta clássica do Pascal se aplica: declare um descendente na mesma unit puramente para ampliar a visibilidade e faça o cast para ele no ponto de chamada

type
  // TPicture.LoadFromStream e protected nas versoes VCL antigas que a
  // biblioteca suporta; um descendente na mesma unit o expoe
  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);

O truque da classe accessor é seguro aqui porque o descendente não adiciona campos e nunca é instanciado; o cast apenas muda o que o compilador permitirá nomear. Ainda vale um comentário na declaração, pois um leitor que só compila em uma IDE atual verá um tipo sem propósito. O tratamento de imagens de fundo aparece de novo no caminho de renderização da grade VCL customizada, em que o mesmo payload decodificado alimenta a planilha na tela

O tipo do token GdiplusStartup mudou duas vezes

A única rejeição do batch que realmente exige compilação condicional é o tipo do parâmetro var de GdiplusStartup, que mudou entre gerações da VCL de uma forma que não deixa uma única grafia válida em todos os lugares. Probes versão a versão fixaram o comportamento real: as legs 12.0 a 20.0 aceitam somente Cardinal, as legs 21.0 e 22.0 aceitam somente THandle ou ULONG_PTR, e 23.0 e 37.0 aceitam ambos. Nos nomes de release, isso é Cardinal do XE5 ao 10.3 Rio e THandle a partir do 10.4 Sydney. Como os intervalos aceitantes não se sobrepõem de 12.0 a 22.0, nenhuma declaração incondicional funciona: a guarda usa CompilerVersion >= 34, que é Sydney, e a chamada é totalmente qualificada como Winapi.GDIPAPI.GdiplusStartup, para que a ordem de resolução de units não substitua outra declaração em uma versão intermediária

function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
  StartupInput: TGdiplusStartupInput;
  // O tipo do parametro var de GdiplusStartup em GDIPAPI segue a geracao da VCL
  // Cardinal ate Rio, THandle a partir de 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');
  // ... codifique ...
end;

Esse é o branch TIFF do exporter de imagens de página, portanto o raio de impacto de errar é toda a superfície de exportação raster, incluindo os caminhos descritos em exportar um intervalo de células como uma única imagem. Observe também o que a guarda não afirma: ULONG_PTR e THandle têm a mesma largura nas duas plataformas, então a escolha diz respeito a qual identificador a declaração nomeia, não à correção em 32 ou 64 bits

Por que a primeira execução do probe não informou nada?

O probe de versão não informou nada na primeira execução porque atribuições res=$(...) eram feitas dentro de um subshell, onde não se propagam para o pai. dcc32 sai com 0 em caso de sucesso, então o código de saída era o sinal correto a capturar, e o script o capturava em uma variável que deixava de existir uma linha depois. Todas as legs voltaram vazias e a saída parecia de um probe que não havia compilado nada, que era exatamente o que havia acontecido

A segunda falha foi pior, porque produziu uma resposta errada, não ausência de resposta. O probe classificava uma leg contando linhas que correspondiam a Error, e o Delphi não prefixa todo fatal com essa palavra. F1026 File not found é fatal e não corresponde, então um probe que não conseguia resolver uma unit era pontuado como sucesso limpo. O XE5 não distribui Winapi.GDIPOPS.dcu, o primeiro probe encontrou exatamente isso e ficou falsamente verde. A regra que saiu daí é estreita e vale declarar claramente: julgue um probe de compilador pelo artefato produzido ou pela própria linha de resumo do compilador, nunca procurando uma palavra na saída. Procurar Error no stderr é uma heurística que falha na direção que você não pode aceitar, reportando sucesso silenciosamente

Quanto realmente custa suportar uma década de compiladores

A contabilidade honesta é que as mudanças de código aqui são triviais e as mudanças de processo não são. Quatro das cinco rejeições foram corrigidas escrevendo Pascal mais comum, não adicionando mecanismos de versão: remover um cast, dividir em vez de fazer cast, dar um tipo a nil e declarar uma classe accessor. Somente GdiplusStartup mereceu um {$IF}. Uma codebase que vai do XE5 à release atual não vira um matagal de defines condicionais a menos que você deixe casts rígidos e idioms do compilador mais novo se acumularem desde o início

O que realmente custa é tempo de build e disciplina. Quarenta e três legs formam um script lento, justamente por isso ele escorregou para o packaging e depois para o nunca. O meio-termo defensável é manter o loop rápido de quatro scripts para iteração e executar a matriz completa em uma agenda que não possa ser pulada, porque o modo de falha não é um build quebrado que você percebe, mas uma IDE suportada que silenciosamente deixou de ser suportada doze releases atrás

Essa obrigação é o outro lado de distribuir um componente nativo. O HotXLS lê e grava XLS, XLSX e ODS somente por Object Pascal, sem instalação do Excel e sem dependência de COM, o que torna possível a automação de workbooks sem Office em um servidor restrito. A mesma propriedade significa que o compilador é todo o contrato da plataforma, então cada versão na matriz é uma promessa que precisa ser reverificada, não presumida

A matriz de build cross-compiler e o código seguro entre versões discutidos aqui fazem parte do HotXLS Delphi Spreadsheet Component, que oferece suporte a Delphi e C++Builder do XE5 à release atual, com binários de biblioteca pré-compilados para cada IDE compatível