O HotXLS distribui uma única base de código Object Pascal para todas as releases de Delphi e C++Builder desde XE5, e build-All-Lib-TRIAL.cmd é o script que o prova: 43 pernas de build, cobrindo 12 versões de 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é ao fim e a perna XE5 esteve avariada durante todo esse período
Nada na falha era subtil depois de ser vista. Cinco construções distintas que o compilador atual aceita sem comentários são erros graves no RAD Studio XE5, que a matriz identifica como 12.0. A release v2.375.0 corrigiu as cinco e a matriz voltou a verde em 43 de 43. O que se segue é cada rejeição, a razão pela qual o compilador antigo tem, em certa medida, razão nas duas que rejeita por motivos de tipos e a parte mais embaraçosa: o script de probe escrito para diagnosticar a confusão comunicou um falso sucesso na primeira execução
Porque é que a perna XE5 se degradou sem ninguém notar?
A perna XE5 degradou-se 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 não invocou. A matriz completa é um script separado e lento que o instalador trial chama antes de o Inno Setup recolher os ficheiros, por isso é exercitada no momento de empacotar e não no momento do commit. Doze releases couberam nessa lacuna
A aritmética das pernas merece ser explicitada porque é aí que vive 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 XE5 tem um projeto de pacote C++Builder mas não distribui o objeto de arranque do pacote Win64 c0pkg64.o. Doze mais doze mais dez mais nove são 43. Executar quatro delas e chamar portátil à base de código é um erro de categoria e é o erro específico que permitiu que isto acontecesse
O HotXLS já foi atingido pela mesma forma de problema na direção oposta. Uma unidade nova alcançável através de uma cláusula uses mas ausente da lista de ficheiros .cbproj compila perfeitamente em Delphi, porque o dcc puxa implicitamente as unidades não listadas para o pacote e, no pior caso, emite uma indicação W1033. O C++Builder emite um .obj apenas para unidades nomeadas em <DelphiCompile>, pelo que o mesmo código morre na fase ilink com um external não resolvido. Um toolchain esconde o que o outro apanha. Esse é todo o argumento para executar a matriz em vez de confiar num compilador representativo
Conversões rígidas de tipos que os compiladores Win32 antigos rejeitam
Duas das cinco rejeições são o mesmo bug com roupas diferentes: um hard cast aplicado a uma expressão de vírgula flutuante em vez de a uma variável. Em Win32, os compiladores mais antigos avaliam a aritmética através da stack x87, pelo que uma adição que envolva um Double é mantida com precisão excedente de 80 bits e o seu tipo estático passa a ser o Extended de 10 bytes. Converter 10 bytes para um TDateTime de 8 bytes não é um typecast legal e o compilador comunica-o com E2089 Invalid typecast
O detalhe enlouquecedor é que a forma com variável está correta. TDateTime(Serial) compila em todas as versões da matriz, porque Serial já tem 8 bytes e a conversão preserva o tamanho. Acrescente qualquer coisa e a expressão alarga-se por baixo de si. A correção não é uma conversão mais larga nem um define condicional; é parar de converter: uma atribuição implícita real para real faz a conversão corretamente em todos os compiladores suportados pelo HotXLS e diz o que o código realmente significa
// Rejeitado no XE5 (Win32): cada adição é avaliada como um
// Extended de 10 bytes e a conversão de 10 para 8 levanta E2089
if Dates1904 then
Value := TDateTime(Serial + XLSDate1904Offset)
else if Serial < 60 then
Value := TDateTime(Serial + 1)
else
Value := TDateTime(Serial); // este é aceite: não há adição
// Compatível entre versões: deixe a atribuição real para real fazer a conversão
if Dates1904 then
Value := Serial + XLSDate1904Offset
else if Serial < 60 then
Value := Serial + 1
else
Value := Serial;
// A mesma classe de rejeição no empacotador de valores de células: um hard cast
// de um inteiro para Double. Divida em vez disso, o operador já devolve um real
if (Scaled = intVal) and (Double(intVal) / 100 = AValue) then // E2089
;
if (Scaled = intVal) and (intVal / 100 = AValue) then // portátil
;
O ramo Serial < 60 é a ficção do ano bissexto de 1900, não um off-by-one: o serial 60 é o 1900-02-29 inexistente do Excel, pelo que os seriais abaixo dele precisam do dia extra antes de DecodeDate os ver. O trabalho de portabilidade nunca deve alterar silenciosamente esse tipo de lógica, razão exata pela qual a edição segura remove a conversão e deixa a aritmética intacta
O que quebra quando nil é um argumento procedural?
Um nil simples passado onde se espera um tipo procedural falha ao associar-se durante a resolução de overloads nos compiladores antigos. O ponto de chamada no HotXLS é ResolveIndexedColor, que está overloaded e recebe um callback TXLSTryResolveSystemColor de que a maioria dos chamadores não precisa. Os compiladores mais recentes resolvem nil contra o parâmetro procedural e escolhem a overload correta. O XE5 não o faz e o diagnóstico aponta para o conjunto de overloads, não para o argumento, que é a forma de perder vinte minutos
A resposta portátil é dar um tipo ao callback nulo. Uma variável ao nível da unidade do tipo procedural é inicializada a zero pela linguagem, pelo que já é nil sem inicializador e transporta a informação de tipo que o resolver antigo quer. Quando uma variável ao nível da unidade seria exagerada, uma local tipada à qual se atribua nil faz o mesmo trabalho
var
// Um literal procedural nil não se associa nos compiladores antigos
// durante a resolução de overloads; uma variável tipada e inicializada a zero associa-se
NilSystemColorResolver: TXLSTryResolveSystemColor;
// ...
FWorkbook.ResolveIndexedColor(AIndexedColor, xicsBiffIcv, ARole,
NilSystemColorResolver, Resolution);
// A mesma correção 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;
Note que esta é uma diferença real ao nível da linguagem e não um bug do compilador a contornar com defines. A variável inicializada a zero está correta em todas as versões da matriz e custa uma linha, pelo que não há aqui compilação condicional. Recorra a {$IF CompilerVersion} apenas quando a plataforma realmente difere entre releases, que é o caso exatamente uma vez neste batch
Métodos VCL protected mudam entre releases
TPicture.LoadFromStream é public na VCL atual e protected nas versões mais antigas suportadas pelo HotXLS, pelo que uma chamada direta compila agora e falha depois. O HotXLS usa-o para validar que o payload de uma imagem de fundo de worksheet é realmente descodificado, uma verificação de assinatura que corre antes de o exportador HTML se comprometer a incorporar os bytes. Aplica-se a resposta clássica de Pascal: declare um descendente na mesma unidade apenas para alargar a visibilidade e faça o cast através dele no ponto de chamada
type
// TPicture.LoadFromStream é protected nas versões VCL antigas suportadas
// pela biblioteca; um descendente na mesma unidade expõe-o
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 acrescenta campos e nunca é instanciado; o cast apenas muda o que o compilador permite nomear. Ainda vale a pena deixar um comentário na declaração, porque um leitor que só construa num IDE atual verá de outro modo um tipo sem utilidade. O tratamento de imagens de fundo surge novamente no percurso de renderização de grelha VCL personalizada, onde o mesmo payload descodificado alimenta a folha no ecrã
O tipo do token GdiplusStartup mudou duas vezes
A única rejeição do batch que exige genuinamente 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 todo o lado. A sondagem versão a versão fixou o comportamento real: as pernas 12.0 a 20.0 aceitam apenas Cardinal, as pernas 21.0 e 22.0 aceitam apenas THandle ou ULONG_PTR e 23.0 e 37.0 aceitam ambos. Nos nomes das releases, isso é Cardinal do XE5 até ao 10.3 Rio e THandle a partir do 10.4 Sydney. Como os dois intervalos de aceitação não se sobrepõem entre 12.0 e 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 unidades não substitua uma declaração diferente em alguma versão no meio do intervalo
function TXLSPageImageExporter.EncodeTiff(Stream: TStream): Integer;
var
StartupInput: TGdiplusStartupInput;
// O tipo do parâmetro var de GdiplusStartup em GDIPAPI segue a geração da VCL
// Cardinal até 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');
// ... codificar ...
end;
Este é o ramo TIFF do exportador de imagens de páginas, pelo que o raio de explosão de o fazer mal é toda a superfície de exportação raster, incluindo os percursos descritos em exportar um intervalo de células como uma única imagem. Note também o que a guarda não afirma: ULONG_PTR e THandle têm a mesma largura nas duas plataformas, pelo que a escolha diz respeito ao identificador que a declaração nomeia, não à correção de 32 bits contra 64 bits
Porque é que a primeira execução da probe não comunicou nada?
A probe de versões não comunicou nada na primeira execução porque as atribuições res=$(...) estavam a ser feitas dentro de um subshell, onde não propagam para o processo pai. dcc32 sai com 0 em caso de sucesso, pelo que o código de saída era o sinal certo a capturar e o script estava a capturá-lo numa variável que deixava de existir uma linha depois. Todas as pernas voltaram vazias e a saída parecia a de uma probe que não tinha compilado nada, que era exatamente o que acontecera
A segunda falha foi pior, porque produziu uma resposta errada em vez de nenhuma resposta. A probe classificava uma perna contando linhas que correspondessem a Error e o Delphi não prefixa todas as mensagens fatais com essa palavra. F1026 File not found é fatal e não corresponde, pelo que uma probe que nem sequer conseguisse resolver uma unidade era pontuada como sucesso limpo. O XE5 não distribui Winapi.GDIPOPS.dcu, a primeira probe encontrou exatamente isso e ficou falsamente verde. A regra que saiu daí é estreita e vale a pena dizê-la claramente: avalie uma probe de compilador pelo artefacto produzido ou pela linha de resumo do próprio compilador, nunca fazendo grep à saída por uma palavra-chave. Fazer grep a stderr por Error é uma heurística que falha na direção que não pode permitir, comunicando sucesso silenciosamente
Quanto custa realmente suportar uma década de compiladores?
A contabilidade honesta é que as alterações de código aqui são triviais e as alterações de processo não são. Quatro das cinco rejeições foram corrigidas escrevendo Pascal mais normal, não acrescentando maquinaria de versões: remover um cast, dividir em vez de converter, dar um tipo a nil e declarar uma classe accessor. Apenas GdiplusStartup ganhou um {$IF}. Uma base de código que vai de XE5 à release atual não se transforma num emaranhado de defines condicionais a menos que deixe hard casts e idioms do compilador mais recente acumularem-se logo no início
O que custa realmente é tempo de build e disciplina. Quarenta e três pernas formam um script lento, precisamente a razão pela qual derivou para o momento de empacotar e depois para nunca ser executado. O meio-termo defensável é manter o ciclo rápido de quatro scripts para iterar e executar a matriz completa numa agenda que não possa ser ignorada, porque o modo de falha não é um build quebrado que se nota: é uma IDE suportada que deixou silenciosamente de ser suportada há doze releases
Essa obrigação é o reverso de distribuir um componente nativo. O HotXLS lê e escreve XLS, XLSX e ODS apenas através de Object Pascal, sem instalação do Excel e sem dependência COM, que é o que torna possível a automação de workbooks sem Office num servidor bloqueado. A mesma propriedade significa que o compilador é o contrato de toda a plataforma, pelo que cada versão da matriz é uma promessa que tem de ser verificada novamente, não assumida
A matriz de build cross-compiler e o código compatível entre versões aqui discutidos são distribuídos como parte do HotXLS Delphi Spreadsheet Component, que suporta Delphi e C++Builder desde XE5 até à release atual, com binários de biblioteca pré-compilados para cada IDE suportado