Artigo Técnico

Vinculando jbig2enc estaticamente ao Free Pascal sem DLL

O PDFlibPas 3.538.0 vincula estaticamente o encoder JBIG2 externo a programas Free Pascal e Lazarus. O projeto adiciona a unit PDFlibJBIG2EncC, a mesma unit que Delphi e C++Builder já usam, e o encoder acaba dentro do executável, sem nenhum arquivo extra para distribuir ao lado dele. Isso muda a conclusão anterior sobre este recurso, segundo a qual o Free Pascal só conseguiria acessar o encoder externo por meio de uma DLL

Por que a DLL parecia ser a única opção?

A DLL parecia ser a única opção porque três caminhos de linkedição falhavam de três maneiras sem relação entre si, e nenhuma opção do compilador alcançava qualquer um desses problemas. O linker interno rejeita diretamente seções COMDAT associativas. Um link externo pelas binutils incluídas trava durante a coleta de seções, uma etapa que o Free Pascal passa sem condição no alvo Windows de 64 bits. Uma binutils mais nova nem consegue processar o script de link do Free Pascal. Recompilar o lado C++ com a outra toolchain apenas troca uma recusa por outra, porque instanciações de templates e de funções inline geram símbolos externos fracos por construção, e o Free Pascal os reporta como Unsupported COFF symbol type 105. Nenhuma dessas evidências estava errada, e a explicação anterior sobre os backends do encoder JBIG2 e o linker do Free Pascal percorre cada beco sem saída de uma forma que ainda pode ser reproduzida hoje. O erro estava na suposição sobre onde a correção poderia existir. Toda tentativa passava pelo compilador ou pelo linker, e nenhum dos dois consegue alterar o que um arquivo de objeto já contém. O arquivo de objeto sempre foi o problema. O ObjConv lê COFF e grava COFF, e cada construção que faz o Free Pascal travar tem um equivalente mecânico que ele aceita

O erro que nunca informa a causa

O linker interno do Free Pascal implementa COMDAT pick-any apenas pela metade, e essa implementação incompleta é o ponto mais difícil de diagnosticar aqui. Ele realmente dobra definições duplicadas, como o formato determina. Mas TExeOutput.RemoveUnreferencedSections redireciona por meio de exesymbol para a definição vencedora ao marcar seções como usadas, enquanto TCoffexeoutput.DoRelocationFixup lê diretamente objreloc.symbol.objsection. Quando uma seção usada referencia um símbolo que o próprio objeto define em uma cópia que perdeu a dobra, as duas passagens estão olhando para seções diferentes, e o link para em Internal error 200603061

Compare isso com os dois limites ao redor dele. Unsupported COFF symbol type 105 indica um externo fraco. Associative or exact match COMDAT sections are not yet supported indica COMDAT associativa e ainda nomeia o símbolo problemático. O erro interno 200603061 não diz nada: nenhum nome de símbolo, nome de seção, nome de arquivo ou informação sobre a etapa. Ele também é o caso normal, não uma situação de borda, porque o MSVC coloca cada literal de texto e cada instanciação inline ou de template em uma COMDAT pick-any, e nos 186 objetos deste conjunto do encoder o linker executou 2656 dobras. Compilar com /Gy- mantém as funções comuns fora das seções COMDAT por função, mas deixa literais de texto e instanciações de templates exatamente onde estavam

Por que substituir símbolos da CRT sempre parece fazer o último quebrar o build?

Porque o linker só chega à etapa de fixups depois que todos os símbolos foram resolvidos. Enquanto ainda falta qualquer símbolo, a execução termina cedo com Undefined symbol, e o problema de COMDAT nem tem chance de aparecer. Ao preencher o último stub do runtime C, o linker avança uma etapa e cai direto no erro interno 200603061. O sintoma observado é, portanto, sistematicamente enganoso: ao adicionar os corpos Pascal para os símbolos C referenciados um por vez, sempre parece que a adição mais recente quebrou o build ou que algum limite perto de cem stubs foi ultrapassado. Nada disso é verdade. Qual símbolo entrou por último e quantos entraram no total 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 reclamação depois que você corrige algo sem relação, pergunte primeiro se você avançou uma etapa em vez de ter causado uma regressão

A correção é uma passagem pelo ObjConv, não uma flag 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 dessas opções foram adicionadas para este trabalho. -xw resolve símbolos IMAGE_SYM_CLASS_WEAK_EXTERNAL em externos comuns. -xn normaliza símbolos IMAGE_SYM_CLASS_NULL, como _fltused, que o Free Pascal reporta como Unsupported COFF symbol type 0. -xc faz o trabalho principal: rebaixa cada seção COMDAT a uma seção comum e torna estáticos os símbolos que ela define. Isso remove a falha ao remover a decisão: sem seções COMDAT não há dobra, nem uma cópia vencedora para uma passagem redirecionar e outra não encontrar, e as seções associativas de unwind .pdata e .xdata também desaparecem. O custo existe, mas é pequeno, pois as cópias que poderiam ser mescladas legitimamente agora sobrevivem individualmente

