PDFlibPas 3.538.0 collega staticamente l'encoder JBIG2 esterno ai programmi Free Pascal e Lazarus. Un progetto aggiunge l'unità PDFlibJBIG2EncC, la stessa unità già usata da Delphi e C++Builder, e l'encoder finisce nell'eseguibile senza nulla di aggiuntivo da distribuire accanto. Questo ribalta la conclusione precedente su questa funzionalità, secondo cui Free Pascal poteva raggiungere l'encoder esterno solo tramite una DLL
Perché la DLL sembrava l'unica opzione?
La DLL sembrava l'unica opzione perché tre percorsi di linking fallivano in tre modi indipendenti e nessun'opzione del compilatore arrivava a correggerli. Il linker interno rifiuta direttamente le sezioni COMDAT associative. Un linking esterno tramite i binutils inclusi va in crash durante la garbage collection delle sezioni, che Free Pascal passa incondizionatamente per il target Windows a 64 bit. Binutils più recenti non riescono proprio a elaborare lo script di linking di Free Pascal. Ricostruire il lato C++ con l'altra toolchain scambia un rifiuto con un altro, perché l'instanziazione di template e inline emette per costruzione simboli esterni weak e Free Pascal li segnala come Unsupported COFF symbol type 105. Nessuna di queste evidenze era sbagliata, e la precedente analisi dei backend dell'encoder JBIG2 e del linker Free Pascal percorre ogni vicolo cieco in una forma che ancora oggi si riproduce. Era sbagliata l'ipotesi su dove potesse vivere la correzione. Ogni tentativo passava da un compilatore o da un linker, e nessuno dei due può cambiare ciò che un object file contiene già. Il problema era l'object file fin dall'inizio. ObjConv legge COFF e scrive COFF, e ogni costrutto che fa inciampare Free Pascal ha un equivalente meccanico che accetta
L'errore che non nomina mai la causa
Il linker interno di Free Pascal implementa i COMDAT pick-any solo a metà, e questa implementazione parziale è la cosa più difficile da diagnosticare in questo caso. Accorpa le definizioni duplicate, come richiede il formato. Ma TExeOutput.RemoveUnreferencedSections passa da exesymbol alla definizione vincente quando marca le sezioni come usate, mentre TCoffexeoutput.DoRelocationFixup legge direttamente objreloc.symbol.objsection. Quando una sezione usata fa riferimento a un simbolo che il suo object file definisce in una copia che ha perso l'accorpamento, i due passaggi guardano sezioni diverse e il linking si interrompe con Internal error 200603061
Confrontate questo limite con i due che lo affiancano. Unsupported COFF symbol type 105 indica un esterno weak. Associative or exact match COMDAT sections are not yet supported indica un COMDAT associativo e nomina perfino il simbolo problematico. Internal error 200603061 non dice assolutamente nulla: nessun nome di simbolo, nessun nome di sezione, nessun nome di file, nessuna fase. È anche il caso normale, non un caso limite, perché MSVC inserisce ogni string literal e ogni instanziazione inline o di template in un COMDAT pick-any, e nei 186 object file di questo insieme di encoder il linker ha eseguito 2656 accorpamenti. Compilare con /Gy- tiene le funzioni ordinarie fuori dalle sezioni COMDAT per funzione, ma lascia string literal e instanziazioni di template esattamente dove erano
Perché gli stub dei simboli CRT sembrano sempre essere quello che rompe tutto?
Perché il linker raggiunge il passaggio di fixup solo dopo aver risolto ogni simbolo. Finché manca qualcosa, l'esecuzione termina prima con Undefined symbol e il problema COMDAT non ha modo di emergere. Completate l'ultimo stub del runtime C e il linker avanza di una fase, entrando direttamente in Internal error 200603061. Il sintomo sul campo è quindi sistematicamente fuorviante: aggiungendo uno alla volta i corpi Pascal per i simboli C referenziati, sembra sempre che l'ultima aggiunta abbia rotto la build, oppure che sia stata superata una soglia intorno a un centinaio di stub. Non è così. Quale simbolo è stato aggiunto per ultimo e quanti ne sono stati aggiunti in totale sono dati irrilevanti, perché il guasto era latente fin dal primo object file ed è diventato raggiungibile solo quando la risoluzione è andata a buon fine. Quando un linker cambia messaggio dopo che avete corretto qualcosa di non correlato, chiedetevi se avete fatto avanzare una fase invece di introdurre una regressione
La correzione è un passaggio ObjConv, non un flag del compilatore
La correzione completa è un solo comando di post-processing eseguito su ogni object file compilato: ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Tre di queste opzioni sono state aggiunte per questo lavoro. -xw risolve i simboli IMAGE_SYM_CLASS_WEAK_EXTERNAL trasformandoli in normali esterni. -xn normalizza i simboli IMAGE_SYM_CLASS_NULL, come _fltused, che Free Pascal segnala come Unsupported COFF symbol type 0. -xc svolge il lavoro principale: declassa ogni sezione COMDAT a sezione normale e rende statici i simboli che definisce. Elimina così il guasto eliminando la decisione: senza sezioni COMDAT non ci sono accorpamenti, non c'è una copia vincente verso cui un passaggio possa reindirizzare e un altro non possa arrivare, e le sezioni associative di unwind .pdata e .xdata spariscono con esse. Il costo esiste, ma è contenuto: le copie che avrebbero potuto essere accorpate ora sopravvivono separatamente
La rinomina del prefisso -np:__imp_:pdflibimp_ risolve una collisione distinta. MSVC chiama le API Win32 importate tramite celle di indirezione chiamate __imp_*, Free Pascal riserva quel prefisso al proprio sistema di importazione e definire direttamente uno di quei nomi provoca lo stesso Internal error 200603061. Rinominare le celle consente al lato Pascal di pubblicarle come variabili normali e valorizzarle a runtime. Gli object file vengono compilati con il flag di linking statico attivo, /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, e con i codec immagine disattivati, così i percorsi morti di I/O file e codec richiedono molti meno stub solo per il linking. Vengono collocati in Lib\thirdparty\Win64f, mentre il percorso Delphi e C++Builder continua a collegare il proprio insieme Win64x senza modificarlo: è il risultato corretto per una correzione di portabilità confinata a una sola toolchain
Cosa deve ancora esportare il lato Pascal
Free Pascal risolve l'importazione di un object file C per nome di simbolo e ha bisogno che quel nome sia scritto esplicitamente, quindi ogni routine Pascal che sostituisce un entry point C porta una clausola public name esplicita. Delphi usa il nome della routine come nome del simbolo e non ha bisogno di alcuna clausola, perciò una sola unità serve entrambi i compilatori con le clausole sotto {$IFDEF FPC}. Il tranello è che una dichiarazione external 'msvcrt.dll' non soddisfa nulla: crea un'importazione, mai una definizione a cui un object file collegato possa associarsi. Il corpo di forwarding deve esistere
// Una dichiarazione external crea solo un'importazione. Nessun object file collegato può
// associarsi a essa.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// È un corpo Pascal pubblicato con il nome esatto del simbolo C a fornire il
// collegamento effettivo all'insieme di object file.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
Gli entry point variadici rompono questo schema, perché un wrapper Pascal non può inoltrare i propri varargs a un altro callee variadico. La soluzione è smettere di essere un wrapper: esportare una routine naked con il nome C ed eseguire un tail jump verso l'implementazione reale con i registri degli argomenti e lo stack esattamente come li ha preparati il caller. Il layer JPEG 2000 gestisce già così snprintf e vsnprintf, saltando alle grafie msvcrt con il prefisso underscore perché i nomi senza underscore sono esportati solo da UCRT. C'è un'altra restrizione collegata allo stesso errore interno: le celle di importazione rinominate vengono riempite da una sezione initialization tramite GetModuleHandleA e GetProcAddress, non da initializer statici, perché prendere l'indirizzo di una routine importata in un initializer induce il compilatore a emettere un fixup che non sa gestire e a fallire di nuovo con 200603061
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// I varargs non possono essere inoltrati da un wrapper Pascal, quindi il simbolo
// esportato esegue un tail jump mantenendo il frame impostato dal caller.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Cosa cambia ora in un progetto Free Pascal
Nulla oltre al nome dell'unità nella clausola uses, e non c'è più alcun file da distribuire. Il backend si registra dalla propria sezione di inizializzazione tramite RegisterJBIG2EncoderBackend, e i caller lo richiedono esattamente come prima: tramite il bit di opzioni PDF_JBIG2_OPTION_EXTERNAL_ENCODER, che ha valore 4, oppure tramite l'argomento UseExternalEncoder degli entry point estesi. Richiederlo resta una preferenza, non una garanzia, perché una build che esclude l'unità torna silenziosamente all'encoder MMR nativo Pascal e produce file più grandi invece di un errore
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder e, dalla 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 dichiarare con chiarezza due limiti. Esiste un solo insieme di object file Win64, quindi su qualsiasi altro target Free Pascal l'entry point dell'encoder esterno segnala un errore e il lavoro viene svolto dall'encoder Pascal nativo. Inoltre la regressione che governa tutto questo è un confronto del rendering, non un controllo delle dimensioni: entrambi gli encoder sono lossless sulla stessa sorgente, quindi l'output viene renderizzato e confrontato byte per byte, e la suite Lazarus supera 26 test su 26, compreso questo. Confrontare le dimensioni degli stream compressi non avrebbe dimostrato nulla, perché una pagina invertita si comprime all'incirca alla stessa dimensione di una corretta
La lezione va oltre JBIG2. Una DLL è la forma giusta quando il confine è davvero dinamico, che è il caso servito dalle superfici di integrazione DLL, ActiveX e dylib; è la forma sbagliata quando è solo un workaround per un lettore COFF, perché aggiunge un file a ogni installer, un percorso di ricerca a ogni distribuzione e una modalità di errore per disallineamento di versione che il linking statico non può avere. Conta anche la sorgente, perché il modo in cui viene prodotta l'immagine bitonale incide più dell'encoder sulla dimensione finale, e il rendering monocromatico per regioni in Delphi copre quella metà della pipeline. La copertura delle toolchain, gli insiemi di object file per compilatore e i target supportati sono elencati nella pagina del prodotto PDF Developer Library di losLab