Article technique

Exports PDFium Optionnels : Portes de Capacite en Delphi

Votre pdfium.dll se charge sans problème et une procédure manque quand même. PDFium Component gère cela en divisant ses bindings en deux classes : les exports requis résolus via CheckGetProcAddress, qui interrompent le chargement purement et simplement, et les exports optionnels résolus via TryGetProcAddress, qui laissent un pointeur nil et un contrôle de capacité à la place

Ce n est pas le même problème qu une DLL introuvable. Si votre application meurt avec une erreur de mauvais format EXE, un fichier manquant, ou une incompatibilité d architecture, cette histoire est racontée dans l article compagnon sur le déploiement de pdfium.dll et le diagnostic des échecs de chargement. Ici le chargeur a réussi. Le handle de module est valide, des centaines d exports se sont résolus, et l exécution se termine quand même avant que votre première page ne se rende car un point d entrée arrivé dans une version plus récente de PDFium n est pas dans le binaire sur disque

Pourquoi un export manquant casse-t-il toute la bibliothèque ?

Parce qu un binding requis est un contrat strict, et il est appliqué durant une seule séquence de liaison tout-ou-rien. PDFium Component résout toute sa table d exports à l intérieur de LoadLibrary, un appel CheckGetProcAddress après l autre. Le premier résultat nil lève EPdfError et appelle UnloadLibrary avant de le faire, ce qui est délibéré : une liaison partielle laisserait autrement des pointeurs déjà résolus pointés vers un module sur le point d être libéré, mettant silencieusement en échec chaque garde Assigned en aval

La conséquence est le mode de défaillance qui amène les gens ici. Vous mettez à niveau le composant, livrez le même pdfium.dll que vous livrez depuis deux ans, et l application ne démarrera pas. L erreur nomme un export pour une fonctionnalité que vous n avez jamais appelée. Rien de ce que vous faites au point d appel n aide, car le point d appel ne s exécute jamais ; l échec s est produit durant la liaison, avant qu aucun document ne soit ouvert

function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // A missing required export means the deployed pdfium.dll is older
    // than this build of the binding. Drop every pointer resolved so far
    // so no caller can reach into the module we are about to free.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // Optional export. nil is a legitimate answer here; every caller is
  // required to test Assigned() before dereferencing the variable.
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
end;

Requis ou optionnel : où se trouve réellement la ligne

La règle que PDFium Component applique est directe. Un export est requis quand son absence rend le composant incapable de faire le travail pour lequel il existe, et optionnel quand son absence ne retire qu une fonctionnalité feuille. FPDF_InitLibrary, FPDF_LoadDocument, FPDF_RenderPageBitmap, FPDF_ClosePage sont requis, et échouer bruyamment sur ceux-là est correct : un visualiseur qui ne peut pas rendre n est pas un visualiseur dégradé, c est un visualiseur cassé

Tout ce qui est atteint aujourd hui via le chargeur tolérant est une feuille. FPDFBookmark_GetColor est arrivé après M109 et ne fournit que le tableau de couleur /C optionnel d une entrée d esquisse, donc une DLL antérieure signale simplement l absence de couleur de signet. Les assistants V8 FPDF_GetRecommendedV8Flags et FPDF_GetArrayBufferAllocatorSharedInstance, et les assistants de chaîne XFA FPDF_BStr_Init, FPDF_BStr_Set et FPDF_BStr_Clear, sont absents de tout build non-V8 par construction, donc les traiter comme requis rendrait le pdfium.dll ordinaire inchargeable. Et la paire qui a motivé cet article : FPDFAttachment_SetDescription et FPDFAttachment_GetDescription, ajoutés en amont le 2026-07-13, plus tard que la date de build des quatre binaires PDFium que le projet livre sous DLLs/Win32 et DLLs/Win64. Ce dernier cas est la forme générale du problème, pas un événement isolé : une couche de binding suit les en-têtes amont, qui bougent continuellement, tandis que la DLL de votre installateur bouge par sauts discrets chaque fois que quelqu un la reconstruit. Il y a toujours une fenêtre pendant laquelle le côté Pascal connaît des exports que le binaire déployé n a pas, et décider à l avance de quel côté de la ligne requis/optionnel se trouve chaque nouvel export est la seule chose qui rend cette fenêtre survivable