A renomeação do prefixo -np:__imp_:pdflibimp_ resolve uma colisão separada. O MSVC chama APIs Win32 importadas por meio de células de indireção chamadas __imp_*, o Free Pascal reserva esse prefixo para seu próprio mecanismo de importação, e definir diretamente um desses nomes dispara o mesmo erro interno 200603061. Renomear as células permite que o lado Pascal as publique como variáveis comuns e as preencha em runtime. Os próprios objetos são compilados com as flags de link estático /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c- e com os codecs de imagem desativados, de modo que os caminhos mortos de E/S de arquivo e codecs precisam de muito menos stubs usados apenas no link. Eles ficam em Lib\thirdparty\Win64f, enquanto o caminho de Delphi e C++Builder continua vinculando seu próprio conjunto Win64x sem alterações, que é o resultado correto para uma correção de portabilidade restrita a uma toolchain

O que o lado Pascal ainda precisa 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, portanto toda rotina Pascal que substitui um entry point C traz uma cláusula public name explícita. Delphi usa o nome da rotina como nome do símbolo e não precisa de cláusula alguma, por isso uma única unit atende aos dois compiladores com as cláusulas sob {$IFDEF FPC}. A armadilha é que uma declaração external 'msvcrt.dll' não satisfaz nada: ela cria uma importação, mas nunca uma definição à qual um objeto vinculado possa se conectar. O corpo de encaminhamento precisa existir

// Uma declaracao externa cria apenas uma importacao. Nenhum objeto vinculado pode
// se associar a ela.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// Um corpo Pascal publicado sob o nome exato do simbolo C e o que o conjunto
// de objetos realmente usa para fazer a ligacao.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

Entry points variádicos quebram esse padrão, porque um wrapper Pascal não consegue encaminhar seus próprios varargs para outro callee variádico. A saída é deixar de ser um wrapper: exporte uma rotina naked sob o nome C e faça um salto final para a implementação real, com os registradores de argumentos e a pilha exatamente como o chamador os organizou. A camada JPEG 2000 já trata snprintf e vsnprintf dessa forma, saltando para as grafias do msvcrt com prefixo de sublinhado porque os nomes simples só são exportados pelo UCRT. Outra restrição relacionada vem do mesmo erro interno: as células de importação renomeadas são preenchidas em uma seção initialization por meio de GetModuleHandleA e GetProcAddress, em vez de inicializadores estáticos, pois obter o endereço de uma rotina importada dentro de um inicializador faz o compilador emitir um fixup que ele 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;

// Varargs nao podem ser encaminhados por um wrapper Pascal, portanto o simbolo
// exportado salta ate o fim, deixando o frame como o chamador o preparou.
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

O que um projeto Free Pascal faz de diferente agora?

Nada além do nome da unit na cláusula uses, e não há mais arquivo para distribuir. O backend se registra na própria seção de initialization por meio de RegisterJBIG2EncoderBackend, e os chamadores o solicitam exatamente como antes: pelo bit de opções PDF_JBIG2_OPTION_EXTERNAL_ENCODER, cujo valor é 4, ou pelo argumento UseExternalEncoder dos entry points estendidos. Solicitá-lo continua sendo uma preferência, não uma garantia, pois um build que deixe a unit de fora recua silenciosamente para o encoder Pascal nativo MMR e produz arquivos maiores em vez de um erro

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder e, a partir da 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;

Há dois limites que vale declarar claramente. Existe apenas um conjunto de objetos Win64, portanto em qualquer outro alvo do Free Pascal o entry point de codificação externa reporta falha e o encoder Pascal nativo assume o trabalho. E a regressão que controla tudo isso é uma comparação de renderização, não uma verificação de tamanho: os dois encoders são sem perdas para a mesma fonte, então as saídas são renderizadas e comparadas byte a byte, com a suíte Lazarus passando nos 26 testes, inclusive esse. Comparar os tamanhos dos streams comprimidos não provaria nada, porque uma página invertida comprime para aproximadamente o mesmo tamanho que uma página correta

A lição mais ampla vai além do JBIG2. Uma DLL tem o formato certo quando a fronteira é realmente dinâmica, que é o caso atendido pelas interfaces de integração DLL, ActiveX e dylib; ela tem o formato errado quando é apenas um contorno para um leitor de COFF, porque adiciona um arquivo a cada instalador, um caminho de busca a cada distribuição e uma falha por divergência de versão que o link estático não teria. O que vem antes também importa, pois a forma como a imagem de dois níveis é produzida decide mais sobre o tamanho final do que o encoder, e a renderização monocromática baseada em regiões no Delphi cobre essa metade do pipeline. A cobertura das toolchains, os conjuntos de objetos por compilador e os alvos compatíveis estão listados na página do produto losLab PDF Developer Library