Eis um problema que surge no instante em que uma biblioteca PDF sai da sua linguagem nativa. Tem um binding que funciona perfeitamente a partir de C# em Windows. Precisa das mesmas chamadas a partir de Python em macOS, por isso copia o ficheiro de declarações do Windows, altera o nome do binário e executa-o. Todos os símbolos são resolvidos. A primeira chamada devolve lixo, a segunda falha com uma violação de acesso (access violation) e nada do seu código PDF foi alterado. A falha está um nível abaixo do PDF: as exportações em Windows utilizam a convenção Stdcall, enquanto o dylib em macOS exporta as mesmas funções como Cdecl com um sublinhado (underscore) inicial. Qualquer declaração de função externa que falhe num destes detalhes corrompe a pilha (stack) antes de ser aberto um único documento.
Toda essa classe de falhas provém de uma decisão de design que convém compreender logo à partida. O PDFlibPas, o motor PDF com código-fonte disponível da losLab para Delphi e C++Builder, encapsula todo o seu modelo de objetos numa única classe de fachada plana, TPDFlib, disponibilizando essa fachada em três formatos binários: uma DLL Windows com cerca de 1.250 funções exportadas, um objeto de automação COM/ActiveX e um dylib macOS. A semântica do PDF é idêntica nos três casos. O aspeto crítico reside na ABI subjacente: convenções de chamada, codificações de strings, propriedade de handles e qual das partes tem permissão para libertar cada buffer.
Uma fachada, três formatos binários
Cada função pública da TPDFlib tem uma contraparte plana com o prefixo DL seguido do nome do método. LoadFromFile passa a DLLoadFromFile, Encrypt a DLEncrypt, NewSignProcessFromFile a DLNewSignProcessFromFile. O primeiro parâmetro de quase todas as exportações é um InstanceID devolvido por DLCreateLibrary, representando a referência do objeto que, de outro modo, seria mantida por um chamador Delphi. Compreenda este mapeamento desde cedo. Significa que a referência da API do Delphi serve também como documentação para qualquer outra linguagem: tudo o que a classe faz, a DLL faz sob um nome previsível, bastando ler a assinatura do método Pascal para saber qual a chamada necessária em Python ou C#.
A compilação para Windows produz os ficheiros PDFlibDLL32.dll e PDFlibDLL64.dll; escolha o que corresponder à arquitetura do seu processo anfitrião, uma vez que um processo Java ou .NET de 64 bits não conseguirá carregar a biblioteca de 32 bits, independentemente da declaração utilizada.
Windows: Instâncias Stdcall e os pares de funções W/A
Cada exportação que aceita strings existe em duas versões. Uma versão wide aceita PWideChar (UTF-16, o ajuste natural para .NET, Java e o c_wchar_p do Python), e uma versão com o sufixo A aceita PAnsiChar. Ambas têm a mesma semântica e diferem apenas na codificação, o que torna a sua mistura difícil de diagnosticar: não são lançadas exceções, não são devolvidos códigos de erro, obtém simplesmente caracteres corrompidos (mojibake) nos metadados ou um erro espúrio de "ficheiro não encontrado" em caminhos com caracteres fora do ASCII básico. O primeiro bug de codificação com que uma equipa depara desta forma costuma custar uma tarde de trabalho, porque o sintoma aponta para os dados quando a causa está na declaração.
// Windows binding (PDFlibDLL64.dll): Stdcall, plain export names
function DLCreateLibrary: Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLCreateLibrary';
function DLReleaseLibrary(InstanceID: Integer): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLReleaseLibrary';
function DLLoadFromFile(InstanceID: Integer;
FileName, Password: PWideChar): Integer; stdcall;
external 'PDFlibDLL64.dll' name 'DLLoadFromFile';
// macOS binding: same function, Cdecl, and an underscore prefix on the export
function DLCreateLibrary: Integer; cdecl;
external 'PDFlibDylib.dylib' name '_DLCreateLibrary';
Escolha uma largura de caracteres por sistema anfitrião e codifique-a no gerador de ligações. Uma regra prática: se a linguagem anfitriã tiver strings UTF-16 nativas, efetue a ligação às versões W em todo o lado e nunca mais utilize a família A.
macOS: os mesmos nomes, ABI diferente
O dylib exporta o mesmo conjunto de funções DL com duas alterações sistemáticas. A convenção de chamada é Cdecl em vez de Stdcall, e cada nome exportado inclui um sublinhado inicial (_DLCreateLibrary, _DLLoadFromFile, etc.). Ambas as alterações são puramente mecânicas, o que as torna ideais para uma ligação gerada automaticamente e perigosas para uma cópia editada manualmente a partir do ficheiro Windows. Mantenha uma lista de funções canónica e gere declarações específicas por plataforma se as suas ferramentas o permitirem. Se não o fizer, sofrerá a corrupção de pilha descrita no início desta página, que se manifestará apenas na plataforma que a sua integração contínua menos testar.
Anfitriões COM e ActiveX: Processamento com Safecall e Olevariant
Para anfitriões de automação herdados, VB.NET, C# e VBScript, a compilação OCX encapsula a mesma fachada num objeto de automação IDispatch, IPDFlibrary, com todos os métodos declarados como Safecall. Esta convenção altera a forma como os erros chegam até si. O Safecall traduz uma falha interna num HRESULT de COM, permitindo a um chamador C# capturar uma exceção onde a DLL plana teria devolvido apenas um inteiro silencioso que o programador teria de se lembrar de verificar. A mesma operação, dois modos de tratamento de falhas, dependendo do binário carregado.
Os dados binários seguem uma segunda regra específica de COM. A interface de automação não possui qualquer parâmetro do tipo ponteiro. Qualquer dado binário (bytes de imagem de entrada ou bytes de PDF de saída) cruza a fronteira como um Olevariant através de métodos como AddImageFromVariant e AppendToVariant. Fazer o marshaling de um byte array para um variant requer apenas uma linha em .NET. Se tentar passar um ponteiro bruto, argumentando ser o mesmo processo, a camada de despacho rejeitará ou corromperá a chamada. Um detalhe adicional de registo pode dificultar as implementações: o registo COM depende da arquitetura (bitness), pelo que um OCX registado com o regsvr32 de 32 bits fica invisível para um anfitrião de 64 bits. Esta incompatibilidade manifesta-se como o clássico erro de "classe não registada" na máquina do cliente, muito depois de o software ter saído da sua.
Disciplina de handles: as instâncias possuem os documentos
A API plana funciona com base em handles inteiros. O método DLCreateLibrary devolve uma instância. O carregamento de um ficheiro devolve um ID de documento dentro dessa instância. Os processos de assinatura, as listas de strings e os ficheiros de acesso direto devolvem todos os seus próprios handles inteiros, todos confinados à mesma instância. O ciclo de vida tem o mesmo aspeto a partir de qualquer anfitrião FFI, apresentado aqui em Pascal pela clareza de leitura:
var
Inst, Doc: Integer;
begin
Inst := DLCreateLibrary; // one instance per worker thread
try
Doc := DLLoadFromFile(Inst, 'in.pdf', ''); // returns a DocumentID, 0 on failure
if Doc <> 0 then
begin
DLEncrypt(Inst, 'owner-secret', 'user-secret', 3,
DLEncodePermissions(Inst, 1, 0, 0, 0, 0, 0, 0, 1));
DLSaveToFile(Inst, 'out.pdf');
end;
finally
DLReleaseLibrary(Inst); // frees every document the instance owns
end;
end;
Duas conclusões decorrem desta árvore de propriedade. O método DLReleaseLibrary é a única chamada de limpeza estritamente necessária, uma vez que elimina todos os handles de documentos e processos sob a instância numa única operação. Num script curto, isto é suficiente. Num serviço de execução contínua, transforma-se numa fuga de memória lenta acompanhada de burocracia extra; por isso, liberte os documentos à medida que termina a sua utilização, em vez de os deixar acumular até que a instância termine. A instância é também a unidade natural de isolamento de threads. Atribua a cada thread de processamento o seu próprio InstanceID e nunca partilhe um entre threads sem bloqueio externo, pelo mesmo motivo que nunca partilharia um único objeto TPDFlib entre threads.
As strings devolvidas são emprestadas, não possuídas
As funções que devolvem texto, como DLGetPageText, entregam um PWideChar ou PAnsiChar que aponta para um buffer detido e reciclado pela instância da biblioteca. O contrato estabelecido é: copiar imediatamente, nunca libertar.
var
P: PWideChar;
PageText: string;
begin
P := DLGetPageText(Inst, 7); // pointer into a library-owned buffer
PageText := P; // copy now; a later call may reuse the buffer
end;
Em C#, isto implica fazer o marshaling do IntPtr para uma string gerida antes da chamada de biblioteca seguinte. Em Python ctypes, significa extrair a string da memória do ponteiro de imediato. Se mantiver o ponteiro bruto ativo entre chamadas, criará um erro que passa em todos os testes unitários e falha quando dois pedidos se sobrepõem em produção, pois a segunda chamada recicla o buffer que a primeira ainda estava a ler. A mesma regra de propriedade aplica-se no sentido inverso para callbacks registados através de DLSetProgressCallback. Qualquer ponteiro que a biblioteca passe para o seu callback é válido apenas durante a execução desse callback, e o próprio objeto de callback tem de permanecer ativo (afixado na memória, num anfitrião com garbage collector) enquanto a instância o puder invocar. Um delegado recolhido a meio de uma tarefa é o exemplo típico do erro de violação de acesso "aleatório" que surge numa ligação .NET que funcionou sem falhas durante meses.
Desenhe um teste rápido (smoke test) na própria ligação e execute-o antes do lançamento de qualquer conjunto de declarações geradas. Teste uma chamada de cada categoria propensa a revelar falhas de ABI: uma função sem parâmetros como DLCreateLibrary para confirmar que a convenção está correta, uma função de entrada de string com caracteres não-ASCII para validar a codificação do caminho, uma função de saída de string para atestar o tratamento do buffer emprestado e uma operação que falhe de propósito para observar como o erro chega ao anfitrião. São quinze minutos de trabalho que evitam a ocorrência de falhas de convenção de chamada e codificação que, de outro modo, chegariam meses depois como um relatório de erro do cliente.
O caso concreto do Python ctypes
O Python ctypes é a ligação que vejo ser desenvolvida manualmente com mais frequência, sendo ideal para demonstrar a separação multiplataforma. No Windows, carregue a biblioteca com ctypes.WinDLL para que o ctypes aplique Stdcall, associe as funções W sem sufixo e declare cada parâmetro de string como c_wchar_p. No macOS, carregue com ctypes.CDLL para Cdecl, mantenha a lista de funções idêntica e resolva os nomes sem o sublinhado inicial. A maioria das camadas FFI, incluindo o ctypes, simplifica a convenção de sublinhado no macOS, mas esta é a hipótese a validar com uma única chamada bem-sucedida antes de criar centenas de declarações com base nela.
Duas questões de implementação decorrem da ligação e têm respostas claras. A DLL comum não necessita de registo: o utilitário regsvr32 aplica-se apenas à versão ActiveX; a DLL é distribuída por cópia de ficheiro, o que constitui o principal motivo para a preferir em serviços e contentores Windows onde se evite alterar o registo do sistema. A segurança de execução paralela (thread safety) resume-se à regra já enunciada: uma instância por thread. O handle da instância armazena todo o estado mutável que o motor monitoriza, nomeadamente o documento selecionado, as opções de renderização e as definições de extração, pelo que duas threads a partilharem a mesma instância misturariam o estado uma da outra, mesmo que cada chamada individual indicasse sucesso.
Assim que a ligação estiver consolidada, as operações do outro lado são exatamente as que os artigos do Delphi cobrem em pormenor, incluindo a aplicação e auditoria de encriptação PDF e a extração de texto e imagens de documentos existentes.
As transferências dos binários para as três camadas de integração acompanham a biblioteca; consulte a página do produto PDFlibPas para edições e licenciamento.