Um binding Pascal sobre uma biblioteca C parece Pascal comum. Você chama um método, recebe um record de volta, libera o que alocou. O problema é que o PDFium é uma biblioteca C e C++ com convenção de chamada própria, larguras de inteiro próprias e regras próprias sobre quem é dono da memória e quem a libera. Nada disso atravessa a fronteira da linguagem por conta própria. Cada um desses contratos precisa ser reafirmado à mão nas declarações Pascal, e uma única palavra errada transforma uma chamada de aparência limpa em corrupção de stack, um offset truncado ou um double free. Cada contrato abaixo vem acompanhado do jeito clássico de errar e do formato que o binding do PDFium Component usa no lugar. Vale percorrê-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
O motivo pelo qual esse erro é difícil de encontrar é que o dano não é local. A chamada corrompida retorna e parece certa. O desalinhamento aparece mais tarde, em alguma função sem relação alguma cujo frame agora está apoiado em um ponteiro de stack deslocado em alguns bytes, e se manifesta como uma leitura descontrolada, um endereço de retorno inválido ou um crash com um backtrace que aponta para longe do callback que você realmente errou. O form-fill é o lugar clássico onde isso morde, porque a interface de form-fill é um record cheio de callbacks que o PDFium chama de volta. Um deles, FFI_OpenFile, entrega ao PDFium uma função que ele chamará para abrir um arquivo externo, declarada como function(pThis: PFPDF_FORMFILLINFO; fileFlag: Integer; wsURL: FPDF_WIDESTRING; mode: PAnsiChar): PFPDF_FILEHANDLER; cdecl. O cdecl final é o detalhe que vale copiar. Sem ele o código continua compilando, linkando e rodando, até o 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 sintático opcional, e o compilador não avisa quando ela 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 toda assinatura importada e de todo callback que você passa para fora
size_t tem a largura de um ponteiro, e em FPC Win64 isso significa 64 bits
O segundo perigo é uma incompatibilidade de largura de inteiro que só aparece em um alvo. O size_t do C é definido para ser largo o bastante para guardar qualquer tamanho de objeto, o que em uma plataforma de 64 bits significa um inteiro sem sinal de 64 bits. As interfaces de carregamento progressivo do PDFium falam em offsets de bytes size_t. O record FX_FILEAVAIL do provedor de disponibilidade carrega um callback IsDataAvail que o PDFium chama com um offset e um tamanho, e o callback AddSegment do record FX_DOWNLOADHINTS recebe o mesmo. Os dois 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 você declarar esses offsets como um tipo de 32 bits, o binding funciona em Win32 e em Delphi Win64, e depois quebra silenciosamente em FPC e Lazarus Win64. A causa é sutil. No FPC Win64, NativeUInt é um tipo genuíno de 64 bits com a largura de um ponteiro, e size_t é um alias dele. O binding tem um comentário na seção de tipos alertando justamente contra fazer shadowing de NativeUInt no FPC, porque redefini-lo ali como um alias de 32 bits forçaria o size_t a 32 bits e corromperia todo parâmetro size_t passado para a biblioteca ou escrito por ela. Um offset de 64 bits que chega a um parâmetro de 32 bits perde sua metade superior. Em um arquivo pequeno, todo offset cabe em 32 bits e nada dá errado. Em um arquivo grande, no momento em que um offset cruza a linha dos quatro gigabytes, o valor truncado aponta para um lugar completamente diferente, o PDFium pergunta se a faixa de bytes errada está disponível, e o carregamento progressivo trava ou lê lixo. O erro fica invisível até que o arquivo seja grande o bastante e o alvo seja aquele em que o size_t realmente se 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 pode se desenrolar através deste frame cdecl/C++, então capture-a e reporte
// 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 controle regressa ao lado Pascal da fronteira
A libertação dupla esconde-se no caminho de erro
O quarto perigo é a posse (ownership). Um handle de documento PDFium é aberto pela biblioteca e precisa ser fechado exatamente uma vez, por FPDF_CloseDocument. O perigo é um caminho de erro que libera um handle que uma segunda limpeza também considera seu. Imagine uma rotina que cria um objeto wrapper, atribui a ele um handle de documento recém-aberto e depois faz mais configuração que pode falhar. Se a configuração lançar uma exceção, um handler de retorno antecipado que chama FPDF_CloseDocument no handle bruto o fechará, e então o próprio destrutor do objeto wrapper o fechará de novo quando o objeto for liberado. O handle é liberado duas vezes, o que é comportamento indefinido e um crash provável
O padrão importa mais no caminho de importação estilo imposição que constrói um TPdf em torno de um handle já aberto. O remédio é fazer da transferência de posse a única fonte de verdade. Assim que o handle é atribuído ao campo do wrapper, o wrapper é o dono, e a única limpeza no caminho de erro é liberar o wrapper. O destrutor do wrapper chama FPDF_CloseDocument por você, então um segundo fechamento explícito liberaria em duplicata o mesmo documento. O handler de erro abaixo libera o objeto e relança, e existe exatamente um caminho até o fechamento
Result := TPdf.Create(nil);
try
Result.FDocument := NewDoc; // Result agora possui o handle
Result.InitializeFormFill;
Result.ReloadPage;
except
// Result.Free fecha o handle. Um segundo FPDF_CloseDocument(NewDoc)
// aqui liberaria em duplicado o mesmo documento PDFium.
Result.Free;
raise;
end;
Os registros 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 registro (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 registro novo com FillChar(Result, SizeOf(Result), 0). Isso carimba zeros sobre a referência gerida dentro do registro 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 registro 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 uma temp oculta para
// o resultado da função entre iterações do laço, então o FillChar zeraria
// ponteiros de WideString ativos sem liberá-los.
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 desses cinco perigos é exótico. São os modos de falha previsíveis de uma camada Pascal fina sobre uma API C, e eles se agrupam porque essa camada é exatamente onde quatro contratos separados precisam ser redeclarados. A convenção de chamada precisa estar grafada como cdecl em todo callback. A largura do inteiro precisa corresponder ao size_t no único alvo em que ele realmente se alarga. O modelo de exceções precisa ser convertido em códigos de retorno em todo callback que sai do Pascal. A posse de todo handle e de todo campo gerenciado precisa ser declarada uma vez e respeitada em todos os caminhos, incluindo os caminhos de erro que ninguém exercita até a produção. Errar em qualquer um deles gera uma falha cujo sintoma aparece longe da causa, e é isso que torna essa categoria cara. O valor está menos em qualquer regra individual do que em tratar cada um desses pontos como uma disciplina própria 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