FPDFDoc_GetAttachmentCount    := CheckGetProcAddress('FPDFDoc_GetAttachmentCount');
FPDFDoc_AddAttachment         := CheckGetProcAddress('FPDFDoc_AddAttachment');
FPDFAttachment_GetName        := CheckGetProcAddress('FPDFAttachment_GetName');
FPDFAttachment_GetStringValue := CheckGetProcAddress('FPDFAttachment_GetStringValue');
// Attachment descriptions were added after the bundled DLL revision.
// Keep them optional so older deployments continue to load.
FPDFAttachment_SetDescription := TryGetProcAddress('FPDFAttachment_SetDescription');
FPDFAttachment_GetDescription := TryGetProcAddress('FPDFAttachment_GetDescription');
FPDFAttachment_SetFile        := CheckGetProcAddress('FPDFAttachment_SetFile');
FPDFAttachment_GetFile        := CheckGetProcAddress('FPDFAttachment_GetFile');

Que devrait faire une porte de capacité au point d appel ?

Elle devrait être asymétrique, et cette asymétrie est toute la conception. Une lecture qui ne peut pas s exécuter a une réponse vide honnête. Une écriture qui ne peut pas s exécuter n a aucune réponse honnête du tout, donc elle doit lever une exception. PDFium Component divise la propriété de description de pièce jointe exactement le long de cette ligne, et la division est ce qui empêche un export manquant de se transformer en perte de données silencieuse. TPdf.GetAttachmentDescription teste Assigned(FPDFAttachment_GetDescription) et sort avec un WString vide. Ce n est pas un mensonge : sur une DLL sans l export, le composant ne peut véritablement pas dire si la pièce jointe porte une entrée /Desc, et une description vide se lit de la même façon qu une pièce jointe qui n en a jamais eu. Le reste de l API de pièces jointes, couverte dans l article sur le travail avec les pièces jointes PDF en Delphi, continue de fonctionner intact

TPdf.SetAttachmentDescription prend le chemin opposé. Elle appelle Check sur le même test Assigned et lève EPdfError avec le texte « Attachment descriptions are not supported by the loaded PDFium DLL ». Revenir silencieusement ici serait la pire option disponible : l appelant définirait une description, n obtiendrait aucune erreur, enregistrerait le fichier, et livrerait un PDF où la description est simplement absente. Personne ne le remarque jusqu à ce qu un consommateur en aval demande où elle est passée

function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // Read side degrades: an old DLL cannot report /Desc, and '' is
  // indistinguishable from an attachment that carries no description.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... two-pass buffer sizing against FPDFAttachment_GetDescription ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // Write side refuses: silently dropping the value would produce a file
  // the caller believes carries a description and does not.
  Check(Assigned(FPDFAttachment_SetDescription),
    'Attachment descriptions are not supported by the loaded PDFium DLL');
  // ... FPDFDoc_GetAttachment, then FPDFAttachment_SetDescription ...
end;

Sonder la capacité avant d offrir la fonctionnalité

Attraper une exception est un mauvais moyen de découvrir ce que votre déploiement peut faire, donc PDFium Component expose le même test comme une fonction nommée. AttachmentDescriptionFeaturesAvailable appelle LoadLibrary et renvoie si les deux moitiés de la paire se sont résolues. Elle se trouve à côté de V8FeaturesAvailable, XfaBStrHelpersAvailable et XfaFeaturesAvailable, qui suivent le motif identique pour leurs propres groupes optionnels. Nommer la sonde compte plus qu il n y paraît : un booléen appelé AttachmentDescriptionFeaturesAvailable dit au prochain mainteneur que cette fonctionnalité est conditionnelle au binaire déployé, ce qu un test Assigned nu enterré dans un setter de propriété ne fait jamais. Cela donne aussi à la couche interface utilisateur quelque chose à lier, de sorte que la boîte d édition de description soit désactivée d emblée plutôt que d accepter une saisie et de la rejeter à l enregistrement

