Um binding Pascal sobre uma biblioteca C lê-se como Pascal vulgar. Chama-se um método, recebe-se um registo (record) de volta, liberta-se aquilo que se alocou. O problema é que o PDFium é uma biblioteca C e C++ com a sua própria convenção de chamada, as suas próprias larguras de inteiros e as suas próprias regras sobre a quem pertence a memória e quem a liberta. Nada disso atravessa a fronteira da linguagem por si só. Cada um destes contratos tem de ser reafirmado manualmente nas declarações Pascal, e uma única palavra errada transforma uma chamada de aspeto impecável numa corrupção de stack, num deslocamento (offset) truncado ou numa libertação dupla (double free). Uma auditoria v1.61.0 a um binding do Componente PDFium revelou um defeito de cada tipo. Vale a pena analisá-los porque não são específicos deste binding. São os perigos permanentes de envolver qualquer API C em Delphi ou Lazarus
cdecl faz parte do tipo da função, não é uma decoração
O PDFium é C compilado. Em Win32, os seus exports e, mais importante, os callbacks que invoca utilizam a convenção de chamada cdecl. Em cdecl, é quem chama que limpa a stack depois de a chamada retornar. O padrão nativo do Delphi é register, e o padrão C de Win32 para callbacks é stdcall nalgumas bibliotecas, onde é o chamado que efetua a limpeza. Quando uma estrutura entrega ao PDFium um ponteiro de função e se esquece o cdecl no tipo desse ponteiro, os dois lados discordam sobre quem ajusta o ponteiro da stack. Ou ambos o corrigem, ou nenhum o faz, e o ponteiro da stack desvia-se pelo tamanho dos argumentos a cada invocação
A razão pela qual este defeito é difícil de encontrar é que o dano não é local. A chamada corrompida retorna e parece correta. O desalinhamento surge mais tarde, nalguma função não relacionada cujo frame assenta agora num ponteiro de stack desviado em alguns bytes, manifestando-se como uma leitura descontrolada, um endereço de retorno corrompido ou um crash com um backtrace que não aponta para nenhum sítio próximo do callback que realmente errou. O preenchimento de formulários (form-fill) é o local clássico onde isto morde, porque a interface de preenchimento é um registo cheio de callbacks para os quais o PDFium liga de volta. Um deles, FFI_OpenFile, entrega ao PDFium uma função que este chamará para abrir um ficheiro externo, declarada como function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. O cdecl final é o detalhe que vale a pena copiar. Remova-o e o código continua a compilar, a ligar (link) e a correr, até ao momento em que o PDFium chama a função. A convenção pertence ao próprio tipo da função. Não é um açúcar opcional, e o compilador não o alertará quando estiver em falta, porque um tipo de função simples é um tipo Pascal perfeitamente legal. A única defesa é tratar a convenção de chamada como um campo obrigatório de cada assinatura importada e de cada callback que passa para o exterior
size_t tem a largura de um ponteiro, e em FPC Win64 isso significa 64 bits
O segundo defeito é uma incompatibilidade de largura de inteiros que apenas surge num alvo. O size_t do C está definido para ser suficientemente largo para conter qualquer tamanho de objeto, o que numa plataforma de 64 bits significa um inteiro de 64 bits sem sinal. As interfaces de carregamento progressivo do PDFium comunicam em deslocamentos (offsets) de bytes do tipo size_t. O registo FX_FILEAVAIL do fornecedor de disponibilidade contém um callback IsDataAvail que o PDFium chama com um deslocamento e um tamanho, e o callback AddSegment do registo FX_DOWNLOADHINTS recebe o mesmo. Ambos os parâmetros são size_t
IsDataAvail = function(
pThis : PFX_FILEAVAIL;
offset, size: size_t): FPDF_BOOL; cdecl;
AddSegment = procedure(
pThis : PFX_DOWNLOADHINTS;
offset, size: size_t); cdecl;
Se declarar esses deslocamentos como um tipo de 32 bits, o binding funciona em Win32 e em Delphi Win64, mas falha silenciosamente em FPC e Lazarus Win64. A causa é subtil. Em FPC Win64, NativeUInt é um tipo genuíno de 64 bits com a largura de um ponteiro, e size_t tem um alias para ele. O binding contém um comentário na secção de tipos a alertar precisamente contra a ocultação (shadowing) de NativeUInt em FPC, porque redefini-lo aí para um alias de 32 bits forçaria o size_t a 32 bits e corromperia todos os parâmetros size_t passados para a biblioteca ou escritos por ela. Um deslocamento de 64 bits que chegue a um parâmetro de 32 bits perde a sua metade superior. Para um ficheiro pequeno, cada deslocamento cabe em 32 bits e nada está errado. Para um ficheiro grande, no momento em que um deslocamento cruza a fronteira dos quatro gigabytes, o valor truncado aponta para um sítio completamente diferente, o PDFium pergunta se a gama de bytes errada está disponível, e o carregamento progressivo bloqueia ou lê lixo. O defeito é invisível até que o ficheiro seja suficientemente grande e o alvo seja aquele onde o size_t realmente alargou
Uma exceção Pascal nunca deve desenrolar (unwind) através de um frame C
A terceira classe diz respeito ao modelo de exceções, que o C não possui. Quando o PDFium chama um dos seus callbacks, o seu código Pascal corre dentro de uma stack de frames C e C++ que nada sabem sobre o mecanismo de exceções do Delphi. Se o seu callback gerar e deixar propagar a exceção, esta desenrola-se através de frames que nunca foram construídos para serem desenrolados. A própria limpeza do PDFium não é executada, as suas invariantes internas ficam parcialmente atualizadas, e o processo fica agora num estado que a biblioteca nunca previu. O contrato para estes callbacks é um código de retorno, não uma exceção
Dois callbacks tornam isto concreto. O FPDF_FILEWRITE é o destino onde o PDFium escreve um documento guardado, e o FPDF_FILEACCESS é a origem a partir da qual lê um documento de entrada. Ambos estão implementados aqui sobre um TStream do Delphi, e ambos podem falhar da forma que qualquer stream falha: o disco enche, o stream é fechado por baixo de si, uma leitura ultrapassa o fim. O callback de escrita envolve a escrita no stream e converte qualquer falha no código de falha do PDFium, em vez de a deixar escapar
function WriteBlock(
pThis: PFPDF_FILEWRITE;
pData: Pointer;
Size : LongWord): Integer; cdecl;
begin
// O PDFium trata qualquer retorno diferente de 1 como falha de escrita.
// Uma exceção Pascal não deve desenrolar através deste frame cdecl/C++,
// por isso capturamo-la e reportamos falha em vez disso.
Result := 0;
try
PPdfWrite(pThis).Stream.WriteBuffer(pData^, Size);
Result := 1;
except
end;
end;
O lado da leitura faz o mesmo: uma leitura falhada reporta zero para corresponder ao contrato de FPDF_FILEACCESS, em vez de gerar um erro através da fronteira. Um except vazio sem relançamento parece errado para um programador Pascal treinado para nunca silenciar exceções, e em Pascal comum é errado. Numa fronteira ABI tem o formato correto, porque o único valor seguro a devolver ao chamador C é um código de estado que ele saiba interpretar. A falha continua a propagar-se, apenas através do valor de retorno, e o código chamador acima da biblioteca expõe-na como EPdfError assim que o controlo regressa ao lado Pascal da fronteira
A libertação dupla esconde-se no caminho de erro
O quarto defeito é a propriedade (ownership). Um handle de documento PDFium é aberto pela biblioteca e tem de ser fechado exatamente uma vez, através de FPDF_CloseDocument. O perigo é um caminho de erro que liberta um handle que uma segunda rotina de limpeza também possui. Imagine uma rotina que cria um objeto wrapper, lhe atribui um handle de documento acabado de abrir e depois efetua mais configuração que pode falhar. Se a configuração falhar com uma exceção, um manipulador de retorno antecipado que chame FPDF_CloseDocument sobre o handle simples fechá-lo-á, e depois o próprio destrutor do objeto wrapper fechá-lo-á novamente quando o objeto for libertado. O handle é libertado duas vezes, o que constitui comportamento indefinido e um provável crash
A auditoria encontrou esta situação num caminho de importação do tipo imposição que constrói um TPdf em torno de um handle já aberto. A correção consiste em fazer da transferência de propriedade a única fonte de verdade. Assim que o handle é atribuído ao campo do wrapper, o wrapper passa a ser o proprietário, e a única limpeza no caminho de erro é libertar o wrapper. O destrutor do wrapper chama FPDF_CloseDocument por si, pelo que um segundo fecho explícito libertaria em duplicado o mesmo documento. O manipulador de erro corrigido liberta o objeto e relança a exceção, existindo exatamente um caminho até ao fecho
Result := TPdf.Create(nil);
try
Result.FDocument := NewDoc; // Result passa a ser dono do handle
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free fecha o handle. Um segundo FPDF_CloseDocument(NewDoc)
// aqui faria a libertação dupla do mesmo documento PDFium.
Result.Free;
raise;
end;
Os registos geridos e uma biblioteca cheia de exports precisam ambos de desmontagem explícita
A última classe diz respeito a memória que o compilador gere em seu nome, e que um hábito de C corrompe silenciosamente. Muitas das funções auxiliares deste binding retornam um registo (record) que contém uma WideString ou um array dinâmico. Estes são campos com contagem de referências, e o compilador emite contabilidade oculta para manter as suas contagens. O instinto trazido do C é limpar um registo novo com FillChar(Result, SizeOf(Result), 0). Isso carimba zeros sobre a referência gerida dentro do registo sem a decrementar primeiro. O compilador reutiliza uma única variável temporária oculta para o resultado de uma função ao longo das iterações do ciclo, pelo que na segunda iteração o FillChar sobrescreve um ponteiro de string ativo que nunca foi libertado, e a string para a qual ele apontava fica perdida (leak). Chame a função num ciclo sobre mil anotações e ficará com mil strings perdidas
A correção consiste em deixar a linguagem limpar o registo da forma que conhece, com Default(T), que liberta qualquer campo gerido antes de o pôr a zeros
// Default() em vez de FillChar: o compilador reutiliza um único temporário
// oculto para o resultado da função ao longo das iterações do ciclo, por
// isso FillChar poria a zeros ponteiros WideString ativos sem os libertar.
Result := Default(TPdfAnnotation);
Um problema de propriedade relacionado reside na fronteira do carregamento da biblioteca. Este binding resolve várias centenas de ponteiros de funções a partir da DLL do PDFium com GetProcAddress depois de um LoadLibrary. Se faltar um export obrigatório, o estado parcialmente vinculado é perigoso: dezenas de ponteiros são válidos, os restantes são nil ou desatualizados, e qualquer chamada posterior através de um deles salta para um módulo que pode já estar descarregado. O binding trata disto descarregando a biblioteca e executando um ClearAllBindings completo que repõe a nil cada ponteiro importado sempre que a resolução de um export obrigatório falha. Depois disso, nenhum ponteiro de função fica pendente para um módulo descarregado, e uma chamada posterior falha de forma limpa com uma verificação de ponteiro nulo (nil-pointer) em vez de se desviar para código libertado
O wrapper é onde quatro contratos são reafirmados manualmente
Nenhum destes cinco defeitos é exótico. São os modos de falha previsíveis de uma fina camada Pascal sobre uma API C, e agrupam-se porque essa camada é exatamente onde quatro contratos distintos têm de ser declarados de novo. A convenção de chamada tem de ser explicitada como cdecl em cada callback. A largura do inteiro tem de corresponder a size_t no único alvo onde ela realmente alarga. O modelo de exceções tem de ser convertido em códigos de retorno em cada callback que sai do Pascal. A propriedade de cada handle e de cada campo gerido tem de ser definida uma vez e respeitada em todos os caminhos, incluindo os caminhos de erro que ninguém exercita até à produção. Falhe em qualquer um deles e obterá um defeito cujo sintoma surge longe da sua causa, o que torna esta categoria dispendiosa. O valor da auditoria residiu menos em qualquer correção individual do que em tratar cada uma destas como a sua própria disciplina a verificar em todo o binding
Se quiser ver o binding a fazer trabalho real em vez de apenas guardar as suas arestas, as técnicas de cache de renderização e zoom na nossa nota sobre desempenho de cache de renderização e zoom mostram o caminho de renderização, e o passo a passo entre compiladores em construir um visualizador Lazarus e FPC é o sítio onde o comportamento do size_t em Win64 aqui descrito realmente importa. Ambos assentam no mesmo trabalho de segurança de memória e ABI que é entregue no PDFium Component para Delphi, Lazarus e C++Builder, a par das APIs de renderização, extração de texto e formulários abordadas noutros artigos deste blog