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

Le PDFium Component lie sa table d'exportations Delphi en une seule passe, où CheckGetProcAddress abandonne le chargement sur une exportation requise manquante tandis que TryGetProcAddress dégrade sûrement une exportation optionnelle
Les exports requis se lient en tout-ou-rien et avortent le chargement au premier nil, tandis que les exports optionnels laissent un pointeur nil derrière un contrôle de capacité Assigned
function CheckGetProcAddress(const Name: string): Pointer;
begin
  Result := GetProcAddress(PDFiumLibrary, PChar(Name));
  if Result = nil then
  begin
    // Un export requis manquant signifie que le pdfium.dll déployé est plus
    // ancien que cette version de la liaison. Supprimez chaque pointeur résolu
    // jusqu'ici afin qu'aucun appelant ne puisse atteindre le module à libérer.
    UnloadLibrary;
    raise EPdfError.Create('Required PDFium export not found: ' + Name);
  end;
end;

function TryGetProcAddress(const Name: string): Pointer;
begin
  // Export optionnel. nil est une réponse légitime ici ; chaque appelant est
  // tenu de tester Assigned() avant de déréférencer la 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');
// Les descriptions de pièce jointe ont été ajoutées après la révision de DLL fournie.
// Gardez-les optionnelles pour que les déploiements plus anciens continuent de se charger.
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

Une exportation de description de pièce jointe PDFium manquante en Delphi renvoie une lecture vide via TPdf.GetAttachmentDescription et lève à l'écriture, sous le contrôle de AttachmentDescriptionFeaturesAvailable
Le côté lecture se dégrade en réponse vide, le côté écriture lève avec une raison nommée, et une sonde nommée laisse l'UI désactiver la fonctionnalité d'emblée
function TPdf.GetAttachmentDescription(Index: Integer): WString;
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  Result := '';

  // Le côté lecture se dégrade : une ancienne DLL ne peut pas rapporter /Desc, et '' est
  // impossible à distinguer d'une pièce jointe qui ne porte aucune description.
  if not Assigned(FPDFAttachment_GetDescription) then
    Exit;
  // ... dimensionnement de tampon sur deux passes avec FPDFAttachment_GetDescription ...
end;

procedure TPdf.SetAttachmentDescription(Index: Integer; const Value: WString);
begin
  CheckActive;
  Check((Index >= 0) and (Index < AttachmentCount), 'Incorrect attachment index');
  // Le côté écriture refuse : abandonner silencieusement la valeur produirait un fichier
  // que l'appelant croit porteur d'une description et qui n'en porte pas.
  Check(Assigned(FPDFAttachment_SetDescription),
    'Attachment descriptions are not supported by the loaded PDFium DLL');
  // ... FPDFDoc_GetAttachment, puis 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
  // Demandez une fois, à la configuration du formulaire, au lieu de découvrir la limite à l'enregistrement.
  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