PDFlibPas 3.538.0 lie statiquement l’encodeur JBIG2 externe aux programmes Free Pascal et Lazarus. Un projet ajoute l’unité PDFlibJBIG2EncC, la même unité que Delphi et C++Builder utilisent déjà, et l’encodeur se retrouve dans l’exécutable sans rien d’autre à distribuer à côté. Cela renverse la conclusion précédente sur cette fonctionnalité, selon laquelle Free Pascal ne pouvait atteindre l’encodeur externe qu’au moyen d’une DLL
Pourquoi la DLL semblait-elle être la seule option ?
La DLL semblait être la seule option parce que trois chemins d’édition de liens échouaient de trois manières indépendantes, sans qu’aucun commutateur du compilateur n’atteigne le problème. L’éditeur de liens interne rejette purement et simplement les sections COMDAT associatives. Une édition de liens externe passant par les binutils fournis s’effondre pendant le garbage collection des sections, que Free Pascal active inconditionnellement sur la cible Windows 64 bits. Des binutils plus récents ne peuvent pas traiter du tout le script de liens Free Pascal. Reconstruire la partie C++ avec l’autre toolchain échange un refus contre un autre, car l’instanciation des templates et des fonctions inline émet par construction des symboles externes faibles, que Free Pascal signale sous la forme Unsupported COFF symbol type 105. Rien dans ces observations n’était faux, et l’analyse précédente des backends d’encodeur JBIG2 et de l’éditeur de liens Free Pascal parcourt chaque impasse d’une façon qui se reproduit encore aujourd’hui. Ce qui était faux, c’était l’hypothèse sur l’endroit où le correctif pouvait vivre. Chaque tentative passait par un compilateur ou un éditeur de liens, alors qu’aucun des deux ne peut modifier ce qu’un fichier objet contient déjà. Le fichier objet était le problème depuis le début. ObjConv lit du COFF et écrit du COFF, et chaque construction qui fait trébucher Free Pascal possède un équivalent mécanique qu’il accepte
L’erreur qui ne nomme jamais sa cause
L’éditeur de liens interne de Free Pascal n’implémente les COMDAT pick-any qu’à moitié, et cette demi-implémentation est ici la chose la plus difficile à diagnostiquer. Il fusionne bien les définitions dupliquées, comme le prévoit le format. Mais TExeOutput.RemoveUnreferencedSections passe par exesymbol pour rediriger vers la définition gagnante lorsqu’il marque les sections comme utilisées, tandis que TCoffexeoutput.DoRelocationFixup lit directement objreloc.symbol.objsection. Lorsqu’une section utilisée référence un symbole que son propre objet définit dans une copie ayant perdu la fusion, les deux passes regardent des sections différentes et l’édition de liens s’arrête avec Internal error 200603061
Comparez cela aux deux limites qui l’encadrent. Unsupported COFF symbol type 105 indique un externe faible. Associative or exact match COMDAT sections are not yet supported indique une COMDAT associative et nomme même le symbole fautif. L’erreur interne 200603061 ne dit absolument rien : ni nom de symbole, ni nom de section, ni nom de fichier, ni phase. C’est aussi le cas normal plutôt qu’un cas limite, car MSVC place chaque littéral de chaîne et chaque instanciation inline ou de template dans une COMDAT pick-any, et sur les 186 objets de cet ensemble d’encodeur l’éditeur de liens a effectué 2656 fusions. La compilation avec /Gy- maintient les fonctions ordinaires hors des sections COMDAT par fonction, mais laisse les littéraux de chaîne et les instanciations de templates exactement là où ils étaient
Pourquoi les stubs de symboles CRT donnent-ils toujours l’impression que le dernier ajouté a cassé le build ?
Parce que l’éditeur de liens n’atteint la passe de correction qu’une fois tous les symboles résolus. Tant qu’il en manque un, l’exécution s’arrête plus tôt avec Undefined symbol et le problème de COMDAT n’a jamais l’occasion d’apparaître. Ajoutez le dernier stub du runtime C et l’éditeur de liens avance d’une phase, directement vers l’erreur interne 200603061. Le symptôme observé est donc systématiquement trompeur : lorsque vous ajoutez un par un les corps Pascal des symboles C référencés, tout donne l’impression que le dernier ajout a cassé le build, ou qu’un seuil d’environ une centaine de stubs vient d’être franchi. Ni l’un ni l’autre n’est vrai. Le symbole ajouté en dernier et le nombre total de symboles ajoutés sont tous deux sans importance, car l’échec était latent dès le premier objet et n’est devenu atteignable qu’une fois la résolution terminée. Lorsqu’un éditeur de liens change de plainte après la correction d’un élément sans rapport, demandez-vous si vous avez avancé d’une phase au lieu de provoquer une régression
Le correctif tient en une passe ObjConv, pas dans un commutateur du compilateur
La correction complète est une seule commande de post-traitement exécutée sur chaque objet compilé : ObjConv -fcoff64 -xw -xc -xn -np:__imp_:pdflibimp_. Trois de ces options ont été ajoutées pour ce travail. -xw transforme les symboles IMAGE_SYM_CLASS_WEAK_EXTERNAL en externes ordinaires. -xn normalise les symboles IMAGE_SYM_CLASS_NULL tels que _fltused, que Free Pascal signale comme Unsupported COFF symbol type 0. -xc fait l’essentiel : il rétrograde chaque section COMDAT en section ordinaire et rend statiques les symboles qu’elle définit. L’échec disparaît parce que la décision disparaît : sans sections COMDAT, il n’y a plus de fusion, plus de copie gagnante vers laquelle une passe pourrait rediriger et qu’une autre manquerait, et les sections de déroulage associatives .pdata et .xdata disparaissent avec elles. Le coût est réel mais limité, puisque les copies qui auraient pu être fusionnées subsistent désormais chacune
Le renommage du préfixe -np:__imp_:pdflibimp_ résout une collision distincte. MSVC appelle les API Win32 importées par l’intermédiaire de cellules d’indirection nommées __imp_*, Free Pascal réserve ce préfixe à son propre mécanisme d’importation, et définir directement l’un de ces noms déclenche la même erreur interne 200603061. Renommer les cellules permet à la partie Pascal de les publier comme des variables ordinaires et de les remplir à l’exécution. Les objets eux-mêmes sont compilés avec le flag d’édition de liens statique activé, /GS- /Gs999999 /Gy- /Zl /GR- /EHs-c-, et avec les codecs d’image désactivés, de sorte que les chemins morts d’E/S de fichiers et de codecs nécessitent bien moins de stubs uniquement destinés à l’édition de liens. Ils se trouvent dans Lib\thirdparty\Win64f, tandis que le chemin Delphi et C++Builder continue d’utiliser son propre ensemble Win64x sans le modifier, ce qui est le bon résultat pour un correctif de portabilité limité à un seul toolchain
Ce que la partie Pascal doit toujours exporter
Free Pascal résout l’importation d’un objet C par nom de symbole et exige que ce nom soit explicité ; toute routine Pascal qui remplace un point d’entrée C porte donc une clause public name explicite. Delphi prend le nom de la routine comme nom de symbole et n’a besoin d’aucune clause, raison pour laquelle une même unité sert les deux compilateurs avec les clauses placées sous {$IFDEF FPC}. Le piège est qu’une déclaration external 'msvcrt.dll' ne satisfait rien : elle crée un import, jamais une définition à laquelle un objet lié puisse s’attacher. Le corps de transfert doit exister
// Une déclaration external ne crée qu’un import. Aucun objet lié ne peut
// s’y lier.
function crt_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
external 'msvcrt.dll' name 'memcmp';
// C’est un corps Pascal publié sous le nom exact du symbole C que
// l’ensemble d’objets utilise réellement.
function jbig2_memcmp(Buf1, Buf2: Pointer; Count: NativeUInt): Integer; cdecl;
public name 'memcmp';
begin
Result := crt_memcmp(Buf1, Buf2, Count);
end;
Les points d’entrée variadiques brisent ce schéma, car un wrapper Pascal ne peut pas transmettre ses propres varargs à un autre callee variadique. La solution consiste à cesser d’être un wrapper : exportez une routine nue sous le nom C et effectuez un saut final vers l’implémentation réelle, avec les registres d’arguments et la pile exactement dans l’état où l’appelant les a préparés. La couche JPEG 2000 gère déjà snprintf et vsnprintf de cette manière, en sautant vers les noms msvcrt préfixés par un underscore, car les noms simples ne sont exportés que par l’UCRT. Une contrainte liée vient de la même erreur interne : les cellules d’import renommées sont remplies depuis une section initialization via GetModuleHandleA et GetProcAddress, plutôt que depuis des initialiseurs statiques, car prendre l’adresse d’une routine importée dans un initialiseur force le compilateur à émettre une correction qu’il ne sait pas gérer et provoque de nouveau l’échec 200603061
function crt_sprintf(Buffer: PAnsiChar; Fmt: PAnsiChar): Integer; cdecl; varargs;
external 'msvcrt.dll' name 'sprintf';
var
SPrintfTarget: Pointer = @crt_sprintf;
// Les varargs ne peuvent pas être transmis par un wrapper Pascal ; le symbole
// exporté saute donc vers la cible en laissant la trame comme l’appelant l’a préparée.
procedure jbig2_sprintf; assembler; nostackframe;
public name 'sprintf';
asm
jmp qword ptr [rip + SPrintfTarget]
end;
Ce qu’un projet Free Pascal fait désormais différemment
Rien au-delà du nom de l’unité dans la clause uses, et il n’y a plus de fichier à déployer. Le backend s’enregistre depuis sa propre section d’initialisation par l’intermédiaire de RegisterJBIG2EncoderBackend, et les appelants le demandent exactement comme avant : via le bit d’options PDF_JBIG2_OPTION_EXTERNAL_ENCODER, dont la valeur est 4, ou via l’argument UseExternalEncoder des points d’entrée étendus. La demande reste une préférence plutôt qu’une garantie, car un build qui omet l’unité revient silencieusement à l’encodeur Pascal MMR natif et produit des fichiers plus volumineux au lieu de renvoyer une erreur
uses
Classes, SysUtils,
PDFlibrary,
PDFlibJBIG2EncC; // Delphi, C++Builder et, depuis 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 et 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;
Deux limites doivent être énoncées clairement. Seul un ensemble d’objets Win64 existe ; sur toute autre cible Free Pascal, le point d’entrée d’encodage externe signale un échec et l’encodeur Pascal natif prend le relais. Ensuite, la régression qui valide tout cela est une comparaison de rendu plutôt qu’un contrôle de taille : les deux encodeurs sont sans perte sur la même source, leur sortie est donc rendue et comparée octet par octet, et la suite Lazarus réussit 26 tests sur 26, y compris celui-ci. Comparer la taille des flux compressés n’aurait rien prouvé, car une page inversée se compresse à peu près à la même taille qu’une page correcte
La leçon dépasse JBIG2. Une DLL est la bonne forme lorsque la frontière est réellement dynamique, ce qui est le cas des surfaces d’intégration DLL, ActiveX et dylib ; c’est la mauvaise forme lorsqu’elle ne sert que de contournement pour un lecteur COFF, car elle ajoute un fichier à chaque installateur, un chemin de recherche à chaque déploiement et un mode de défaillance lié au décalage de versions que l’édition de liens statique n’a pas. L’amont compte aussi, car la manière dont l’image à deux niveaux est produite influence davantage la taille finale que l’encodeur, et le rendu monochrome par région dans Delphi couvre cette moitié du pipeline. La couverture des toolchains, les ensembles d’objets par compilateur et les cibles prises en charge sont indiqués sur la page produit losLab PDF Developer Library