PDFlibPas 3.538.0 liga estaticamente o codificador JBIG2 externo a programas Free Pascal e Lazarus. O projeto acrescenta a unidade PDFlibJBIG2EncC, a mesma unidade que Delphi e C++Builder já usam, e o codificador acaba dentro do executável, sem ser necessário distribuir qualquer ficheiro adicional ao lado dele. Isto altera a conclusão anterior sobre esta funcionalidade, segundo a qual o Free Pascal só conseguia aceder ao codificador externo através de uma DLL
Porque é que a DLL parecia ser a única opção?
A DLL parecia ser a única opção porque três percursos de ligação falhavam de três formas independentes e nenhuma opção do compilador chegava a qualquer um desses problemas. O linker interno rejeita liminarmente secções COMDAT associativas. Uma ligação externa através dos binutils incluídos termina numa falha dentro da recolha de secções não utilizadas, que o Free Pascal passa incondicionalmente no alvo Windows de 64 bits. Uma versão mais recente dos binutils não consegue processar de todo o script do linker do Free Pascal. Reconstruir o lado C++ com o outro toolchain troca uma recusa por outra, porque a instanciação de templates e de funções inline emite, por construção, símbolos externos fracos e o Free Pascal comunica-os como Unsupported COFF symbol type 105. Nenhuma dessas evidências estava errada, e a explicação anterior dos backends do codificador JBIG2 e do linker do Free Pascal percorre cada beco sem saída de uma forma que ainda hoje é reproduzível. O erro estava na suposição sobre o local onde a correção podia ser feita. Cada tentativa passava pelo compilador ou pelo linker, mas nenhum dos dois pode alterar o conteúdo que um ficheiro objeto já tem. O problema esteve sempre no ficheiro objeto. ObjConv lê COFF e escreve COFF, e cada construção que faz o Free Pascal tropeçar tem uma equivalente mecânica que ele aceita
O erro que nunca indica a causa
O linker interno do Free Pascal implementa apenas parcialmente o pick-any COMDAT, e essa implementação incompleta é a parte mais difícil de diagnosticar aqui. Ele funde definições duplicadas, como o formato prevê. No entanto, TExeOutput.RemoveUnreferencedSections redireciona através de exesymbol para a definição vencedora quando marca as secções como utilizadas, enquanto TCoffexeoutput.DoRelocationFixup lê diretamente objreloc.symbol.objsection. Quando uma secção utilizada referencia um símbolo que o seu próprio objeto define numa cópia que perdeu a fusão, as duas passagens estão a olhar para secções diferentes e a ligação termina com Internal error 200603061
Compare isto com os dois limites de cada lado. Unsupported COFF symbol type 105 indica um símbolo externo fraco. Associative or exact match COMDAT sections are not yet supported indica uma secção COMDAT associativa e até identifica o símbolo problemático. Internal error 200603061 não diz absolutamente nada: não há nome do símbolo, nome da secção, nome do ficheiro nem informação sobre a fase. Também é o caso normal, não uma situação marginal, porque o MSVC coloca todos os literais de cadeia e todas as instanciações inline ou de templates num pick-any COMDAT, e nos 186 objetos deste conjunto de codificadores o linker fez 2656 fusões. Compilar com /Gy- mantém as funções normais fora das secções COMDAT por função, mas deixa os literais de cadeia e as instanciações de templates exatamente onde estavam
Porque é que substituir símbolos CRT por stubs parece sempre culpar o último?
Porque o linker só chega à passagem de fixup depois de resolver todos os símbolos. Enquanto faltar alguma coisa, a execução termina cedo com Undefined symbol e o problema do COMDAT nunca tem oportunidade de aparecer. Preencha o último stub do runtime C e o linker avança uma fase, diretamente para Internal error 200603061. O sintoma observado é, por isso, sistematicamente enganador: ao adicionar um a um os corpos Pascal para os símbolos C referenciados, parece sempre que a adição mais recente estragou o build ou que foi ultrapassado algum limiar perto de uma centena de stubs. Nenhuma das duas coisas é verdade. O símbolo que entrou por último e o número total de símbolos adicionados são irrelevantes, porque a falha estava latente desde o primeiro objeto e só se tornou alcançável quando a resolução teve sucesso. Quando um linker muda a mensagem depois de corrigir algo não relacionado, pergunte se não avançou simplesmente uma fase em vez de ter introduzido uma regressão
A correção é uma passagem do ObjConv, não uma opção do compilador
A correção completa é um único comando de pós-processamento executado sobre cada objeto compilado: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Três destas opções foram acrescentadas para este trabalho. -xw resolve símbolos IMAGE_SYM_CLASS_WEAK_EXTERNAL em símbolos externos normais. -xn normaliza símbolos IMAGE_SYM_CLASS_NULL como _fltused, que o Free Pascal comunica como Unsupported COFF symbol type 0. -xc faz o trabalho pesado: rebaixa cada secção COMDAT a uma secção normal e torna estáticos os símbolos que ela define. Isto remove a falha porque remove a própria decisão: sem secções COMDAT não há fusão, não há uma cópia vencedora para a qual uma passagem possa redirecionar e outra não encontrar, e as secções associativas de unwind .pdata e .xdata desaparecem com ela. O custo é real, mas pequeno: as cópias que podiam ser fundidas legitimamente passam agora a sobreviver todas separadamente
A renomeação do prefixo -np:__imp_:pdflibimp_ resolve uma colisão separada. O MSVC chama APIs Win32 importadas através de células de indireção chamadas __imp_*, o Free Pascal reserva esse prefixo para o seu próprio mecanismo de importação e definir diretamente um desses nomes volta a provocar o mesmo Internal error 200603061. Renomear as células permite ao lado Pascal publicá-las como variáveis normais e preenchê-las em tempo de execução. Os objetos são compilados com o conjunto de opções de ligação estática /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- e com os codecs de imagem desligados, pelo que os caminhos mortos de I/O de ficheiros e de codecs precisam de muito menos stubs exclusivamente para a ligação. Eles ficam em Lib\thirdparty\Win64f, enquanto o percurso de Delphi e C++Builder continua a ligar o seu próprio conjunto Win64x sem alterações, que é o resultado certo para uma correção de portabilidade confinada a um único toolchain
O que o lado Pascal ainda tem de exportar
O Free Pascal resolve a importação de um objeto C pelo nome do símbolo e precisa que esse nome seja escrito explicitamente, por isso cada rotina Pascal que substitui um ponto de entrada C inclui uma cláusula public name explícita. O Delphi usa o nome da rotina como nome do símbolo e não precisa de cláusula, razão pela qual uma unidade serve ambos os compiladores com as cláusulas sob {$IFDEF FPC}. A armadilha é que uma declaração external 'msvcrt.dll' não satisfaz nada: cria uma importação, nunca uma definição à qual um objeto ligado possa associar-se. O corpo de encaminhamento tem de existir
// Uma declaração external cria apenas uma importação. Nenhum objeto ligado pode
// associar-se-lhe
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// É o corpo Pascal publicado sob o nome exato do símbolo C que o conjunto de
// objetos usa efetivamente para fazer a associação
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
Os pontos de entrada variádicos quebram esse padrão, porque um wrapper Pascal não pode encaminhar os seus próprios varargs para outro callee varargs. A saída é deixar de ser um wrapper: exporte uma rotina nua sob o nome C e faça um salto final para a implementação real, com os registos de argumentos e a stack exatamente como o caller os preparou. A camada JPEG 2000 já trata snprintf e vsnprintf desta forma, saltando para as grafias do msvcrt com prefixo underscore, porque os nomes normais só são exportados pelo UCRT. Há outra restrição relacionada que vem do mesmo Internal error: as células de importação renomeadas são preenchidas numa secção initialization através de GetModuleHandleA e GetProcAddress, em vez de inicializadores estáticos, porque obter o endereço de uma rotina importada num inicializador faz o compilador emitir um fixup que não consegue tratar e falhar novamente com 200603061
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// Não é possível encaminhar varargs a partir de um wrapper Pascal, por isso o símbolo
// exportado faz um salto final com a frame exatamente como o caller a preparou
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
O que um projeto Free Pascal faz agora de forma diferente
Nada além do nome da unidade na cláusula uses, e deixou de existir um ficheiro para distribuir. O backend regista-se a partir da sua própria secção de initialization através de RegisterJBIG2EncoderBackend, e os chamadores pedem-no exatamente como antes: através do bit de opções PDF_JBIG2_OPTION_EXTERNAL_ENCODER, que tem o valor 4, ou através do argumento UseExternalEncoder dos pontos de entrada estendidos. Pedi-lo continua a ser uma preferência, não uma garantia, porque um build que exclua a unidade recua silenciosamente para o codificador MMR nativo do Pascal e produz ficheiros maiores em vez de um erro
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder e, a partir de 3.538.0, Free Pascal
var
Pdf: TPDFlib;
Scan: TStream;
ImageId: Integer;
begin
Pdf := TPDFlib.Create(nil);
try
Pdf.NewDocument;
Pdf.NewPage;
Scan := TFileStream.Create('scan-page-1.tif', fmOpenRead);
try
// Interpolate, SymbolExtract, UseExternalEncoder, SkipBlackDots,
// BlackDotSize, LossyLevel
ImageId := Pdf.AddImageJBIG2FromStreamEx(Scan, 0, 1, 1, 0, 0, 0);
finally
Scan.Free;
end;
if ImageId = 0 then
raise Exception.Create('JBIG2 encoding failed');
Pdf.SaveToFile('archive.pdf');
finally
Pdf.Free;
end;
end;
Vale a pena declarar claramente dois limites. Só existe um conjunto de objetos Win64, por isso em qualquer outro alvo Free Pascal o ponto de entrada de codificação externo comunica uma falha e o codificador Pascal nativo assume o trabalho. E a regressão que valida tudo isto é uma comparação de renderização, não uma verificação de tamanho: ambos os codificadores são sem perdas para a mesma origem, pelo que a saída é renderizada e comparada byte a byte, com a suite do Lazarus a passar os 26 testes, incluindo este. Comparar tamanhos de streams comprimidos não provaria nada, porque uma página invertida comprime aproximadamente para o mesmo tamanho que uma página correta
A lição mais ampla aplica-se para além do JBIG2. Uma DLL é a forma certa quando a fronteira é realmente dinâmica, que é o caso servido pelas interfaces de integração de DLL, ActiveX e dylib; é a forma errada quando é apenas um contorno para um leitor COFF, porque acrescenta um ficheiro a cada instalador, um caminho de pesquisa a cada implementação e um modo de falha por divergência de versões que a ligação estática não pode ter. A etapa anterior também importa, pois a forma como a imagem binária é produzida decide mais sobre o tamanho final do que o próprio codificador, e o rendering monocromático baseado em regiões no Delphi cobre essa metade do pipeline. A cobertura dos toolchains, os conjuntos de objetos por compilador e os alvos suportados estão listados na página do produto losLab PDF Developer Library