Artigo Técnico

Diagnosticando falhas silenciosas de stub em biblioteca PDF

Quando uma biblioteca Delphi ganha uma configuração de build sem o framework visual, as classes substitutas são onde os bugs vivem. Não a plataforma, não o compilador: os stand-ins. O PDFlibPas tem uma camada de gráficos que fornece equivalentes de bitmap, canvas, fonte, metafile e impressora para builds sem a VCL, e portá-la para o Free Pascal trouxe à tona todo modo de falha que um stand-in pode ter. Eles se ordenam de forma clara por custo de diagnóstico, e a ordenação é o oposto do que a intuição sugere

Um stand-in que lança exceção é barato de encontrar; a exceção nomeia o método. Um stand-in que retorna dados vazios é caro, porque a falha aparece várias camadas longe de sua causa. Um stand-in que retorna sucesso é o pior de todos, porque o código de retorno é válido, o código de erro é zero, nenhuma exceção é lançada, e a única evidência de que algo deu errado está nos bytes que saíram

Três formas de falha silenciosa de stub em uma biblioteca PDF Pascal ordenadas por custo de diagnóstico, de um stub que lança exceção a sucesso sobre saída vazia
Um stand-in que lança exceção é barato de diagnosticar, dados vazios são caros, e um retorno de sucesso sobre um artefato vazio é o pior de encontrar

Forma três: um identificador de imagem válido sobre um XObject vazio

O conversor vetorial de metafile era um corpo de procedimento vazio na configuração sem VCL. Tudo acima dele continuava funcionando. Os pontos de entrada de importação EMF e o ponto de entrada de captura de canvas rodavam até o fim e retornavam um identificador de imagem legal, que o chamador então colocava em uma página. O que parava no arquivo era um form XObject com comprimento de conteúdo zero. A página renderizava branca

Nada reportou um problema, e isso inclui o próprio programa de demonstração da biblioteca para este recurso, que desenhou uma página em branco e não percebeu. Não havia valor de retorno de falha a verificar, porque a sequência de chamadas genuinamente todas teve sucesso; a única coisa errada era o tamanho do stream produzido. Diagnosticar essa classe de defeito significa fazer uma pergunta diferente: não "a chamada falhou", mas "o artefato é plausível". Um form XObject de comprimento zero, uma imagem de zero pixels, uma página de zero bytes de conteúdo, essas são as asserções que o capturam

A correção tem duas metades e a segunda metade é fácil de esquecer. Primeiro, faça a implementação vazia lançar exceção, para que a falha tenha um canal ao menos. Segundo, converta essa exceção em um resultado nulo na fábrica de imagens e adicione verificações de null nos dois lugares que consomem um identificador de imagem, porque de outra forma "falha limpa" vira diretamente um access violation quando a árvore de páginas desreferencia nada. Um stub que lança exceção só é uma melhoria se os chamadores estavam preparados para uma falha que antes nunca podiam receber

Forma dois: dados vazios, três camadas do crash

O stand-in de canvas de metafile não preenchia suas dimensões físicas. Esse valor divide em um cálculo de geometria de página, então o cálculo produzia zero, então o cálculo da bounding box dividia por zero. Um handler de exceção bare engolia isso, a fábrica de imagens retornava um resultado nulo, e o access violation finalmente acontecia na árvore de páginas quando o nulo era usado. Três camadas entre causa e sintoma, com um handler de exceção no meio apagando a evidência

Cadeia de falha de um stand-in de canvas de metafile vazio no PDFlibPas alcançando um access violation três camadas após a divisão por zero
A dimensão vazia leva um cálculo de geometria a zero, um handler bare apaga a exceção, e o identificador nulo crasha a árvore de páginas

A mesma unidade tinha mais duas instâncias do padrão. A classe de fonte tinha corpos de Assign e de construtor vazios, o que importa mais do que parece porque a propriedade de fonte do canvas é somente leitura: atribuir a ela é o único jeito de entregar uma fonte, então uma implementação vazia torna a seleção de fonte silenciosamente ineficaz e o texto sai com o que quer que o padrão fosse. E um valor de pixels por polegada de zero fazia todo chamador que dimensiona um canvas a partir de métricas de fonte produzir um canvas de zero por zero, o que rende uma página em branco e um retorno de sucesso

// A forma a procurar em uma unidade de stand-in: um método que nem
// lança exceção nem faz nada. Ambos compilam e ambos produzem
// "sucesso" sem saída alguma
procedure TMetafileCanvasStandIn.Create(...);
begin
  // sem chamada herdada, sem inicialização de campos
end;

function TBitmapStandIn.LoadFromStream(Stream: TStream): Boolean;
begin
  Result := True;    // e o bitmap continua vazio
end;