procedure TAttachmentFrame.SyncCapabilities;
begin
  // Ask once, at form setup, instead of discovering the limit on save.
  DescriptionEdit.Enabled := AttachmentDescriptionFeaturesAvailable;
  if not DescriptionEdit.Enabled then
    DescriptionEdit.TextHint := 'Requires a newer pdfium.dll';
end;

procedure TAttachmentFrame.SaveDescription(Pdf: TPdf; Index: Integer);
begin
  if not AttachmentDescriptionFeaturesAvailable then
    Exit;
  Pdf.AttachmentDescription[Index] := DescriptionEdit.Text;
end;

Pourquoi la couverture de liaison doit-elle être prouvée par un outil ?

Parce que les chiffres ont dépassé le point où un humain peut leur être confié. PDFium Component a audité 21 en-têtes PDFium publics contre une base amont du 2026-07-29 et trouvé 470 fonctions ABI C exportées. Le binding en couvrait déjà 468. Personne n a localisé cet écart de deux en lisant les en-têtes ; un script l a fait, en une seconde, et il le refera au prochain saut amont. tools/audit_pdfium_public_api.py est délibérément petit : il applique une correspondance regex à FPDF_EXPORT ... FPDF_CALLCONV name( sur chaque en-tête du répertoire public, applique une correspondance regex à chaque CheckGetProcAddress('Name') et TryGetProcAddress('Name') dans PDFium.pas, et affiche les deux différences d ensemble : missing pour les exports sans binding, stale pour les bindings dont l export n existe plus en amont. Il sort avec un code non nul quand l un ou l autre ensemble est non vide, donc il s intègre dans une étape de build sans autre cérémonie. Le résultat actuel est 470 sur 470 liés, 0 manquant, 0 périmé

La direction « périmé » mérite sa place autant que « manquant ». Un export que l amont retire laisse derrière une ligne CheckGetProcAddress qui échouera durement à chaque chargement futur, et ce genre de pourriture est invisible jusqu au jour où quelqu un met à jour la DLL. La revue manuelle trouve la fonction à laquelle vous pensiez ; elle ne trouve pas celle à laquelle vous ne pensiez pas. Notez aussi que l audit compte délibérément les deux chargeurs comme couverture, ce qui est le bon choix pour la dérive d API et la raison pour laquelle la division requis/optionnel doit être une décision documentée plutôt qu un sous-produit de qui a ajouté la ligne

Où la liaison optionnelle cesse d être honnête

Deux limites méritent d être énoncées clairement, car le motif est facile à sur-appliquer. La première est qu un pointeur de fonction nil n est sûr que si littéralement chaque chemin qui le touche teste Assigned d abord. Dans une unité qui déclare des centaines de variables de fonction cdecl, un seul appel non gardé est une violation d accès à une adresse qui ne signifie rien dans une trace de pile. La même discipline qui gouverne les conventions d appel et les durées de vie à travers la frontière C s applique ici, et c est le sujet de l article sur le renforcement du binding PDFium contre les défauts d ABI et de sécurité mémoire

La seconde limite est la portée. La liaison optionnelle n est pas une licence générale pour rendre tout tolérant. Si FPDF_RenderPageBitmap était optionnel, le composant se chargerait joyeusement puis échouerait sur chaque page, convertissant une erreur de démarrage claire en une dispersion d erreurs d exécution sans cause évidente. Requis est le défaut correct. Optionnel est l exception à laquelle vous recourez quand une fonctionnalité est véritablement une feuille, quand l absence a un comportement dégradé défendable côté lecture, et quand le côté écriture peut refuser avec un message qui nomme la raison

La conception du chargeur, les sondes de capacité et l outil d audit décrits ici sont livrés dans le cadre de PDFium Component pour Delphi et C++Builder ; la page produit liste les binaires PDFium fournis et la surface complète de l API qu ils exposent