aujourd’hui on va voir un composant fondamental de l’architecture Windows : COM, pour Component Object Model.
C’est un sujet extrêmement important, parce qu’en pratique beaucoup de développeurs utilisent déjà COM sans toujours s’en rendre compte. Dès que tu touches à certaines API Windows un peu sérieuses, tu finis très souvent par tomber sur du COM, directement ou indirectement.
On retrouve COM dans énormément de domaines :
- DirectX
- Media Foundation
- Windows Shell
- WMI
- extensions Explorer
- certaines APIs d’administration
- beaucoup de technologies Microsoft historiques et modernes
Dans ce cours on va donc poser une base propre, claire et vraiment utile, dans un style développement système. Le but n’est pas juste de retenir trois noms, mais de comprendre comment COM fonctionne vraiment au niveau binaire et pourquoi il a été si important dans l’écosystème Windows.
1) C’est quoi COM
COM signifie Component Object Model.
COM est un modèle de composants binaires.
Le mot important ici, c’est binaire.
Cela veut dire que COM ne définit pas seulement une manière “logique” d’écrire du code objet. Il définit surtout une manière stable au niveau binaire pour que des composants puissent communiquer entre eux, meme s’ils ont été compilés séparément, développés séparément, ou utilisés depuis des langages différents.
Autrement dit, COM sert à faire dialoguer des composants logiciels selon un contrat binaire très précis.
L’idée générale est la suivante :
- une application n’est plus forcément un gros bloc unique
- elle peut être composée de plusieurs composants
- chaque composant expose des interfaces
- le client consomme ces interfaces sans connaitre l’implémentation réelle
2) Pourquoi COM a existé : le problème des applications monolithiques
Avant de comprendre COM, il faut comprendre le problème qu’il voulait résoudre.
Dans un modèle monolithique classique, on a souvent :
- un seul gros exécutable
- peu de séparation nette entre les parties
- faible réutilisation
- fort couplage entre les composants internes
- modifier une petite partie peut imposer de recompiler ou redistribuer tout le programme
- réutiliser une fonctionnalité dans un autre logiciel devient pénible
- faire dialoguer proprement deux logiciels ou deux modules devient difficile
- le remplacement d’un composant
- la réutilisation d’un composant
- la séparation claire entre interface publique et implémentation privée
3) L’idée centrale : programmer contre une interface, pas contre l’objet concret
En COM, le client ne travaille jamais “directement” avec l’objet concret comme dans une vision naïve de la programmation objet.
Il travaille avec une interface.
Le schéma mental de base est :
Code: Select all
objet COM
↓
interface
↓
méthodes exposées
- un contrat
- une liste de fonctions
- un type d’interface
- la structure interne de l’objet
- ses données privées
- son implémentation réelle
- sa classe concrète au sens C++ habituel
4) Une interface COM : ce que c’est réellement
Dans COM, une interface est une table de méthodes accessible via un pointeur.
D’un point de vue conceptuel :
- une interface expose des fonctions
- elle ne contient pas de données publiques
- elle n’expose pas l’implémentation
- elle décrit uniquement ce que le composant sait faire
Code: Select all
struct IX
{
virtual void Fx() = 0;
};
Code: Select all
COM n’est pas “juste du C++”
Le C++ est juste un moyen pratique de représenter ce contrat quand on écrit du code C++.
Dans un autre langage, ou meme en C pur, le modèle reste le meme au niveau binaire.
5) Pourquoi COM est un modèle binaire et pas seulement objet
C’est probablement le point le plus important à comprendre.
Si COM n’était qu’un simple style de classes C++, il ne servirait pas à grand-chose au niveau système. Ce qui le rend puissant, c’est qu’il fixe une représentation binaire suffisamment stable pour permettre l’interopérabilité.
Cela implique notamment :
- les interfaces sont identifiées de manière unique
- les méthodes sont accessibles selon un ordre binaire déterminé
- le client n’a pas besoin de connaitre la classe interne
- des composants compilés séparément peuvent coopérer
- les DLL réutilisables
- les composants système Windows
- les objets Shell
- l’automation
- les technologies Microsoft construites au-dessus
Dans COM, les choses ne sont pas identifiées par de simples noms texte.
On utilise des identifiants globaux uniques, les GUID.
Types importants à connaitre :
- GUID
- IID
- CLSID
- une interface possède un IID : Interface Identifier
- une classe COM possède un CLSID : Class Identifier
- pas de collision de noms
- identification binaire claire
- résolution stable des interfaces et des classes
7) Accès à un objet COM
Un objet COM n’est jamais utilisé directement “à mains nues”.
On obtient toujours un pointeur vers une interface.
Le modèle mental à retenir est :
Code: Select all
objet COM
↓
pointeur d’interface
↓
appel de méthodes via cette interface
C’est extrêmement important, parce que cela permet à un même objet de présenter plusieurs interfaces différentes selon les besoins.
8) Toutes les interfaces COM héritent de IUnknown
En COM, toutes les interfaces dérivent de l’interface de base :
Code: Select all
IUnknown
Elle définit trois fonctions fondamentales :
- QueryInterface
- AddRef
- Release
Si tu comprends vraiment leur rôle, tu comprends déjà une grosse partie du fonctionnement réel de COM.
9) Pourquoi IUnknown est si important
IUnknown sert à trois choses fondamentales :
- naviguer entre interfaces
- gérer la durée de vie de l’objet
- fournir une base commune à tout objet COM
- QueryInterface sert à demander une autre interface
- AddRef augmente le compteur de références
- Release diminue le compteur de références
10) Signature classique de IUnknown
En C++, on voit souvent quelque chose comme :
Code: Select all
struct IUnknown
{
virtual HRESULT QueryInterface(REFIID riid, void** ppvObject) = 0;
virtual ULONG AddRef() = 0;
virtual ULONG Release() = 0;
};
- HRESULT
- ULONG
- REFIID
- IID
- CLSID
- COM utilise massivement HRESULT pour exprimer succès ou erreur
- la gestion mémoire classique passe par un reference counting
QueryInterface est la méthode qui permet de demander à un objet :
Code: Select all
“exposes-tu telle interface ?”
Code: Select all
obj->QueryInterface(IID_IX, (void**)&px);
- il retourne S_OK
- il place dans le pointeur de sortie l’adresse de l’interface demandée
- il incrémente aussi le ref count sur l’interface retournée
Code: Select all
E_NOINTERFACE
- on ne caste pas brutalement un objet COM comme on le ferait parfois en C++
- on demande explicitement une interface
QueryInterface permet la navigation entre interfaces.
Exemple mental :
Code: Select all
objet COM
├── IX
├── IY
└── IZ
Cela donne un modèle très souple :
- le client commence avec une interface
- il découvre ensuite les autres interfaces disponibles
- il ne dépend pas d’une classe concrète
13) Les règles fondamentales de QueryInterface
QueryInterface doit respecter des règles très précises.
Reflexive
Une interface doit toujours pouvoir se retourner elle-même.
Autrement dit :
Code: Select all
si tu possèdes IX
alors QueryInterface(IID_IX) doit réussir
Si IX permet d’obtenir IY, alors IY doit permettre d’obtenir IX.
Transitive
Si IX permet d’obtenir IY, et IY permet d’obtenir IZ, alors IX doit permettre d’obtenir IZ.
Ces règles sont très importantes, parce qu’elles donnent une cohérence forte au graphe des interfaces d’un objet COM.
Ce n’est pas du “détail de théorie”.
C’est un vrai contrat de cohérence de l’objet.
14) AddRef : augmentation du compteur de références
AddRef sert à augmenter le compteur de références de l’objet.
Exemple :
Code: Select all
p->AddRef();
- tant qu’il existe des clients qui utilisent l’objet, le compteur reste > 0
- chaque nouvelle référence valide doit être comptée
Le but est clair :
Code: Select all
l’objet reste vivant tant qu’il est encore référencé
Release fait l’opération inverse :
Code: Select all
p->Release();
Quand il atteint zéro :
- l’objet se détruit lui-meme
- sa mémoire est libérée
- il cesse d’exister
Très important :
- un oubli de Release produit une fuite
- un Release en trop peut détruire l’objet trop tôt
16) COM et gestion mémoire : différent du C++ classique
Dans du C++ classique, on pense souvent en termes de :
- new / delete
- constructeur / destructeur
- smart pointers du langage
- AddRef
- Release
C’est pour ça que les wrappers modernes utilisent souvent des smart pointers COM spécialisés, mais conceptuellement, la base reste toujours AddRef / Release.
17) Exemple mental simple du ref counting
Imaginons :
- un objet est créé avec refcount = 1
- un client obtient une autre interface → refcount = 2
- le premier client fait Release → refcount = 1
- le second client fait Release → refcount = 0
- l’objet est détruit
18) Un objet COM peut exposer plusieurs interfaces
C’est une idée centrale de COM.
Un même objet peut exposer plusieurs vues fonctionnelles, chacune correspondant à une interface différente.
Exemple :
Code: Select all
objet
├── IX
├── IY
└── IZ
Pourquoi c’est puissant :
- on sépare mieux les responsabilités
- on cache certaines capacités à certains clients
- on garde un modèle modulaire
- on fait évoluer un composant en ajoutant des interfaces
C’est une distinction absolument essentielle.
Quand tu manipules un pointeur COM, tu manipules :
Code: Select all
un pointeur d’interface
Cela explique pourquoi :
- tu dois passer par QueryInterface
- tu n’as pas accès à l’implémentation concrète
- deux pointeurs d’interface différents peuvent référencer le même objet sous des vues différentes
20) HRESULT : le modèle d’erreur COM
COM utilise massivement HRESULT.
C’est le type de retour standard pour de nombreuses méthodes COM.
Valeurs classiques à connaitre :
- S_OK
- S_FALSE
- E_NOINTERFACE
- E_POINTER
- E_FAIL
- E_OUTOFMEMORY
- E_INVALIDARG
- E_ACCESSDENIED
- CLASS_E_CLASSNOTAVAILABLE
- REGDB_E_CLASSNOTREG
- COM n’utilise pas le modèle d’exception C++ comme base du protocole
- il repose sur des codes de retour explicites
- il faut toujours vérifier les HRESULT
- SUCCEEDED(hr)
- FAILED(hr)
Code: Select all
HRESULT hr = obj->QueryInterface(IID_IX, (void**)&px);
if (FAILED(hr))
{
// gestion d'erreur
}
Même si on reste ici sur les bases, il faut déjà comprendre une chose :
Code: Select all
une interface COM est essentiellement une table de fonctions
C’est précisément ce qui rend COM :
- compatible binaire
- interopérable entre composants
- lisible aussi en reverse engineering
22) Création d’objet COM : aperçu du problème
À ce stade, une question logique arrive :
Code: Select all
comment obtient-on le tout premier pointeur d’interface ?
On n’entre pas encore à fond dedans ici, mais il faut déjà retenir que :
- le client ne construit pas directement l’objet avec new
- la création passe par l’infrastructure COM
- l’objet est identifié par un CLSID
- l’interface initiale demandée est identifiée par un IID
23) In-process, out-of-process et localité du composant
Même pour un cours de base, il est bon de savoir qu’un objet COM n’est pas forcément dans le même module ou le même processus que son client.
Un composant COM peut être :
- in-process : typiquement une DLL chargée dans le processus client
- local server : EXE COM dans un autre processus local
- remote : selon les technologies distribuées plus avancées
24) Pourquoi COM a autant compté dans Windows
COM a servi de base ou de sous-couche à énormément de technologies Microsoft.
Parmi les domaines où tu rencontres COM ou ses dérivés logiques :
- Shell Windows
- WMI
- DirectX classique
- Media Foundation
- certaines APIs audio / vidéo
- Automation
- beaucoup de composants internes historiques de Windows
C’est comprendre une partie très profonde de l’écosystème Windows.
25) Ce qu’il faut retenir sur les bases de COM
Si on résume les idées les plus importantes du cours :
- COM est un modèle de composants binaires
- il permet à des composants logiciels de dialoguer via des interfaces stables
- le client ne voit jamais directement l’implémentation
- tout passe par des pointeurs d’interface
- toutes les interfaces COM héritent de IUnknown
- IUnknown expose QueryInterface, AddRef et Release
- QueryInterface permet de naviguer entre interfaces
- AddRef et Release contrôlent la durée de vie de l’objet
- les interfaces sont identifiées par des IID
- les classes COM sont identifiées par des CLSID
Pour avoir déjà une bonne base, il faut reconnaitre rapidement :
- IUnknown
- HRESULT
- GUID
- IID
- CLSID
- REFIID
- REFCLSID
- ULONG
- S_OK
- S_FALSE
- E_NOINTERFACE
- SUCCEEDED
- FAILED
- QueryInterface
- AddRef
- Release
- CoInitializeEx
- CoUninitialize
- CoCreateInstance
- CoGetClassObject
- IClassFactory
27) Erreurs classiques des débutants en COM
Parmi les erreurs fréquentes :
- croire qu’un objet COM se manipule comme un objet C++ ordinaire
- oublier qu’on travaille via des interfaces
- ne pas comprendre le rôle exact de QueryInterface
- oublier un Release
- faire un Release de trop
- penser que COM se résume à “des GUID bizarres”
- ne pas vérifier les HRESULT
- confondre classe COM et interface COM
28) Conclusion
COM est l’un des grands fondements historiques et techniques de Windows. Ce n’est pas seulement un vieux mécanisme de composants : c’est un modèle binaire extrêmement important pour comprendre comment beaucoup de technologies Windows exposent leurs objets, leurs services, et leurs capacités.
Si tu comprends déjà vraiment :
- ce qu’est une interface COM
- pourquoi tout passe par IUnknown
- à quoi servent QueryInterface, AddRef et Release
- la différence entre objet concret et pointeur d’interface
- la logique des IID et CLSID
Et c’est exactement ce cœur qui permet ensuite d’aborder sans se noyer :
- les class factories
- CoCreateInstance
- les serveurs COM
- l’initialisation COM
- le threading COM
- les technologies Windows construites au-dessus
