Artículo técnico

Enlazar jbig2enc en Free Pascal sin una DLL

PDFlibPas 3.538.0 enlaza estáticamente el encoder JBIG2 externo en programas de Free Pascal y Lazarus. Un proyecto añade la unidad PDFlibJBIG2EncC, la misma unidad que ya utilizan Delphi y C++Builder, y el encoder queda dentro del ejecutable sin nada adicional que distribuir junto a él. Esto cambia la conclusión anterior sobre esta función, según la cual Free Pascal solo podía acceder al encoder externo mediante una DLL

¿Por qué parecía que la DLL era la única opción?

La DLL parecía la única opción porque tres rutas de enlazado fallaban de tres formas independientes y ningún conmutador del compilador llegaba a ninguna de ellas. El enlazador interno rechaza directamente las secciones COMDAT asociativas. Un enlazado externo mediante los binutils incluidos se bloquea dentro de la recolección de secciones, que Free Pascal activa incondicionalmente en el destino Windows de 64 bits. Unos binutils más recientes no pueden procesar el script de enlazado de Free Pascal. Reconstruir el lado C++ con la otra toolchain cambia una negativa por otra, porque la instanciación de plantillas e inline emite símbolos externos débiles por construcción y Free Pascal los informa como Unsupported COFF symbol type 105. Ninguna de esas evidencias era incorrecta, y el análisis anterior de los backends del encoder JBIG2 y del enlazador de Free Pascal recorre cada callejón sin salida de una forma que todavía se reproduce hoy. Lo que sí era incorrecto era suponer dónde podía vivir la solución. Cada intento pasaba por un compilador o un enlazador, y ninguno puede cambiar lo que ya contiene un fichero de objetos. El fichero de objetos era el problema desde el principio. ObjConv lee COFF y escribe COFF, y cada construcción que bloquea a Free Pascal tiene un equivalente mecánico que acepta

El error que nunca nombra su causa

El enlazador interno de Free Pascal implementa pick-any COMDAT solo a medias, y esa implementación parcial es lo más difícil de diagnosticar aquí. Sí combina definiciones duplicadas, como exige el formato. Pero TExeOutput.RemoveUnreferencedSections redirige a través de exesymbol hacia la definición ganadora cuando marca secciones como usadas, mientras que TCoffexeoutput.DoRelocationFixup lee directamente objreloc.symbol.objsection. Cuando una sección usada referencia un símbolo que su propio objeto define en una copia que perdió la combinación, las dos pasadas miran secciones distintas y el enlazado se detiene con Internal error 200603061

Compárelo con los dos límites que tiene a cada lado. Unsupported COFF symbol type 105 indica un externo débil. Associative or exact match COMDAT sections are not yet supported indica un COMDAT asociativo y hasta nombra el símbolo conflictivo. Internal error 200603061 no dice absolutamente nada: ni nombre de símbolo, ni nombre de sección, ni nombre de fichero, ni fase. Además es el caso normal, no una rareza, porque MSVC coloca cada literal de cadena y cada instanciación inline o de plantilla en un COMDAT pick-any, y en los 186 objetos de este conjunto de encoder el enlazador realizó 2656 combinaciones. Compilar con /Gy- mantiene las funciones normales fuera de las secciones COMDAT por función, pero deja exactamente donde estaban los literales de cadena y las instanciaciones de plantillas

¿Por qué los stubs de símbolos CRT siempre parecen ser los que lo rompen?

Porque el enlazador solo llega a la fase de corrección de relocalizaciones después de resolver todos los símbolos. Mientras falte algo, la ejecución termina antes con Undefined symbol y el problema de COMDAT no llega a manifestarse. Complete el último stub de C runtime y el enlazador avanza una fase, directamente hasta Internal error 200603061. Por eso el síntoma de campo resulta sistemáticamente engañoso: al añadir uno a uno los cuerpos Pascal para los símbolos C referenciados, siempre parece que la incorporación más reciente ha roto la compilación, o que se ha cruzado algún umbral alrededor de un centenar de stubs. No es cierto. Qué símbolo se añadió el último y cuántos se añadieron en total son datos irrelevantes, porque el fallo estaba latente desde el primer objeto y solo se volvió alcanzable cuando la resolución terminó correctamente. Cuando un enlazador cambia su queja después de que arregle algo no relacionado, pregúntese si ha avanzado de fase en lugar de provocar una regresión

La solución es una pasada de ObjConv, no un flag del compilador

La corrección completa es un único comando de posprocesado aplicado a cada objeto compilado: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Tres de esas opciones se añadieron para este trabajo. -xw resuelve los símbolos IMAGE_SYM_CLASS_WEAK_EXTERNAL como externos normales. -xn normaliza los símbolos IMAGE_SYM_CLASS_NULL, como _fltused, que Free Pascal informa como Unsupported COFF symbol type 0. -xc hace el trabajo principal: degrada cada sección COMDAT a una sección normal y convierte en estáticos los símbolos que define. Así elimina el fallo porque elimina la decisión: sin secciones COMDAT no hay combinación, ni copia ganadora a la que una pasada redirija y otra no encuentre, y las secciones de unwind asociativas .pdata y .xdata desaparecen con ellas. El coste existe, aunque es pequeño: las copias que podrían haberse combinado sobreviven ahora por separado