A estrutura wide que guarda apenas o primeiro caractere

Esta nem é um problema de stand-in, mas pertence ao mesmo catálogo porque o sintoma está igualmente longe da causa. A estrutura de enumeração de impressoras foi declarada com todos os seus doze membros de string tipados como ponteiros para caracteres de um byte, enquanto a função que a preenche é a variante de caracteres wide da API de enumeração

Os tamanhos de ponteiro são idênticos, então o layout da estrutura está correto e nada crasha. O que acontece em vez disso é que ler uma string UTF-16 como string de um byte para no primeiro byte zero, que para qualquer nome de impressora ASCII é a metade alta do segundo caractere. Todo nome de impressora voltava com exatamente um caractere. A jusante, a validação de nome falhou, a criação de impressora falhou e a impressão falhou para toda impressora real da máquina, e nenhum desses sintomas aponta para uma declaração de estrutura

Nome de impressora UTF-16 truncado a um caractere depois que uma estrutura Win32 wide é declarada com membros PAnsiChar em vez de PWideChar
Membros de um byte leem um nome UTF-16 só até seu primeiro byte zero, então todo nome de impressora volta com exatamente um caractere
// Errado: tamanho certo, tipo de elemento errado. Sem erro de
// compilação, sem crash, toda string truncada a um caractere
type
  TPrinterInfo2Wrong = record
    pServerName: PAnsiChar;
    pPrinterName: PAnsiChar;
    // ... dez a mais
  end;

// Certo: uma estrutura *W tem membros wide por completo
type
  TPrinterInfo2W = record
    pServerName: PWideChar;
    pPrinterName: PWideChar;
    // ... dez a mais
  end;

A regra que sai disso é mecânica e vale aplicar sem pensar: para qualquer estrutura Win32 cujo nome termina em W, verifique que cada membro de string é a variante wide, campo a campo. Misturar os mundos ANSI e wide não produz nem um diagnóstico do compilador nem um crash, só truncamento silencioso, e o mesmo vale ao contrário para as variantes ANSI

Um handler de exceção bare é o adversário real

Cada uma dessas investigações foi retardada pelo mesmo constructo: um handler que captura tudo e converte em um valor de retorno falso. É algo razoável de escrever em torno de um decoder de imagens, já que uma imagem corrompida não deve derrubar um trabalho de documento. Também é um dispositivo para apagar a única informação de que você precisa

A resposta prática é tornar o handler temporariamente barulhento. Despejar a classe de exceção, mensagem e backtrace de dentro do handler bare, sob um condicional de debug, converte um retorno nulo inexplicado em uma exceção nomeada com uma localização. Em dois dos três casos acima esse único passo encerrou a investigação, porque a exceção era uma divisão por zero ou um access violation em um método de stand-in cujo nome dizia tudo

Checklist para adotar um caminho de stand-in

Quatro itens, na ordem em que compensam. Antes de chamar uma classe substituta, leia os métodos que você está prestes a usar e confirme que cada um tem um corpo real; um corpo vazio não é um detalhe de implementação, é um recurso ausente. Prefira stand-ins que lançam exceção a stand-ins que retornam valores neutros, e combine isso com verificações de null nos lugares em que uma fábrica agora pode legitimamente não retornar nada. Verifique um recurso inspecionando o artefato, não o código de retorno, já que todo o modo de falha aqui é um código de retorno limpo sobre um artefato vazio; uma decomposição em nível de byte do que um documento realmente contém é o jeito mais rápido de ver isso, e o artigo de auditoria de tamanho de arquivo cobre essa ferramenta. E quando um recurso não tem implementação substituta viável, encaminhe as amostras afetadas ao caminho que funciona e diga por quê em um comentário, em vez de deixar uma demonstração que silenciosamente produz saída em branco

O ponto mais amplo se aplica bem além de uma biblioteca. Qualquer base de código com uma segunda implementação condicional, uma camada de mock, um modo headless, um shim de plataforma, está exposta à forma três. A razão de ela se esconder tão bem é que todo gate de qualidade em que uma equipe normalmente confia, códigos de retorno, códigos de erro, exceções, status de saída, é um canal de status, e a forma três mantém todos eles limpos. Só a saída o trai. Esse também é o raciocínio por trás de verificar artefatos em vez de status ao tratar entrada não confiável, descrito em o artigo sobre parsing de PDF não confiável, e por trás de comparar saída renderizada entre engines em vez de confiar em uma, descrito em renderização multi-engine

O PDFlibPas é uma biblioteca PDF em Object Pascal nativa para Delphi, C++Builder e Free Pascal, e sua configuração sem VCL é o que torna possíveis builds headless e entre toolchains; a cobertura atual de configurações está listada na página de produto da losLab PDF Developer Library