O mesmo código-fonte Object Pascal pode se comportar de maneira diferente sob Delphi e FPC/Lazarus de quatro formas que rotineiramente afetam o código do PDFium Component: o FPC descarta registros temporários resultantes de funções antes que um teste de pertinência de conjunto in termine de lê-los, o dcc32 vem com a verificação de intervalo (range checking) desativada por padrão de modo que índices de array fora dos limites leiam lixo silenciosamente, apenas o Delphi 13 aceita a atribuição de um array of Byte anônimo para TBytes sem uma coerção (cast) de tipo, e a concatenação AnsiString do Delphi pode destruir bytes em valores iguais ou superiores a $80 por meio de uma conversão implícita de página de código. Cada um desses comportamentos produz uma suíte de testes que passa em um compilador e falha (ou, pior ainda, apresenta erro silencioso) no outro
Se você estiver configurando um projeto para compilador duplo pela primeira vez, o guia passo a passo do visualizador Lazarus e FPC aborda o caminho feliz: pacotes, caminhos de pesquisa e exibição de uma janela de renderização na tela. Este artigo é o oposto de um tutorial. É a lista de problemas que encontramos após o caminho feliz funcionar, quando o CI estava verde sob FPC, verde sob Delphi, e então uma alteração que passou de um lado causou falhas do outro. Cada armadilha abaixo vem de uma falha real na suíte de testes do PDFiumPas ou em seus demonstrativos, com a análise no nível de commit condensada em uma reprodução mínima, a causa raiz e a correção na qual nos padronizamos
Por que um conjunto é lido como vazio sob FPC, mas não no Delphi?
A versão resumida: o FPC pode finalizar a variável temporária que contém o resultado do registro de uma função antes que uma expressão que lê um campo desse resultado tenha terminado, de modo que X in Func().Issues possa testar a pertinência contra um conjunto que já foi liberado, enquanto a expressão equivalente no Delphi funciona. Nossos testes de conformidade PDF/E encontraram isso em sua primeira versão. O validador retorna um registro cujo campo Issues é um conjunto de sinalizadores de violação, e as asserções traziam a chamada na própria linha
// Unreliable under FPC: the function-result record temporary
// can be released before the 'in' test reads Issues
AssertTrue(pveiLzwUsed in ValidateAnsi(Pdf).Issues);
// Reliable on both compilers: pin the result to a local first
var
Vr: TPdfEValidationResult;
begin
Vr := ValidateAnsi(Pdf);
AssertTrue(pveiLzwUsed in Vr.Issues);
end;
A forma em linha (inlined) lia o conjunto como vazio sob FPC, de modo que cada asserção que esperava um sinalizador falhava, enquanto a compilação idêntica no Delphi passava. A causa raiz é uma diferença na forma como os dois compiladores gerenciam o tempo de vida de variáveis temporárias resultantes de funções dentro de expressões maiores: o Delphi mantém a temporária viva até o final da instrução, enquanto o descarte da temporária do registro pelo FPC pode disputar corrida (race condition) com o operador de pertinência de conjunto que ainda o está lendo. Já havíamos documentado esse mesmo comportamento uma vez anterior, em um comentário no utilitário FlagPresent na unidade de testes PDF/A, e então reintroduzimos o bug de qualquer forma ao escrever novos testes do zero, o que mostra quão natural a forma incorreta parece. A correção é mecânica e vale a pena ser adotada como regra geral: nunca encadeie um acesso a campo ou teste de conjunto diretamente a uma chamada de função que retorna um registro; atribua o resultado a uma variável local primeiro e depois leia o campo. Isso custa uma linha e elimina toda uma classe de instabilidade dependente do compilador
Por que o Delphi aceita um índice de array que o FPC se recusa a compilar?
A versão resumida: o dcc32 compila um índice fora da faixa em um array com limites fixos e, com sua verificação de intervalo padrão desativada, lê ou grava memória adjacente em tempo de execução sem gerar erros, enquanto o FPC rejeita o mesmo índice em tempo de compilação. O PDFium Component declara os pontos de quadrilátero como um array baseado em 1, TQuadrilateralPoint = array [1..4] of TPdfPoint, correspondendo a como as entradas QuadPoints do PDF costumam ser numeradas. Um demonstrativo que o preenchia com o loop reflexivo baseado em 0 funcionou por meses sob o Delphi
var
I: Integer;
begin
for I := 0 to 3 do // wrong: the array is [1..4]
Data.AttachmentPoints[I] := Corner[I]; // dcc32 default: compiles, index 0
// silently touches adjacent memory
// FPC: compile-time range check error
for I := Low(TQuadrilateralPoint) to High(TQuadrilateralPoint) do
Data.AttachmentPoints[I] := Corner[I - 1]; // correct on both compilers
end;
A compilação no Delphi foi um falso positivo: com a verificação de intervalo desativada, que é o padrão do dcc32, o índice 0 caía em qualquer campo que precedesse o array no registro, e o demonstrativo parecia rodar. Portar o mesmo demonstrativo para o Lazarus produziu um erro imediato de verificação de intervalo em tempo de compilação pelo FPC, e corrigir o índice então expôs um segundo bug mais profundo no caminho de anotações da biblioteca que as leituras inválidas vinham ocultando — aquele analisado no artigo de anotações com pontos quadriláteros. Duas lições surgiram desse incidente. Primeiro, prefira Low() e High() em vez de limites literais sempre que o tipo de array não for baseado em 0 por construção. Segundo, trate uma compilação FPC, ou no mínimo uma compilação Delphi com {$R+} ativado, como uma etapa obrigatória de primeira execução para qualquer demonstrativo ou teste novo: os padrões do dcc32 não informarão sobre essa classe de bug, e um programador rodando não é evidência de que ele esteja correto
A atribuição de TBytes que apenas o Delphi 13 aceita
A versão resumida: a atribuição de um campo declarado como array of Byte anônimo para uma variável TBytes compila no Delphi 13 (versão do compilador 37.0), mas falha no Delphi 12 Athens e em todas as versões anteriores com o erro E2010 Incompatible types: 'TArray<Byte>' and 'Dynamic array'. Esse caso não é uma divisão Delphi versus FPC, mas sim uma divisão do Delphi versus o seu próprio passado, afetando a base de código de compilador duplo da mesma maneira: o compilador mais novo aceita silenciosamente uma construção que todos os outros rejeitam
type
TValidator = class
private
FBuffer: array of Byte; // anonymous dynamic array type
end;
var
OrigBytes: TBytes;
begin
OrigBytes := FBuffer; // Delphi 13 only; E2010 on Delphi 12
// Athens and earlier
OrigBytes := TBytes(FBuffer); // compiles everywhere; same byte layout,
// safe hard cast
end;
Entregamos exatamente isso em uma rotina de validação, desenvolvida e testada localmente no Delphi 13, onde a conversão implícita foi aceita silenciosamente. O instalador com código-fonte completo atende a usuários no Delphi 12 e anteriores em grande escala e, para eles, a unidade simplesmente não compilava. A correção estrutural é a coerção forçada (hard cast) exibida acima, que é segura porque um array of Byte anônimo e o TBytes compartilham um layout de array dinâmico idêntico, ou melhor, declarar o campo como um tipo nomeado como TBytes desde o início, para que a conversão nunca ocorra. A correção do processo importa mais: uma construção que compila na sua cadeia de ferramentas mais recente não prova nada sobre os compiladores mais antigos que seus usuários realmente executam, e essa categoria de regressão é invisível até que você compile contra todas as versões suportadas. Nossos scripts de lançamento agora compilam a biblioteca em toda a matriz de compiladores precisamente porque uma compilação 37.0 local não consegue capturar uma flexibilidade exclusiva da versão 13
O byte AnsiString que desaparece em uma máquina Windows chinesa
Concatenar um byte bruto igual ou superior a $80 em uma AnsiString com + pode substituir silenciosamente esse byte por ? ($3F) sob o Delphi, porque a expressão realiza uma conversão implícita de AnsiString para UnicodeString e de volta para AnsiString através da página de código do sistema. Descobrimos isso por meio de um teste de PDF/A que constrói um nome contendo um byte $FE isolado — que nunca é um byte de cabeçalho UTF-8 legal — para verificar se o validador sinaliza nomes que não são UTF-8 válidos conforme a ISO 19005-2 cláusula 6.1.8
var
BadName: AnsiString;
begin
// On Delphi with a multi-byte system code page (observed on CP936),
// the concatenation round-trips through UnicodeString and $FE, which
// is not a valid CP936 sequence, comes back as '?' ($3F)
BadName := '/Bad' + AnsiChar($FE) + 'Name';
// Safe: build with an ASCII placeholder, then patch the byte in place;
// indexed assignment into a settled AnsiString does not round-trip
BadName := '/Bad' + #1 + 'Name';
BadName[5] := AnsiChar($FE);
end;
Em um sistema Windows chinês executando a página de código 936, a string concatenada nunca conteve o byte $FE, de modo que a biblioteca corretamente não reportava nada e o teste falhava (ficava vermelho) parecendo um bug da biblioteca. A biblioteca nunca esteve errada: um executor FPC que alimentava um PDF genuinamente contendo o byte $FE obtinha o sinalizador esperado. A corrupção ocorria dentro do executável de testes do Delphi enquanto a expressão de string estava sendo avaliada, porque o modelo de string prioritariamente Unicode do Delphi converte expressões AnsiString mistas através de UnicodeString, e o byte $FE não é um byte de cabeçalho válido no CP936, de modo que a viagem de ida e volta o substitui. Deve-se ser honesto sobre esse limite: em uma página de código ocidental de byte único, como o CP1252, a mesma expressão costuma sobreviver, razão exata pela qual esse bug se esconde na maioria das máquinas de desenvolvimento e surge apenas em sistemas do Leste Asiático ou executores de CI localizados. A regra que adotamos: nunca construa vetores de teste binários contendo bytes iguais ou superiores a $80 por meio de concatenação AnsiString; corrija os bytes diretamente na posição após a string estar definida, como feito acima, ou construa o vetor em TBytes desde o início
O que um fluxo de trabalho com compilador duplo deve verificar por padrão
Quatro armadilhas, um padrão: cada compilador informa sobre um subconjunto diferente dos seus bugs. A análise de intervalo em tempo de compilação do FPC capturou um índice fora dos limites que o dcc32 executou silenciosamente por meses, e o modelo de string Unicode do dcc32 expôs uma dependência de página de código que uma compilação FPC puramente orientada a bytes nunca aciona. A consequência prática é que nenhuma das duas esteiras (pipelines) verdes é suficiente de forma isolada. A compilação cruzada não é apenas uma caixa de seleção de portabilidade — ela é um segundo analisador estático e um segundo modelo de tempo de execução aplicado ao mesmo código-fonte, no mesmo espírito das verificações defensivas de limites descritas no artigo sobre robustecimento de ABI e segurança de memória
As regras permanentes que resultaram desses incidentes são curtas o suficiente para serem memorizadas. Fixe registros resultantes de funções a uma variável local antes de ler campos. Percorra arrays de limites fixos com Low() e High(), e execute pelo menos uma compilação com verificação de intervalo ativada ou no FPC antes de confiar em qualquer demonstrativo novo. Coerça os campos de array dinâmico anônimo explicitamente, ou declare-os com tipos nomeados, e compile em toda a matriz de compiladores antes do lançamento. Mantenha bytes brutos altos totalmente fora da concatenação de AnsiString. Nenhum desses procedimentos custa esforço mensurável depois de se tornarem hábitos, e cada um fecha um modo de falha que um fluxo de trabalho de compilador único estruturalmente é incapaz de enxergar
Todos os outros problemas foram encontrados e corrigidos no decorrer da manutenção do PDFium Component, que fornece o mesmo código-fonte Object Pascal para Delphi, C++Builder e FPC/Lazarus, e executa suas suítes de conformidade e regressão em cada uma dessas cadeias de ferramentas, de modo que as armadilhas descritas neste artigo sejam protegidas por testes, e não pela memória