Compatibilité descendante des interfaces DLL et COM — Tableau de décision : quels changements cassent les appelants
· Go Komura · COM, DLL, .NET, C#, C++, Compatibilité descendante, Versionnage, Technologie héritée, Réutilisation de l'existant, Tableau de décision
« Ce correctif, faut-il simplement remplacer la DLL, ou faut-il aussi recompiler l’appelant ? » — si vous maintenez une DLL partagée ou un composant COM référencé par plusieurs applications, vous finissez par répondre à cette question à chaque publication. Se tromper peut empêcher un vieil EXE encore en service chez un client de démarrer, ou pire : il continue de démarrer normalement pendant que ses résultats de calcul changent silencieusement en coulisses.
Ce qui rend cette décision délicate, c’est qu’elle est souvent prise sur la base d’une simple impression de « ça a l’air risqué ». En réalité, on peut déterminer de façon quasi mécanique quels changements cassent la compatibilité. Les DLL natives obéissent à des règles bien définies concernant les exports et les conventions d’appel ; COM impose la règle d’or explicite selon laquelle les interfaces sont immuables1 ; et .NET publie une liste de règles de changement pour la compatibilité que Microsoft utilise lui-même pour développer les bibliothèques .NET2.
Sur ce blog, nous avons déjà présenté les bases de COM dans « Qu’est-ce que COM / ActiveX / OCX ? » et sa philosophie de conception dans « Qu’est-ce que COM — Pourquoi la conception de COM sous Windows reste élégante aujourd’hui ». Cet article organise, sous forme de tableau de décision, les changements qui cassent les appelants pour les DLL, COM et les assemblies .NET, et détaille la procédure à suivre lorsqu’il devient inévitable de casser la compatibilité.
1. La conclusion d’abord
- La compatibilité comporte trois niveaux : la compatibilité binaire (fonctionne sans recompilation), la compatibilité source (fonctionne après recompilation) et la compatibilité comportementale (le comportement ne change pas). « Pas besoin de recompiler » ne signifie pas « sans danger » — il faut aussi juger la compatibilité comportementale.3
- Pour les DLL natives, la règle de base est : ajouter un export est sûr ; modifier ou supprimer un export existant casse la compatibilité. La signature des fonctions, la convention d’appel et la disposition des structures constituent le contrat binaire lui-même.
- Une interface COM est immuable une fois publiée. Ajouter, supprimer ou réordonner des méthodes après publication viole la spécification ; les changements doivent être ajoutés sous la forme d’une nouvelle interface avec un nouvel IID (IFoo → IFoo2).41
- Les clients VB6/VBA utilisent la liaison anticipée (early binding), qui fige les positions des emplacements de la vtable, ce qui en fait les appelants les plus susceptibles de se casser au moindre changement de disposition d’une interface.
- Pour .NET, ce qui constitue un changement cassant pour une API publique est publié sous forme des règles de changement pour la compatibilité de Microsoft ; la liste classe comme cassants non seulement la suppression de méthode ou les changements de signature, mais aussi l’ajout de virtual à un membre, voire le renommage d’un paramètre.2
- Le versionnage sémantique est une convention consistant à incrémenter la version majeure lors d’un changement cassant, mais il ne fonctionne qu’une fois que vous avez déclaré une définition de ce qui compte comme cassant.5 Vous pouvez utiliser le tableau de décision de cet article comme cette définition.
- Lorsqu’il devient inévitable de casser la compatibilité, procédez dans cet ordre : offrir l’ancienne et la nouvelle version en parallèle → une période de dépréciation → un inventaire des appelants → le retrait. Le principe est de ne jamais remplacer d’un coup.
2. Les trois niveaux de compatibilité — qui casse, et quand
Ce que l’on regroupe familièrement sous le terme de « compatibilité descendante » se décompose en réalité en trois niveaux. Même la documentation officielle de .NET classe les changements cassants selon les axes source, binaire et comportemental.3
| Niveau | Signification | Ce qui se passe en cas de rupture | Qui en souffre principalement |
|---|---|---|---|
| Compatibilité binaire | Les appelants fonctionnent avec la nouvelle DLL sans recompilation | Point d’entrée introuvable au démarrage, MissingMethodException à l’exécution, plantages |
Les vieux EXE encore en service chez les clients, les applications tierces qu’on ne peut pas recompiler |
| Compatibilité source | Les appelants fonctionnent après recompilation | Erreurs de compilation à la prochaine build | Une autre équipe en interne, les développeurs qui détiennent le code source |
| Compatibilité comportementale | Le comportement spécifié ne change pas | Les résultats, le timing ou le type d’exception levée changent sans aucune erreur | Les utilisateurs finaux (et tous ceux qui enquêtent ensuite sur l’incident) |
Ce qui importe avec ces trois niveaux, c’est qu’un niveau extérieur peut se casser même quand un niveau intérieur reste intact. Par exemple, un correctif qui change la signification de la valeur de retour d’une fonction existante préserve à la fois la compatibilité binaire et la compatibilité source tout en cassant uniquement la compatibilité comportementale. Comme ce type de changement ne produit ni erreur d’édition de liens ni erreur de compilation, c’est la ligne du tableau de décision la plus facile à négliger.
À l’inverse, dans un environnement où tous les appelants détiennent le code source et peuvent être recompilés en même temps (un système interne dans un dépôt unique, par exemple), il suffit de préserver la compatibilité source et comportementale — la compatibilité binaire peut être retirée des exigences. Se demander si un appelant de votre DLL est un binaire que vous ne pouvez pas recompiler est le premier embranchement à considérer pour lire le tableau de décision.
3. Le tableau de décision de compatibilité pour les DLL natives (C/C++)
La compatibilité d’une DLL native est déterminée par sa table d’exports, ses conventions d’appel et sa disposition mémoire. La façon dont une DLL est recherchée puis chargée est traitée dans « Comment fonctionne la résolution des noms de DLL sous Windows » ; une fois le chargement réussi, le tableau ci-dessous permet de juger la compatibilité.
| Changement | Compatibilité binaire | Remarques |
|---|---|---|
| Ajout d’une fonction exportée | Ne casse pas | Le moyen le plus sûr d’étendre une API. Toutefois, si vous vous appuyez sur des ordinaux implicites d’un fichier .def, ajouter une fonction peut renuméroter les ordinaux existants selon l’endroit où elle atterrit ; si un client lie par ordinal, fixez explicitement les ordinaux existants et ajoutez les nouveaux à la fin |
| Suppression ou renommage d’une fonction exportée | Casse | La résolution des imports échoue, provoquant une erreur au chargement ou via GetProcAddress |
| Changement de signature d’une fonction existante (ajout/suppression/changement de type de paramètre, changement du type de retour) | Casse | Les conventions de passage par pile ou par registre ne correspondent plus. Il en va de même pour la valeur de retour — passer d’un entier (RAX) à une valeur flottante (XMM0) fait lire à l’appelant des données aberrantes sous l’ancienne ABI. Cela peut s’exécuter sans erreur et simplement partir en vrille |
Changement de la convention d’appel (__cdecl ↔ __stdcall) |
Casse (32 bits) | Sous x86, la responsabilité du nettoyage de la pile s’inverse, provoquant une corruption de pile. x64 n’a qu’une seule convention d’appel et ignore en pratique ces spécificateurs, donc cette ligne concerne uniquement les DLL 32 bits |
| Changement d’un ordinal d’export | Casse, sous condition | Un appelant liant par ordinal finit par appeler une fonction différente. Aucun impact si tous les appelants lient par nom uniquement |
| Ajout d’un membre à une structure allouée par l’appelant | Casse | Un ancien appelant continue d’allouer et de transmettre la version plus petite (peut être atténué par la convention cbSize détaillée plus bas) |
Changement du compactage/alignement d’une structure publique (#pragma pack, /Zp, changement de chaîne d’outils) |
Casse | Les décalages des membres existants et la taille globale changent même sans toucher un seul membre. cbSize ne peut pas vous sauver d’un décalage de disposition, donc fixez explicitement le compactage dans l’en-tête public |
| Changement interne d’une structure que seule la DLL alloue et libère | Ne casse pas | Si la conception n’expose vers l’extérieur qu’un pointeur (un handle), l’intérieur peut être modifié librement |
| Changement de la signification d’une valeur de retour ou d’un code d’erreur | Ne casse pas (mais la compatibilité comportementale, si) | L’édition de liens réussit toujours pendant que le comportement change — le motif le plus long à être découvert |
| Ajout d’un membre de données ou d’une fonction virtuelle à une classe C++ exportée directement | Casse | La taille de l’objet ou la disposition de la vtable change. Ajouter uniquement une fonction membre non virtuelle laisse la disposition inchangée et ne casse pas directement les clients existants, mais exporter directement une classe C++ n’offre de toute façon aucune compatibilité entre compilateurs, et devoir refaire ce jugement à chaque fois est en soi le signe d’une ABI fragile |
L’orientation de conception qui découle de ce tableau n’a pas changé depuis des décennies : limiter la frontière à une ABI C (fonctions extern "C" et structures simples), et étendre par ajout de fonctions. Cela vaut même quand on construit une DLL native depuis C# : la surface d’export traitée dans « Comment appeler une DLL C# Native AOT depuis C/C++ » se gère exactement selon ce tableau.
3.1 La convention cbSize — l’astuce Win32 pour rendre les structures extensibles
La parade classique au problème « ajouter un membre à une structure casse la compatibilité » est la convention Win32 consistant à placer un champ de taille en tête de la structure. L’appelant renseigne cbSize avec la taille de la structure qu’il connaissait à la compilation et la transmet ; la DLL examine cette taille pour déterminer de quelle génération de structure cet appelant a connaissance.
typedef struct KS_CONFIG {
DWORD cbSize; // L'appelant définit sizeof(KS_CONFIG)
DWORD dwMode;
DWORD dwTimeout;
// Toujours ajouter les futurs membres à la fin
} KS_CONFIG;
// Côté DLL : utiliser cbSize pour distinguer les générations, et appliquer une valeur par défaut aux anciens appelants
if (pConfig->cbSize >= FIELD_OFFSET(KS_CONFIG, dwTimeout) + sizeof(DWORD)) {
timeout = pConfig->dwTimeout; // Nouvel appelant
} else {
timeout = DEFAULT_TIMEOUT; // Ancien appelant
}
En fait, la structure NOTIFYICONDATA de l’API Windows est justement versionnée par génération selon ce schéma, et il est officiellement documenté que définir la bonne valeur dans cbSize permet de rester compatible avec d’anciennes versions de Shell32.dll.6 Si vous intégrez cbSize dans les structures publiques de votre propre DLL dès la toute première version, les extensions ultérieures passent du côté « changement cassant » au côté sûr du tableau de décision. Cela dit, les nouveaux membres doivent toujours être ajoutés à la fin, et changer le type ou l’ordre d’un membre existant reste interdit. Autre point : pour une structure utilisée en sortie, la responsabilité côté DLL s’accroît — l’écriture et l’initialisation doivent toujours rester dans les limites du cbSize réellement transmis par l’appelant. Écrire sans condition la totalité du nouveau sizeof déborde du tampon plus petit alloué par un ancien appelant, et c’est alors la DLL elle-même qui provoque exactement le type de rupture que cette convention était censée prévenir.
4. La règle d’or des interfaces COM — aucun changement une fois publiées
COM est la technologie qui a apporté la réponse la plus claire à ce problème. Selon la spécification COM, une interface obéit aux règles suivantes :
- Une interface possède un IID (identifiant d’interface) unique.1
- Une interface est immuable. Une fois créée et publiée, aucune partie de sa définition ne doit jamais être modifiée.1
- Ajouter ou supprimer une méthode, ou changer sa sémantique, ne signifie pas créer « une nouvelle version de l’ancienne interface » — cela signifie créer une nouvelle interface avec un IID différent.4
La raison de cette rigueur est que la substance réelle d’une interface COM est une disposition binaire — la vtable, une table de pointeurs de fonctions. Un client C++ ou VB6 fige à la compilation le fait que « le troisième emplacement est GetName » — une position. Insérer une méthode après publication, et l’ancien client, sans la moindre erreur, appellera une méthode différente. C’est précisément pour cela que COM a effacé de la spécification l’opération même de « modifier » une interface, et a fourni à la place la procédure d’extension suivante.
// v1 : déjà publiée. Ne plus jamais la modifier
[object, uuid(1111....)]
interface ICalc : IUnknown {
HRESULT Add([in] long a, [in] long b, [out, retval] long* result);
};
// v2 : une nouvelle interface avec un nouvel IID. Étend en héritant de ICalc
[object, uuid(2222....)]
interface ICalc2 : ICalc {
HRESULT AddChecked([in] long a, [in] long b, [out, retval] long* result);
};
La classe d’implémentation (la coclass) implémente à la fois ICalc et ICalc2 ; les anciens clients continuent d’utiliser ICalc exactement comme avant, et les nouveaux clients demandent ICalc2 via QueryInterface. La théorie officielle du versionnage pour RPC/COM le formule de la même façon : une nouvelle interface qui hérite de l’ancienne équivaut à une montée de version mineure, tandis que changer une méthode ou un type existant exige une interface entièrement nouvelle et n’héritant de rien — l’équivalent d’une montée de version majeure.7 Ce qui fait fonctionner ce mécanisme, c’est que QueryInterface permet à un appelant de vérifier en toute sécurité, à l’exécution, quelles interfaces sont prises en charge. L’élégance de conception derrière ce mécanisme est ce que nous avons approfondi dans « Qu’est-ce que COM ».
4.1 La répartition des rôles entre CLSID, ProgID et IID
Pour raisonner sur le versionnage COM, il est utile de séparer les rôles de trois types d’identifiants.8
- L’IID identifie une interface (un contrat). Tout changement du contrat implique toujours un nouvel IID.
- Le CLSID identifie la classe d’implémentation. Vous êtes libre de remplacer l’implémentation en gardant le même CLSID, tant que vous respectez le contrat de chaque interface publiée.
- Le ProgID est un alias lisible par un humain (
KomuraSoft.Calc.1) utilisé pour retrouver le CLSID correspondant dans le registre. Il est d’usage de conserver à la fois un ProgID numéroté par version et un ProgID indépendant de la version (KomuraSoft.Calc) qui pointe toujours vers la dernière version, ce dernier étant associé à la version courante viaCurVer.8
Autrement dit, « mettre à jour l’implémentation » relève du monde du CLSID et du ProgID, tandis que « changer le contrat » relève du monde de l’IID — il ne faut pas mélanger les deux. Si vous préférez éviter complètement l’enregistrement dans le registre, cette option est traitée dans « Qu’est-ce que Reg-Free COM ? »
4.2 Pourquoi les clients VB6/VBA sont particulièrement fragiles
Lorsque VB6 ou VBA utilise un composant COM via une référence de projet (liaison anticipée), il lit la bibliothèque de types à la compilation pour résoudre les appels. La liaison anticipée est la forme recommandée — elle apporte IntelliSense, la vérification de type et une exécution plus rapide9 — mais son prix est qu’elle se lie étroitement à la disposition de la bibliothèque de types. Non seulement un changement de la vtable d’une interface pose problème, mais même un changement portant uniquement sur les définitions de la bibliothèque de types peut se manifester par « la référence était cassée à l’ouverture du projet » ou par une erreur d’exécution 430/438.
Cela signifie que les composants ayant VB6, VBA ou des macros Excel comme appelants doivent appliquer la règle d’immuabilité des interfaces avec la plus grande rigueur. Les bibliothèques de types portent aussi une version (majeure.mineure), qu’il faut incrémenter chaque fois que le contrat s’étend. La génération d’une bibliothèque de types lorsqu’on publie du code .NET typé vers VBA est traitée dans « Appeler une DLL .NET 8 typée depuis VBA — dscom et TLB ». Les clients à liaison tardive qui n’utilisent que CreateObject, en revanche, résolvent par nom et résistent donc bien aux changements de disposition, mais restent tout aussi exposés à un changement de sens d’une méthode (compatibilité comportementale).
5. La compatibilité des assemblies .NET — juger mécaniquement grâce aux règles officielles
.NET publie un ensemble de « règles de changement pour la compatibilité » que Microsoft utilise lui-même pour développer les bibliothèques .NET, classant chaque type de changement comme autorisé (✔️), interdit (❌) ou à évaluer au cas par cas (❓).2 La documentation précise explicitement que vous pouvez l’adopter telle quelle comme critère pour vos propres bibliothèques ; en voici les principales lignes.
| Changement apporté à une API publique | Verdict | Remarques |
|---|---|---|
| Ajout d’une méthode, d’un type ou d’un membre | ✔️ Sûr en principe | Attention toutefois à un ajout qui change la résolution de surcharge existante. Ajouter un champ d’instance à une structure publique fait exception : cela change la taille et la disposition, cassant l’interopérabilité et les consommateurs en code unsafe |
| Suppression ou renommage d’un type ou d’un membre public | ❌ Cassant | Casse à l’exécution avec une MissingMethodException ou similaire |
| Changement de signature (ajout/suppression/réordonnancement/changement de type de paramètres, ou du type de retour) | ❌ Cassant | Casse à la fois la compatibilité binaire et source |
| Renommage d’un paramètre | ❌ Cassant | Casse les arguments nommés en C# et la liaison tardive en VB. Facile à négliger |
| Ajout de virtual à un membre | ❌ Cassant | Un piège classique qui paraît sûr parce que c’est « juste un ajout ». Peut provoquer une incohérence entre les instructions IL call/callvirt |
| Suppression de virtual, ou passage d’un membre virtuel en abstract | ❌ Cassant | Casse les redéfinitions (override) dans les classes dérivées |
| Ajout d’un membre abstrait à un type public non scellé | ❌ Cassant | Les classes dérivées existantes n’en ont pas d’implémentation |
| Scellement (sealed) d’un type | ❌ Cassant | Les classes dérivées existantes cessent de compiler |
| Ajout d’un membre à une interface | ❓ À évaluer | Peut être atténué par une implémentation d’interface par défaut (DIM), mais sous conditions de langage/runtime |
| Changement de la valeur d’une constante ou d’une valeur d’énumération, renommage/suppression d’un membre d’énumération | ❌ Cassant | La valeur est figée chez l’appelant à la compilation |
| Modifier le code pour lever une exception plus dérivée | ✔️ Autorisé | Les blocs catch existants continuent de fonctionner |
| Lever un nouveau type d’exception sur un chemin de code existant | ❌ Cassant | La lever uniquement pour une nouvelle valeur de paramètre est acceptable |
Ce n’est pas aussi simple que le « les interfaces sont immuables » de COM, mais la philosophie sous-jacente est la même : une API publique est un contrat ; on peut ajouter au contrat, mais on ne peut pas modifier le contrat existant. Et le fait que des changements comme l’ajout d’une méthode virtuelle ou le renommage d’un paramètre — le genre de chose qui semble sûre au premier regard — soient classés comme cassants est précisément la raison pour laquelle il faut juger sur la base du tableau plutôt qu’à l’instinct.
5.1 Noms forts et trois numéros de version
Les assemblies .NET portent plusieurs numéros de version, chacun avec un rôle différent.10
- AssemblyVersion : la seule version que le runtime utilise pour identifier et charger un assembly. Pour les assemblies à nom fort, le CLR de .NET Framework exige une correspondance exacte, donc chaque incrémentation force les appelants à ajouter une redirection de liaison (binding redirect) (.NET / .NET Core, en revanche, accepte automatiquement une version supérieure). Pour réduire le nombre de redirections, les recommandations officielles suggèrent de ne refléter que la version majeure dans AssemblyVersion.
- FileVersion (AssemblyFileVersion) : visible uniquement dans les propriétés de fichier de l’Explorateur, sans effet sur le comportement à l’exécution. C’est l’emplacement recommandé pour y inscrire un numéro de build de CI.
- InformationalVersion : une chaîne libre destinée aux humains. Utilisée pour enregistrer une version au format semver du paquet ou le hash de commit source.
En pratique, l’organisation viable en trois niveaux consiste donc à déclarer la compatibilité via la version du paquet/produit (semver), ne refléter que la version majeure dans AssemblyVersion, et suivre les builds avec FileVersion.
6. Comment attribuer les numéros de version — le semver ne fonctionne qu’avec une « définition »
Le versionnage sémantique (semver) tient en trois lignes : incrémenter MAJOR pour un changement incompatible, MINOR pour un ajout de fonctionnalité rétrocompatible, et PATCH pour une correction de bug rétrocompatible.5
Ce qu’on néglige souvent, c’est que la toute première exigence de la spécification semver est que tout logiciel utilisant semver doit déclarer une API publique.5 Sans déclarer ce qui constitue l’API publique, il n’existe aucun critère pour définir un « changement incompatible », et la décision d’incrémenter la version majeure dépend alors de l’humeur de la personne en charge ce jour-là. Dans la plupart des équipes où le semver ne fonctionne pas réellement, le problème ne vient pas de la façon dont les numéros sont attribués, mais du fait que cette déclaration a été omise.
Un mode de fonctionnement réaliste pour une DLL distribuée en interne se présente ainsi :
- Déclarer le périmètre de l’API publique — pour une DLL native, ce sont les fonctions exportées et les en-têtes publics ; pour COM, l’IDL/la bibliothèque de types ; pour .NET, les types et membres publics. Indiquer explicitement que « tout ce qui est en dehors de ce périmètre est un détail d’implémentation interne pouvant changer sans préavis ».
- Adopter une définition du changement cassant — placer dans votre dépôt, en tant que « notre propre définition », les tableaux de décision des chapitres 3 et 5 de cet article, ainsi que les règles de changement de .NET2.
- Automatiser la vérification — pour .NET, des outils comme Package Validation et ApiCompat permettent de vérifier mécaniquement la compatibilité binaire par rapport à la version précédente.11 Cela élimine le jugement « probablement bon » lors de la revue.
- Ajouter une rubrique compatibilité aux notes de version — préciser à chaque fois, parmi trois valeurs : recompilation non nécessaire / recompilation recommandée / changement cassant inclus. C’est un mécanisme qui met par écrit, avant même qu’on ne la pose, la réponse à la question « peut-on juste remplacer la DLL ? ».
7. La procédure à suivre lorsqu’il devient inévitable de casser la compatibilité
Lorsqu’un changement que le tableau de décision qualifie de cassant s’avère malgré tout inévitable, procédez en offrant les deux versions en parallèle plutôt qu’en remplaçant l’une par l’autre.
- Offrir l’ancienne et la nouvelle version en parallèle — pour COM, cela consiste à ajouter
IFoo2tout en laissantIFooen place (chapitre 4). Pour une DLL native, ajouter une nouvelle fonction (FooEx) ou faire coexister une nouvelle DLL portant un autre nom avec l’ancienne. Pour .NET, publier le changement sous forme d’un nouveau paquet avec une version majeure incrémentée, et ne maintenir l’ancienne version majeure que pour des corrections de bugs. - Prévoir une période de dépréciation — en .NET, l’attribut
[Obsolete]peut déclencher un avertissement à la compilation. Pour le code natif/COM, le déclarer via des commentaires d’en-tête et les notes de version, en précisant explicitement la date de retrait prévue. L’essentiel est de s’engager sur une date réelle, pas sur un vague « on la retirera un jour ». - Faire l’inventaire des appelants — dresser la liste de qui appelle encore l’ancienne API, en recherchant dans le code source interne, en consultant les registres de distribution des installateurs et, pour COM, en vérifiant l’utilisation des références dans le registre. Si vous trouvez un binaire que vous ne pouvez pas recompiler — un outil créé par quelqu’un qui a quitté l’entreprise, ou une application tierce —, prolongez la durée de vie de l’ancienne API pour ce seul cas, ou construisez un pont avec un wrapper.
- Supprimer l’ancienne API — uniquement après que l’inventaire a confirmé qu’il ne reste plus aucun appelant, la supprimer et incrémenter la version majeure.
Cette procédure a un coût. C’est justement pour cela que, paradoxalement, concevoir dès la toute première publication une API réduite en gardant le tableau de décision à l’esprit — puisque tout ce qu’on ne publie jamais n’entraîne aucune obligation de compatibilité — constitue la plus grande protection de compatibilité qui soit.
8. Résumé
- Pensez la compatibilité en trois niveaux : binaire, source et comportemental. La compatibilité comportementale peut se casser même sans recompilation.3
- Pour les DLL natives, l’ajout est sûr ; modifier un export, une signature ou la disposition d’une structure existants est cassant. Dotez les structures d’un champ
cbSizepour laisser de la place à l’extension.6 - Une interface COM est immuable une fois publiée. Ajoutez les changements sous forme d’une nouvelle interface avec un nouvel IID (IFoo2) et laissez les appelants les distinguer via
QueryInterface.147 Appliquez cette règle avec une rigueur particulière dès que des clients VB6/VBA à liaison anticipée sont impliqués. - Pour .NET, vous pouvez juger mécaniquement grâce aux règles officielles de changement pour la compatibilité. Méfiez-vous des changements « apparemment sûrs » — ajout de virtual, renommage de paramètre, scellement de type — qui sont classés comme cassants.2
- Une organisation réaliste en trois niveaux : AssemblyVersion ne porte que la version majeure, FileVersion suit le build, et le semver déclare la compatibilité.10
- Le semver ne fonctionne qu’une fois qu’on a déclaré une API publique et une définition du changement cassant.5 Adoptez le tableau de décision comme cette définition et vérifiez-le automatiquement avec des outils comme Package Validation.11
- Quand vous devez casser quelque chose, procédez par offre en parallèle → période de dépréciation → inventaire → suppression. Ne jamais remplacer d’un coup est ce qui protège le vieil EXE encore en service chez un client.
Articles connexes
- Qu’est-ce que COM / ActiveX / OCX ? — Différences et relations expliquées ensemble
- Qu’est-ce que COM — Pourquoi la conception de COM sous Windows reste élégante aujourd’hui
- Appeler une DLL .NET 8 typée depuis VBA — dscom et TLB
- Comment fonctionne la résolution des noms de DLL sous Windows — ordre de recherche et SxS
- Qu’est-ce que Reg-Free COM ? — Utiliser COM sans enregistrement
- Comment appeler une DLL C# Native AOT depuis C/C++
Domaines de conseil associés
Komura Software LLC prend en charge la conception de la compatibilité des DLL, des composants COM et des bibliothèques .NET référencés par d’autres systèmes ; l’inventaire des API publiques et la mise en place d’une politique de versionnage ; ainsi que la conception et l’implémentation d’extensions — le schéma IFoo2, l’offre en parallèle — qui ne cassent pas les clients existants.
- Réutilisation de l’existant et accompagnement à la migration
- Modification et maintenance de logiciels Windows existants
- Conseil technique et revue de conception
- Contact
Références
-
Microsoft Learn, Interface Design Rules. Sur le fait que les interfaces des objets COM doivent posséder un IID unique, et qu’aucune partie de la définition d’une interface ne peut être modifiée une fois créée et publiée (immuabilité). ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Change rules for compatibility (.NET). Sur la classification des changements d’API .NET en autorisés, interdits ou à évaluer ; sur le fait que la suppression ou le renommage de types/membres publics, les changements de signature, le renommage de paramètres, l’ajout/la suppression de virtual, le scellement et le changement de valeurs de constantes/énumérations sont tous classés comme interdits (cassants) ; que l’ajout d’un membre à une interface est à évaluer ; et que les auteurs de bibliothèques peuvent utiliser ces règles comme critère d’évaluation pour leur propre bibliothèque. ↩ ↩2 ↩3 ↩4 ↩5
-
Microsoft Learn, Breaking changes (.NET library guidance). Sur la classification des changements cassants en cassants pour la source, le comportement ou le binaire, et sur le fait qu’un changement cassant au niveau binaire fait échouer à l’exécution, avec une MissingMethodException ou similaire, un assembly compilé contre l’ancienne version. ↩ ↩2 ↩3
-
Microsoft Learn, Interface Pointers and Interfaces. Sur l’immuabilité des interfaces COM, et sur le fait qu’ajouter ou supprimer une méthode, ou changer sa sémantique, revient à créer une nouvelle interface plutôt qu’une nouvelle version de l’ancienne, l’IID définissant de façon unique le contrat. ↩ ↩2 ↩3
-
semver.org, Semantic Versioning 2.0.0. Sur l’incrémentation de MAJOR pour les changements d’API incompatibles, de MINOR pour les ajouts de fonctionnalités rétrocompatibles, et de PATCH pour les corrections de bugs rétrocompatibles ; sur le fait que tout logiciel utilisant semver doit déclarer une API publique ; et qu’un changement incompatible avec l’ancienne version de l’API publique doit toujours entraîner une incrémentation de la version MAJOR. ↩ ↩2 ↩3 ↩4
-
Microsoft Learn, NOTIFYICONDATAW structure (shellapi.h). Sur la définition de la taille de la structure dans le membre cbSize, l’extension de la structure au fil des générations, et le fait que définir la valeur appropriée de cbSize permet de rester compatible avec d’anciennes versions de Shell32.dll. ↩ ↩2
-
Microsoft Learn, The Versioning Theory for RPC and COM. Sur le fait que créer une nouvelle interface est le meilleur moyen d’étendre les fonctionnalités en COM, qu’une nouvelle interface héritant de l’ancienne équivaut à une montée de version mineure, que les changements portant sur des méthodes ou types existants exigent une interface entièrement nouvelle n’héritant de rien, et que QueryInterface permet aux appelants de vérifier ce qui est pris en charge. ↩ ↩2
-
Microsoft Learn, COM Registry Keys. Sur le fait que le CLSID est le GUID identifiant une classe COM, que le ProgID est une chaîne lisible associée à un CLSID sans garantie d’unicité, que le ProgID indépendant de la version est associé à la version la plus récente de la classe via CurVer, et que la clé Interface enregistre les IID. ↩ ↩2
-
Microsoft Learn, OLE programmatic identifiers, late binding, and early binding (Project). Sur le fait que la liaison anticipée via une référence de projet est recommandée en VBA, que la liaison tardive (CreateObject/ProgID) ne montre pas les membres lors de l’écriture du code et s’exécute plus lentement, et que la liaison anticipée exige une référence à la bibliothèque d’objets cible. ↩
-
Microsoft Learn, Versioning (.NET library guidance). Sur le fait qu’AssemblyVersion est utilisé par le runtime pour le chargement et exige une correspondance exacte pour les assemblies à nom fort sous .NET Framework, la suggestion de n’inclure que la version majeure dans AssemblyVersion, le fait que FileVersion sert à l’affichage sous Windows sans effet sur le comportement à l’exécution, qu’InformationalVersion enregistre des informations de version supplémentaires, et que la version semver 2.0.0 est recommandée pour les versions de paquets NuGet. ↩ ↩2
-
Microsoft Learn, NuGet package compatibility rules. Sur la nécessité d’éviter les changements cassants au niveau binaire, sur le fait que les outils Package Validation et ApiCompat peuvent détecter automatiquement la compatibilité par rapport à une version de référence, et sur le fait qu’AssemblyVersion ne doit jamais diminuer entre deux versions. ↩ ↩2
Articles associés
Articles récents partageant les mêmes étiquettes, pour approfondir des sujets proches.
Jusqu'à quand les applications VB6 continueront-elles de fonctionner ? — état du support du runtime et démarche concrète vers une migration .NET
Jusqu'à quand les applications VB6 continueront-elles de fonctionner ? Cet article clarifie l'asymétrie entre la politique de support du ...
Comment choisir la communication inter-processus sous Windows ── Tableau de décision : tubes nommés / TCP / gRPC / mémoire partagée / COM
Comment choisir le moyen de faire communiquer des applications Windows entre elles ? Cet article organise les tubes nommés, le TCP local,...
Versionner le schéma de base de données d'une application métier — pratiques de migration pour éviter que « chaque client ait une base différente »
Guide pratique pour versionner le schéma de base de données d'applications métier dont les bases sont dispersées chez chaque client. Impl...
CI/CD pratique pour les applications WinForms / WPF — Automatiser du build à la signature et à la distribution avec GitHub Actions
Guide pratique pour mettre en place le CI/CD des applications WinForms / WPF avec GitHub Actions. Couvre un YAML minimal de build+tests s...
Modifier en toute sécurité une application métier legacy sans tests — Tests de caractérisation et refactorisation en pratique
Pour modifier en toute sécurité une application métier sans tests, cet article explique, avec des exemples en C#, la procédure du test de...
Sujets associés
Ces pages replacent le sujet dans un contexte plus large de services et de décisions.
Thèmes techniques Windows
Portail des sujets sur le développement Windows, l'analyse des incidents et la valorisation des actifs existants.
Migration ActiveX
Choisir de conserver, encapsuler ou remplacer des composants COM / ActiveX / OCX.
Services liés à ce sujet
Cet article est directement lié aux services suivants.
Développement d'applications Windows
Applications métier, intégration d'équipements et outils de communication, des besoins au développement.
Réutilisation et migration d'actifs existants
Réutilisation et migration d'actifs COM / ActiveX / OCX et de dépendances 32 ou 64 bits.
Questions fréquentes
Questions souvent posées lors d’une consultation sur le sujet de cet article.
- Si je me contente d'ajouter une fonction à une DLL, faut-il recompiler les appelants ?
- En principe, si vous vous contentez d'ajouter une fonction exportée, les appelants existants continuent de fonctionner tels quels, car la résolution des imports reste valable tant que vous ne changez pas le nom, la signature, la convention d'appel ou l'ordinal d'export d'une fonction existante. Cela dit, si vous ajoutez un membre à une structure que l'appelant alloue et transmet, ou si vous changez la signification de la valeur de retour ou du code d'erreur d'une fonction existante, la compatibilité comportementale peut se casser même si tout fonctionne sans recompilation. La règle de base est : « ajouter une fonction est sûr, modifier une signature existante est cassant ».
- Pourquoi est-il interdit d'ajouter une méthode à une interface COM après coup ?
- Parce que, selon la spécification COM, une interface est immuable une fois publiée. Une interface est le contrat d'une disposition binaire — la vtable, une suite de pointeurs de fonctions — et insérer, supprimer ou réordonner des méthodes fait qu'un ancien binaire appelle une méthode différente à la position qu'il a figée à la compilation. Ajouter à la fin ne déplace pas les emplacements existants, mais crée un nouveau risque : un nouveau client peut récupérer une instance d'un ancien composant en supposant que la méthode ajoutée est présente, puis appeler un emplacement qui n'existe pas réellement. C'est pourquoi même un ajout en fin de liste, sous le même IID, n'est pas autorisé. Si vous voulez ajouter des fonctionnalités, ajoutez une nouvelle interface portant un nouvel IID (IFoo2) et laissez l'ancienne IFoo inchangée. Les appelants peuvent alors déterminer en toute sécurité, à l'exécution via QueryInterface, s'ils dialoguent avec l'ancienne ou la nouvelle interface.
- Comment différencier l'usage d'AssemblyVersion, de FileVersion et d'InformationalVersion en .NET ?
- AssemblyVersion est la seule version que le runtime utilise pour identifier et charger un assembly ; pour les assemblies à nom fort, .NET Framework exige une correspondance exacte, donc chaque incrémentation nécessite une redirection de liaison côté appelant. C'est pourquoi les recommandations officielles suggèrent de ne refléter que la version majeure. FileVersion ne s'affiche que dans les propriétés de l'Explorateur et n'a aucun effet sur le comportement à l'exécution ; elle convient bien pour y inscrire par exemple un numéro de build de CI. InformationalVersion est une chaîne libre destinée aux humains, utilisée pour enregistrer une version au format semver ou un hash de commit.
- L'adoption du versionnage sémantique (semver) résout-elle à elle seule les problèmes de compatibilité ?
- Non, le semver seul ne résout pas le problème. Le semver est la convention selon laquelle on incrémente la version majeure lors d'un changement non rétrocompatible, mais il exige au préalable de déclarer ce qui constitue l'API publique et ce qui constitue un changement cassant. Attribuer des numéros de version sans cette définition fait que le jugement varie d'une personne à l'autre et le système cesse de fonctionner. Le semver ne prend tout son sens qu'à partir du moment où vous adoptez, comme « votre propre définition du changement cassant », un critère tel que le tableau de décision de cet article pour une DLL native, ou les règles de changement pour la compatibilité de Microsoft pour .NET, et que vous l'intégrez à votre processus de publication.
Profil de l’auteur
Page de présentation de l’auteur de l’article.
Go Komura
Représentant de KomuraSoft LLC
Spécialisé dans le développement de logiciels Windows, le conseil technique et l’analyse de pannes, notamment pour les systèmes existants et les incidents difficiles à reproduire.
Liens publics