El renombrado del prefijo -np:__imp_:pdflibimp_ resuelve una colisión distinta. MSVC llama a las API Win32 importadas mediante celdas de indirección llamadas __imp_*, Free Pascal reserva ese prefijo para su propio sistema de importación, y definir directamente uno de esos nombres provoca el mismo Internal error 200603061. Renombrar las celdas permite que el lado Pascal las publique como variables normales y las rellene durante la ejecución. Los propios objetos se compilan con el flag de enlazado estático activo, /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, y con los codecs de imagen desactivados, de modo que las rutas muertas de E/S de fichero y de codecs necesitan muchos menos stubs solo para enlazar. Se colocan en Lib\thirdparty\Win64f, mientras que la ruta de Delphi y C++Builder sigue enlazando su propio conjunto Win64x sin tocarlo: es el resultado correcto para una corrección de portabilidad confinada a una sola toolchain

Qué tiene que exportar todavía el lado Pascal

Free Pascal resuelve la importación de un objeto C por nombre de símbolo y necesita que ese nombre se escriba explícitamente, por lo que cada rutina Pascal que sustituye a un punto de entrada C lleva una cláusula public name explícita. Delphi toma el nombre de la rutina como nombre de símbolo y no necesita ninguna cláusula, por eso una unidad sirve para ambos compiladores con las cláusulas bajo {$IFDEF FPC}. La trampa es que una declaración external 'msvcrt.dll' no satisface nada: crea una importación, nunca una definición a la que pueda vincularse un objeto enlazado. El cuerpo de reenvío tiene que existir

// Una declaración external solo crea una importación. Ningún objeto enlazado puede
// vincularse a ella.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  external 'msvcrt.dll' name 'memcmp';

// Un cuerpo Pascal publicado con el nombre exacto del símbolo C es lo que el
// conjunto de objetos enlazado realmente utiliza
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
  public name 'memcmp';
begin
  Result := crt_memcmp(Buf1, Buf2, Count);
end;

Los puntos de entrada variádicos rompen ese patrón, porque un wrapper Pascal no puede reenviar sus propios varargs a otro callee variádico. La salida es dejar de ser un wrapper: exportar una rutina naked con el nombre C y hacer un salto final a la implementación real con los registros de argumentos y la pila exactamente como los preparó el caller. La capa JPEG 2000 ya gestiona así snprintf y vsnprintf, saltando a las grafías de msvcrt con guion bajo porque las versiones sin guion bajo solo las exporta UCRT. Hay otra restricción relacionada con el mismo error interno: las celdas de importación renombradas se rellenan desde una sección initialization mediante GetModuleHandleA y GetProcAddress, no desde inicializadores estáticos, porque tomar la dirección de una rutina importada en un inicializador hace que el compilador emita una corrección que no puede manejar y vuelva a fallar con 200603061

function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
  external 'msvcrt.dll' name 'sprintf';

var
  SPrintfTarget: Pointer = @crt_sprintf;

// Los varargs no se pueden reenviar desde un wrapper Pascal, así que el símbolo
// exportado salta al final con el marco exactamente como lo preparó el caller
procedure jbig2_sprintf; assembler; nostackframe;
  public name 'sprintf';
asm
  jmp qword ptr [rip + SPrintfTarget]
end;

Qué hace ahora de forma distinta un proyecto de Free Pascal

Nada aparte del nombre de la unidad en la cláusula uses, y ya no hay ningún fichero que desplegar. El backend se registra desde su propia sección de inicialización mediante RegisterJBIG2EncoderBackend, y los callers lo solicitan exactamente como antes: mediante el bit de opciones PDF_JBIG2_OPTION_EXTERNAL_ENCODER, cuyo valor es 4, o mediante el argumento UseExternalEncoder de los puntos de entrada extendidos. Solicitarlo sigue siendo una preferencia, no una garantía, porque una compilación que omita la unidad vuelve silenciosamente al encoder MMR nativo de Pascal y produce ficheros más grandes en lugar de un error

uses
  Classes, SysUtils,
  PDFlibrary,
  PDFlibJBIG2EncC;   // Delphi, C++Builder y, desde 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;

Conviene declarar claramente dos límites. Solo existe un conjunto de objetos Win64, así que en cualquier otro destino de Free Pascal el punto de entrada del encoder externo informa de un fallo y el encoder Pascal nativo realiza el trabajo. Y la regresión que gobierna todo esto es una comparación de renderizado, no una comprobación de tamaño: ambos encoders son lossless con la misma fuente, por lo que su salida se renderiza y se compara byte a byte, y la suite de Lazarus supera las 26 de 26 pruebas incluida esa. Comparar los tamaños de los streams comprimidos no habría demostrado nada, porque una página invertida se comprime aproximadamente al mismo tamaño que una correcta

La lección generaliza más allá de JBIG2. Una DLL es la forma adecuada cuando el límite es realmente dinámico, que es el caso al que sirven las superficies de integración DLL, ActiveX y dylib; es la forma equivocada cuando solo es un workaround para un lector COFF, porque añade un fichero a cada instalador, una ruta de búsqueda a cada despliegue y un modo de fallo por desfase de versiones que el enlazado estático no puede tener. El origen también importa, porque la forma de producir la imagen bitonal decide más sobre el tamaño final que el encoder, y el renderizado monocromo por regiones en Delphi cubre esa mitad de la pipeline. La cobertura de toolchains, los conjuntos de objetos por compilador y los destinos admitidos figuran en la página del producto PDF Developer Library de losLab