| Historique des versions | ||
|---|---|---|
| Version 1.0.6 | 5 décembre 2000 | Revised by: CC |
| Corrections orthographiques. Corrections et précisions sur les abonnements sur les dispinterfaces événementielles. | ||
| Version 1.0.5 | 1 octobre 2000 | Revised by: CC |
| Corrections typographiques. Précisions sur la distribution des composants. | ||
| Version 1.0.4 | 11 septembre 2000 | Revised by: CC |
| Passage au format SGML. Corrections mineures. | ||
| Version 0.03 | 25 juillet 2000 | Revised by: CC |
| Précision sur le modèle de threading « Both ». | ||
| Version 0.02 | 9 juillet 2000 | Revised by: CC |
| Corrections diverses. Passage en licence FDL. Réécriture de la description des mécanismes de threading et de marshalling. Description de OLE automation. Description des ATL. | ||
| Version 0.01 | 1 août 1998 | Revised by: CC |
| Version initiale. | ||
Ce document donne les définitions relatives au système à composant DCOM/OLE, la liste des principales fonctionnalités disponibles et les diverses difficultés que l'on peut rencontrer lorsque l'on cherche à travailler avec ce système.
Ce document est informel, il ne traite pas de tous les aspects de OLE. Cependant, il fournit la réponse à la plupart des questions que l'on peut se poser lorsque l'on travaille avec DCOM et OLE.
Le début de ce document présente les définitions et les grands principes de OLE. La suite est nettement plus technique et s'adresse plus aux programmeurs avertis. Il y est supposé que le lecteur maîtrise le langage C++ et les principales notions de programmation système nécessaires à la programmation avec DCOM. Bien que DCOM et OLE soient utilisables dans la plupart des langages, tous les exemples seront donnés en C++ en raison de la meilleure compréhension que ce langage permet d'acquérir, du fait qu'il travaille au plus bas niveau.
OLE est l'abréviation de « Object Linking and Embedding », ce qui signifie « Intégration d'objets et Lien sur des objets ». OLE est une technologie qui a été développée par Microsoft, initialement dans le but de permettre la programmation d'objets capables d'être insérés dans des applications réceptacles soit par intégration complète, soit par référence (ce que l'on appelle une liaison). Ce but a été atteint pour la plupart des applications et apparaît à présent dans le menu « Coller | Collage spécial ». Les objets intégrés ou liés sont capables de s'afficher dans l'application qui les contient. Ils sont également capables de fournir un certain nombre de services standards permettant leur manipulation (ces services peuvent être la sauvegarde de leur état, la capacité à être édité, etc…). OLE est donc un remplacement efficace des liaisons DDE.
Pour réaliser cet objectif, Microsoft a dû fournir un standard de communication entre les différentes applications qui voulaient être OLE. Ce standard de communication, définit la méthode à employer pour accéder aux fonctionnalités des objets OLE, ainsi que les principaux services qui peuvent être nécessaires lors de l'intégration d'objets. Les programmeurs désirant réaliser une application OLE devaient se conformer au protocole d'appel du standard et fournir un certain nombre des services définis dans OLE. Ce standard a été conçu de manière ouverte, c'est à dire qu'il n'est pas nécessaire de fournir tous les services définis dans OLE pour être fonctionnel. Cependant, plus un objet OLE offre de services, meilleure son intégration est. De même, plus une application est capable d'utiliser des services, plus elle est fonctionnelle avec les objets qui fournissent ces services. Par ailleurs, il est possible de définir des services différents de ceux définis dans OLE, le système est donc également extensible.
Le standard de communication qui a été défini se nomme COM, ce qui signifie « Component Object Model », ou « Modèle Objet de Composants ». La plupart des services sont définis dans OLE, cependant, COM lui-même utilise un certain nombre de services. La limite entre ce qui est défini par COM et ce qui est défini par OLE est donc floue, mais en principe, les services COM sont tous des services systèmes.
Comme on l'a dit, initialement, OLE devait permettre l'intégration des objets entre applications. En fait, il s'est avéré qu'OLE faisait beaucoup plus que cela : grâce à COM, il permettait l'écriture de composants logiciels réutilisables par différentes applications. Il s'agit donc bien d'une technologie à composants.
Microsoft a ensuite complété la technologie COM afin de permettre la répartition des composants sur un réseau. L'aspect distribué des composants COM ainsi répartis a donné le nom à cette extension de COM, que l'on appelle simplement DCOM (pour « Distributed COM »). Dans la suite du document, on considérera que COM est distribué, on utilisera donc systématiquement le terme DCOM.
Du point de vue du programmeur, DCOM spécifie la manière dont les composants peuvent être utilisés. Du point de vue du système, DCOM spécifie les services que ce dernier doit fournir aux programmeurs. Ce dernier aspect est moins intéressant (ou du moins il n'intéresse que Microsoft), parce que ces spécifications ne concernent que les éditeurs de logiciels qui désirent implémenter DCOM. Je ne décrirai donc DCOM que du point de vue des programmeurs.
DCOM regroupe les fonctionnalités des composants par interfaces. Une interface est un jeu de fonctions plus ou moins liées sémantiquement, qui permettent d'utiliser un composant. Un composant peut implémenter plusieurs interfaces.
DCOM spécifie la forme des interfaces au niveau binaire. Ces interfaces sont en fait un pointeur sur le tableau de pointeurs de fonctions que les compilateurs C++ pour Windows génèrent pour gérer les méthodes virtuelles des classes C++. Ceci implique qu'il est relativement facile de programmer des composants DCOM et encore plus facile de les utiliser en C++ : les interfaces sont simplement des classes abstraites pures. Cependant, comme cette structure peut être recréée dans d'autres langages, on n'est donc pas obligé d'utiliser le C++. DCOM est donc indépendant du langage. En particulier, on peut programmer et utiliser des composants DCOM en C, en Pascal, en Visual Basic, en Assembleur, en Visual J++.
Les interfaces sont identifiées dans le système par des nombres uniques à 128 bits. Ces nombres sont appelés les GUID (« Globally Unique IDentifier », soit « Identificateur globalement unique »), ou UUID (« Universally Unique IDentifier », soit « Identificateur universel unique »). Un utilitaire, UUIDGEN.EXE, est fourni pour générer de tels nombres. Ces nombres sont calculés pour être absolument unique dans le monde et en tout temps, ils permettent donc d'éviter toute collision entre les identificateurs des interfaces définies par tous les programmeurs du monde. Ceci implique qu'à chaque interface correspond un GUID et un seul. Une fois qu'une interface a été définie, on ne peut plus la modifier (pas même l'étendre) : on doit refaire une autre interface (cette nouvelle interface pourra cependant hériter des méthodes de l'interface à étendre, mais sera identifiée par son propre UUID). Une interface est donc parfaitement identifiée par son UUID, souvent appelé IID (pour « Interface IDentifier »), et constitue le contrat qu'un composant passe avec ceux qui l'utilisent. Les composants qui disposent de cette interface signifient simplement qu'ils gèrent les fonctionnalités de cette interface, et qu'ils les géreront toujours sous cette forme.
Pour utiliser une interface d'un composant, il faut d'abord la demander. Ce mécanisme permet l'identification dynamique des fonctionnalités d'un composant : soit le composant serveur gère l'interface demandée et on obtient un pointeur sur cette interface, soit il ne la gère pas et on ne reçoit rien en retour. On ne peut donc utiliser les composants que par l'intermédiaire des interfaces qu'ils gèrent. DCOM spécifie la manière de créer les composants et d'obtenir des interfaces.
Les composants sont également identifiés par un GUID. Comme les composants de COM sont appelés des classes, dont les instances sont les objets OLE eux-mêmes, les GUID qui leurs sont attribués sont appelés les CLSID (« CLaSs IDentifier », soit « Identificateur de classe »). Tous ces GUID sont stockés dans des entrées spécifiques de la base de registre, que le système peut consulter pour faire fonctionner DCOM. La clé principale qui permet de stocker toutes les informations du système sur DCOM est HKEY_CLASSES_ROOT.
Les composants peuvent être écrits aussi bien sous la forme de DLL que sous la forme d'exécutables. Les fichiers qui implémentent des composants sont appelés des serveurs. Les programmes qui utilisent un composant sont ses clients. Quel que soit le type de serveur, DCOM fournit les mécanismes nécessaires pour que ceux-ci s'enregistrent dans le système et puissent être utilisés. Ces mécanismes assurent une totale transparence aussi bien pour l'implémentation des composants vis-à-vis du type de serveur qui les contient que pour les clients. De plus, les serveurs peuvent être exécutés sur une autre machine que celle sur laquelle tourne un client. DCOM assure ainsi une complète transparence par rapport au réseau ou aux communications entre composants. Pour le client, l'utilisation d'un service revient toujours à appeler une fonction d'une interface, et pour le serveur, l'implémentation d'un service revient toujours à écrire cette interface.
Note: On se méfiera de la terminologie de DCOM. Les serveurs peuvent eux-mêmes être clients d'autres composants. Il s'agit ici d'une extension du modèle client/serveur, où chacun peut être à la fois soit l'un, soit l'autre.
Pour assurer la transparence au niveau du réseau, DCOM utilise les RPC (« Remote Procedure Call », ou « Appels de procédures à distance »). Il facilite ainsi énormément la gestion du réseau pour les programmeurs. En particulier, il prend en charge les communications et le marshalling, c'est à dire l'encapsulation du côté client des paramètres donnés par le client à la fonction appelée, ainsi que la reconstitution de ces paramètres du coté serveur pour l'appel de la fonction. Bien entendu, DCOM reste ouvert et permet au programmeur de réaliser son propre marshalling, ou de contrôler son propre protocole de communication.
Note: Le protocole RPC utilisé par Microsoft est basé sur les spécifications DCE (« Distributed Computing Environment ») et n'a absolument rien à voir avec le standard RPC de Sun, utilisé couramment dans le monde Unix.
DCOM est multithreadé. Il permet de réaliser des appels synchrones (bloquants) ou asynchrones. Il permet de contrôler les problèmes de réentrance par sérialisation des appels ou de laisser la gestion du multithreading et des concurrences d'accès au programmeur. Il peut également gérer les interblocages.
DCOM est sécurisé par les droits d'accès des utilisateurs. Ceci signifie qu'il est nécessaire de disposer d'un serveur de domaine pour donner ces droits (en pratique, ce serveur est un poste Windows Server). DCOM gère également les licences d'utilisation pour les composants commerciaux.
DCOM gère la durée de vie des objets OLE par compte de références sur ces objets. Les objets ne sont détruits que lorsque tous les clients de ces objets ont relâché leur référence qu'ils détenaient sur eux.
DCOM permet de stocker la description des interfaces dans ce qu'on appelle des « type libraries ». Ces librairies stockent la description des interfaces, des foncions des interfaces, de leurs paramètres et de leurs rôles. Elles permettent donc à un tout le monde d'utiliser un composant même sans en avoir la documentation.
DCOM définit les interfaces qui sont nécessaires pour l'utiliser. En particulier, il est possible d'utiliser les interfaces donnant accès aux fonctionnalités du marshaling, à la gestion de la mémoire de manière uniforme entre les objets OLE, à la gestion du multithreading, à la gestion des type libraries et à la connectivité des objets OLE entre eux.
À toutes ces fonctionnalités, on peut ajouter des fonctionnalités système implémentées sous la forme d'objets DCOM. La plupart des nouvelles fonctionnalités ajoutées à Windows par Microsoft ne sont accessibles que par l'intermédiaire d'objets COM systèmes. On prendra par exemple la gestion directe des périphériques par Direct X, dont toutes les fonctionnalités sont implémentées sous la forme de composants DCOM.
OLE ajoute aux services fournis par DCOM les fonctionnalités suivantes :
gestion de la persistance des objets à l'aide de stockages structurés ;
gestion des noms des objets et de leur recherche dans le système à l'aide de composants spécifiques (à l'aide des « monikers », qui sont des composants permettant de localiser d'autres composants) ;
gestion des transferts de données uniformes entre applications ;
gestion du copier/coller ;
gestion de la visualisation des objets dans leurs conteneurs ;
gestion de l'édition et de la modification des objets au sein même de leurs conteneurs ;
gestion de la programmation des objets par script (automation) ;
mise à disposition de contrôles OLE permettant de simplifier la programmation ;
intégration dans l'interface utilisateur Windows 9x ou NT.
Les objets OLE peuvent être copiés d'une application OLE et collés dans une autre. L'opération de copie s'effectue exactement comme une copie classique, en sélectionnant l'objet à copier (par exemple quelques lignes d'un document) et en utilisant la commande « Copier ». En revanche, l'opération de collage doit se faire par l'intermédiaire du menu « Édition | Collage spécial ». Cette option fait apparaître une boîte de dialogue permettant de choisir le format des données à coller. Il faut choisir le format « Objet xxxx », qui correspond au format des objets OLE. Il est également possible de choisir le type de collage : avec liaison ou non. Si le collage se fait avec liaison, il s'agit d'une liaison OLE, sinon, c'est une intégration.
La différence entre une liaison et une intégration est importante. Dans le premier cas, l'objet n'est pas réellement collé, seul un lien vers l'objet est créé. Ceci implique plusieurs choses :
le document source doit avoir un nom pour que le lien soit valide. Il doit donc avoir été enregistré ;
on peut faire autant de lien que l'on veut sans consommer de mémoire ou de place disque, puisque les données ne sont stockées que dans le document original ;
le document source ne doit pas être déplacé ou effacé, sinon le lien n'est plus valide ;
la modification de l'objet ne peut pas se faire dans l'application qui contient le lien. Pour le modifier, il faut aller dans l'application qui a servi à créer l'objet ;
le contenu de l'objet doit être mis à jour lors de l'ouverture du document qui contient le lien.
Inversement, dans le cas d'une intégration, les données sont copiées dans
le document conteneur. Les conséquences sont les suivantes :
le document dans lequel on a collé le donné contient ces données, il consomme donc plus d'espace disque et de mémoire ;
il n'est plus nécessaire de sauvegarder les données originales, ni le document original ;
il n'est plus nécessaire de mettre à jour les liens, puisque les données de l'objet sont directement accessibles de son conteneur ;
l'objet OLE vit de manière indépendante, il peut donc être modifié et édité au sein même de son conteneur.
Pour éditer un objet OLE, il suffit de double-cliquer dessus. L'application qui a servi à créer cet objet est alors lancée. Si l'objet a été collé avec liaison, Windows bascule vers cette application et celle-ci ouvre le document source pour que l'on puisse le modifier. Si l'objet a été intégré, cette application se fond dans le conteneur et lui apporte ses fonctionnalités : on peut éditer l'objet directement à partir du conteneur. Les menus, barres d'outils et autres aspects visuels du conteneur sont modifiés pour prendre en compte les fonctionnalités de l'application qui permet d'éditer l'objet.
Les composants OLE automation peuvent être utilisés sans avoir recours à un langage de programmation. Il n'est pas nécessaire d'utiliser des appels de fonction, on peut utiliser à la place un langage de script. Les scripts les plus courants sont VB Script, qui est dérivé de Visual Basic, et Java Script. Il est également possible d'utiliser l'automation dans les langages de macros des logiciels (pour les logiciels Microsoft, ces langages sont en fait un langage dérivé de Visual Basic).
Les composants OLE qui gèrent l'automation apparaissent comme des objets possédant des propriétés et des méthodes. Les propriétés peuvent être lues ou écrites, et les méthodes peuvent être appelées comme des fonctions. Si l'appel des méthodes revient à une programmation classique, l'utilisation des propriétés est une nouveauté, puisque les interfaces COM ne donnent accès qu'à des fonctions.
En réalité, les accès aux propriétés des objets OLE automation sont traduits par OLE dans des appels de méthodes particulières. L'automation permet donc d'utiliser les objets OLE beaucoup plus naturellement qu'avec une programmation directe. En revanche, les performances sont moins bonne que par une programmation directe, car on évite une étape d'interprétation des commandes.
Un serveur in-process est un serveur dont les composants fonctionnent dans le même processus que ses clients. En pratique, les serveurs in-process sont les serveurs DLL.
L'avantage des serveurs in-process est qu'ils sont dans le même espace d'adressage que leur client. Ceci implique :
qu'il est possible d'accéder directement à leurs composants, et de créer ceux-ci sans passer par DCOM ;
que les appels des méthodes de leurs composants peuvent être effectués directement, donc plus efficacement que pour les composants des serveurs exécutables ;
qu'il est possible de réutiliser leurs composants à l'aide du mécanisme d'agrégation.
Toutefois, ce gain disparaît si DCOM constate une incompatibilité entre la gestion du mutithreading effectué par la DLL et celle effectuée par les autres composants du processus. Dans ce cas en effet, DCOM s'intercale et les appels ne se font plus directement.
Les inconvénients des serveurs in-process sont les suivants :
les erreurs dans leurs composants peuvent entraîner la terminaison du client.
Un serveur out-of-process est un serveur qui s'exécute dans un autre processus que le processus client. En pratique, ces serveurs sont des exécutables, qui enregistrent leurs composants lors de leur initialisation.
Les avantages des serveurs out-of-process sont les suivants :
les clients sont mis à l'abri des fautes des serveurs. La terminaison du serveur n'engendre pas la terminaison du client ;
les serveurs out-of-process peuvent partager des données entres les différents clients.
Les inconvénients des serveurs out-of-process sont les suivants :
les appels aux méthodes de leurs composants nécessite le passage des paramètres du processus client au processus serveur, ce qui est bien évidemment plus lent ;
ils ne peuvent pas être utilisés en tant qu'objets agrégés.
Note: Cette dernière limitation est un handicap majeur de DCOM, qui remet en cause complètement la possibilité de réutiliser facilement les composants distribués. En fait, comme on le verra plus tard, l'agrégation ne peut être utilisée qu'entre objets vivant dans le même appartement (voire la gestion du multithreading pour plus de détails à ce sujet).
Comme on l'a vu ci-dessus, les composants et les interfaces de ces composants sont identifiés de manière unique dans le système par les GUID. En fait, les GUID sont utilisés à chaque fois que l'on a besoin d'un identificateur dont on veut être sûr qu'il est unique dans le monde et qu'il le restera. Les GUID permettent donc d'identifier de manière unique tous les objets du système afin de les référencer d'une manière complètement indépendante de tout contexte.
Pour parvenir à ce résultat, il est nécessaire que ces GUID soient affectés rigoureusement sans collision à quiconque en demande. Ceci est réalisé par l'utilitaire UUIDGEN.EXE, qui calcule un nouveau GUID à chaque fois qu'on l'utilise. Ce GUID est calculé sur des données spécifiques à la machine, sur la date courante et sur un compteur à variation très rapide. Ainsi, il est impossible de générer deux fois le même GUID dans le monde, ce pour un temps très grand.
Bien entendu, il doit y avoir un grand nombre de GUID possibles pour qu'il n'y ait aucune collision. En fait, les GUID sont des nombres à 128 bits exprimés en hexadécimal, ce qui donne un nombre total de GUID immense. Le format des GUID générés par UUIDGEN.EXE est donné par l'exemple suivant :
Lorsque ces GUID sont stockés dans la base de registres, ils apparaissent entre accolades :
Cependant, ce format n'est pas celui que DCOM utilise. En effet, DCOM regroupe les huit derniers octets du GUID. La structure des GUID dans DCOM est donc définie ci-dessous :
typedef struct GUID
{
unsigned long Data1;
unsigned short Data2;
unsigned short Data3;
unsigned char Data4[8];
} GUID;
Les GUID utilisés couramment par DCOM et les GUID des composants systèmes sont déclarés dans les fichiers d'en-tête de Windows et définis dans les librairies uuid.lib, uuid2.lib et uuid3.lib.
L'intérêt des GUID est de rendre indépendant les composants et leurs interfaces du contexte d'exécution et de leur installation. En effet, comme les clients n'utilisent que les CLSID pour référencer les composants, ils n'ont pas à se préoccuper de savoir si les composants en question sont implémentés dans une DLL ou dans un exécutable. Ils n'ont même pas à savoir si ce serveur fonctionne en local ou sur une autre machine. Par ailleurs, ils n'ont pas à connaître le nom exact du fichier contenant le serveur, ce qui permet de faire des mises à jour de ces fichiers très souplement, sans même à avoir à modifier les clients. Ceci est un très grand progrès, parce qu'il peut y avoir potentiellement beaucoup de clients qui utilisent un même composant.
De même, en référençant les interfaces par leurs IID, les composants n'ont pas à connaître le nom des fonctions implémentées par les composants. Comme les interfaces sont immuables, les clients qui fonctionnaient à un instant t avec un composant fonctionneront toujours, même si une nouvelle version de ce composant apparaît. Ceci n'empêchera cependant pas le composant d'implémenter de nouvelles interfaces : les anciens clients ne les demanderont pas. En revanche, les clients plus récents peuvent les demander et en tirer profit. Inversement, si un client est mis à jour et demande un nouveau service à un ancien composant, celui-ci répondra simplement qu'il ne peut pas le fournir. Le client est alors libre d'utiliser un ancien service ou de demander une mise à jour du composant. Si le composant est mis à jour, le client sera tout de suite capable d'utiliser les nouvelles fonctionnalités, sans qu'on l'ait lui-même mis à jour. Donc le client et le composant sont complètement indépendants, et peuvent chacun être mis à jours, installé ou déployé sans que l'autre en soit affecté.
Bien entendu, il faut mettre en relation les GUID avec les fichiers utilisés pour implémenter les serveurs et avec les ordinateurs. Toutes ces données sont stockées dans la base de registres. C'est elle qui maintient les associations entre les CLSID et les serveurs, et qui mémorise toutes les interfaces disponibles. D'autres options peuvent être définies, notamment, les noms des machines utilisées et les droits d'accès. La base de registres met également en relation les composants avec leurs type libraries. Elle stocke les GUID des composants qui doivent être utilisés pour réaliser un grand nombre de fonctions du système. Par exemple, les viewers de fichiers de QuickView sont des composants OLE, la Corbeille est un composant OLE, les feuilles de propriétés pour les objets système et pour les fichiers sont des composants OLE, et les raccourcis sont liens OLE.
On peut donc en déduire que toute la configuration de DCOM est stockée dans la base de registres, et que DCOM est lui-même profondément intégré dans le système.
Une interface est un groupement de fonctions gérées par un composant. C'est par ces fonctions que l'on peut utiliser le composant. À chaque interface correspond un IID unique qui l'identifie dans l'espace et le temps.
DCOM définit la forme des interfaces au niveau binaire. Une interface est pointeur sur un tableau de pointeurs sur les fonctions qui la constituent. L'ordre des fonctions pointées dans les interfaces est très important. C'est pour cela qu'il ne faut pas changer la structure d'une interface une fois qu'elle a été définie et que ses spécifications ont été publiées. Les interfaces constituent donc un contrat entre le composant et ses clients. Il est donc impossible de modifier une interface, même de l'étendre. À chaque fois que l'on doit changer le contrat défini par une interface, il est nécessaire d'en redéfinir une.
DCOM définit également les conventions d'appel des fonctions des interfaces pour chaque système d'exploitation sur lequel il est implémenté. Ceci permet d'assurer un bon fonctionnement du passage des paramètres lors des appels de fonctions des interfaces. Les conventions d'appel pour Win32 sur les plates-formes x86 sont les suivantes :
les arguments sont passés par la pile ;
l'ordre d'empilement est de droite à gauche (le dernier argument est empilé en premier) ;
les paramètres sont retirés de la pile par la fonction appelée ;
les valeurs de retour de type flottantes sont retournés dans le registre st(0) du coprocesseur ;
un code d'erreur est retourné dans l'accumulateur EAX.
Ces conventions d'appel sont définies par le mot-clé __stdcall pour les compilateurs C/C++. Afin de rendre le code source portable entre les différentes plates-formes, la macro STDMETHODCALLTYPE a été définie dans le fichier d'en-tête objbase.h.
Toutes les interfaces doivent au moins fournir les fonctionnalités d'une interface particulière, l'interface « IUnknown ». Les fonctions de l'interface IUnknown doivent obligatoirement apparaître en premier dans le tableau de pointeurs de l'interface. Ces fonctions permettent de gérer la durée de vie des objets et d'obtenir de nouveaux pointeurs sur d'autres interfaces à partir du pointeur sur l'interface courante.
La structure binaire des interfaces est exactement celle qu'utilise les compilateurs C++ pour stocker les pointeurs sur les tables de fonctions virtuelles des classes C++. Ceci implique que l'utilisation des interfaces en C++ est très facile. Les interfaces sont des classes qui ne contiennent aucune donnée membre et dont toutes les méthodes sont publiques et virtuelles pures. L'inclusion des fonctionnalités d'une interface existante se fait aisément par héritage simple. Par exemple, l'interface suivante dispose, outre des fonctions de l'interface IUnknown dont elle hérite, de deux fonctions membres :
struct IAdder : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Add(long i, long j,
long *pResult)=0;
virtual HRESULT STDMETHODCALLTYPE Sub(long i, long j,
long *pResult)=0;
};
En général, on ne dispose que d'un pointeur sur les interfaces, quel que soit le moyen d'obtention utilisé (il s'agit donc d'un double pointeur sur la table des fonctions virtuelle). Dans notre exemple, si pI est un pointeur sur cette interface, il est possible d'utiliser directement les fonctions Add et Sub avec la syntaxe suivante :
pI->Add(2, 3, &i);
Note: Il faut faire très attention à bien utiliser la macro STDMETHODCALLTYPE dans la déclaration des fonctions membres des interfaces. En effet, une discordance des conventions d'appel entre le compilateur et les composants utilisés peut être très difficile à détecter.
Il est très important de ne pas définir de données membres dans une interface C++. En effet, pour pouvoir utiliser le pointeur sur cette interface, il est essentiel qu'elle ait exactement la même structure binaire que l'interface DCOM sous-jacente. Si l'on inclut des données membres dans l'interface C++, il n'est pas certain que le compilateur considérera que pointeur sur la table de fonctions virtuelles est au début des données de l'interface. Par conséquent, il risque d'interpréter le pointeur sur le tableau de pointeurs des fonctions de l'interface comme une des données de la classe. Ce problème vient du fait que les compilateurs C++ ne garantissent pas l'emplacement du pointeur sur la table de fonctions virtuelles : il peut très bien être placé après les données de la classe. Le seul moyen d'être sûr et certain de son emplacement est de ne pas mettre de données du tout. Heureusement, ceci n'empêche pas du tout le programmeur de créer un composant en faisant hériter une de ses classes d'une interface, car les compilateurs C++ seront capables de retrouver le sous objet qui représente l'interface.
Il est impossible de constituer une interface qui définit les fonctions membres de plusieurs interfaces par héritage multiple. En effet, les compilateurs C++ utilisent alors plusieurs sous objets, donc plusieurs pointeurs sur les tables de fonctions virtuelles. Cette structure ne correspond plus à celle que DCOM impose pour les interfaces.
À de rares exceptions près, les méthodes des interfaces doivent toujours renvoyer un code d'erreur dont le type est « HRESULT ». Ce type est défini dans le fichier d'en-tête winerror.h. Si les fonctions peuvent être exécutées de manière asynchrone, elles ne doivent renvoyer aucune valeur. En fait, il est possible de réaliser des interfaces dont les méthodes retournent un autre type que le code d'erreur HRESULT ou void. Mais dans ce cas, les objets qui implémentent ces méthodes doivent être capables de gérer eux-même le marshalling de leurs interfaces. Voir le paragraphe concernant le marshalling des interfaces pour plus de détails à ce sujet.
Pour utiliser un composant, il est nécessaire d'obtenir un pointeur sur une interface de ce composant. Cette opération peut être réalisée de plusieurs manières :
en appelant une fonction spécifique du serveur dans le cas des serveurs in-process ;
en appelant une des fonctions de l'API qui renvoient un pointeur sur une interface. Par exemple, les interfaces sur les composants de Direct X s'obtiennent souvent à l'aide de l'appel d'une fonction globale de l'API Direct X ;
en appelant une fonction d'une autre interface, comme on le verra plus tard ;
en utilisant le mécanisme générique défini par DCOM. Ceci se fait en appelant la fonction CoCreateInstance, qui prend en paramètre le CLSID du composant dont on cherche à créer une instance et l'IID de l'interface que l'on désire sur ce composant. On remarquera qu'un même composant peut disposer de plusieurs interfaces, et donc qu'on peut utiliser différents IID d'interface pour un même CLSID dans CoCreateInstance. En général, la première interface que l'on désire recevoir est l'interface IUnknown, parce qu'on est sûr que le composant la gère (elle est obligatoire pour tous les composants).
L'interface IUnknown est l'interface de base dans DCOM. Elle doit être implémentée par tous les composants, quels qu'ils soient. De plus, toutes les interfaces doivent hériter de l'interface IUnknown, quelles qu'elles soient. Les fonctions de l'interface IUnknown permettent de gérer le cycle de vie des objets et d'obtenir les autres interfaces gérées par le composant. L'IID de l'interface IUnknown est déclaré dans le fichier d'en-tête unknwn.h pour les compilateurs C/C++ et défini dans la librairie statique uuid.lib.
La spécification de IUnknown est la suivante :
struct IUnknown
{
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
REFIID iid, void **ppvObject)=0;
virtual ULONG STDMETHODCALLTYPE AddRef(void)=0;
virtual ULONG STDMETHODCALLTYPE Release(void)=0;
};
La fonction QueryInterface permet de récupérer un pointeur sur une nouvelle interface à partir des interfaces dont on a déjà un pointeur. Elle prend en paramètre une référence sur l'IID de l'interface désirée et l'adresse d'un pointeur. Le pointeur dont on donne l'adresse en paramètre est le pointeur qui recevra l'adresse de l'interface désirée si celle-ci est gérée par le composant, ou la valeur 0 si celle-ci n'est pas gérée. Ceci implique trois choses :
il est impossible de demander à un composant de faire ce qu'il ne sait pas faire ;
il est impossible de modifier les données de l'objet sans passer par une de les interfaces du composant dont il est l'instance (c'est ce qu'on appelle l'encapsulation) ;
il est possible de demander au composant ce qu'il est capable de faire dynamiquement.
La valeur retournée par QueryInterface est une valeur du type HRESULT, le type des codes d'erreurs dans DCOM/OLE.
Les fonctions AddRef et Release permettent de contrôler la durée de vie des objets (du moins en ce qui concerne le programme client). Un objet reste en vie tant que quelqu'un l'utilise. Les objets comptent donc le nombre de références qui y sont faites. La règle est donc la suivante : à chaque fois que l'on crée une nouvelle référence sur une interface, on doit appeler AddRef, et à chaque fois que l'on détruit une référence, on doit appeler Release. Ainsi, lorsque la dernière référence est détruite, l'objet sait qu'il est libre de se détruire puisque plus personne ne peut y accéder.
Heureusement, il est possible de simplifier ces règles. Lorsqu'on crée une copie d'une référence sur une interface, et que l'on sait que cette copie a une durée de vie comprise dans celle de la référence à partir de laquelle on l'a initialisée, il est inutile d'appeler les fonctions AddRef et Release. De même, si on crée une copie mais que l'on détruit l'original avant la destruction de la copie, il n'est nécessaire d'appeler Release que lors de la destruction de la copie. En règle générale, il suffit que le compte des références indépendantes soit exact : les fonctions AddRef et Release servent simplement à signaler aux objets s'ils doivent continuer à vivre ou s'ils peuvent se détruire.
On ne peut prêter aucune signification aux valeurs retournées par les fonctions AddRef et Release, si ce n'est que Release retourne 0 lorsque l'on vient de libérer la dernière référence sur l'objet. Ceci ne veut pas dire pour autant que celui-ci est détruit (d'autres clients peuvent l'utiliser), et encore moins que son serveur est déchargé de la mémoire (d'autres objets peuvent être implémentés par ce serveur et être en cours d'utilisation).
Les codes d'erreurs OLE sont du type HRESULT. Ce type est défini dans le fichier d'en-tête winerror.h.
Les valeurs HRESULT sont des valeurs codées sur 32 bits, qui permettent non seulement de signaler si l'opération s'est bien déroulée ou non, mais aussi de renseigner sur la manière dont elle s'est effectuée.
Les 16 bits de poids faible des codes d'erreurs donnent un code dont la valeur renseigne sur ce qui s'est passé. Le bit de poids fort indique si le code représente une erreur ou un succès, on l'appelle le bit de sévérité. Enfin, les bits 16 à 28 représentent la facilité de l'erreur, qui représente le groupe d'erreur auquel le code appartient. La facilité permet souvent de renseigner sur la couche logicielle qui est à l'origine de l'erreur. Les bits 29 et 30 sont réservés.
Afin de manipuler plus facilement les codes HRESULT, les macros suivantes ont été définies :
Tableau 1. Macros de manipulation des codes d'erreurs
| Macro | Signification |
|---|---|
| SUCCEEDED(x) | Indique si l'opération a réussi |
| FAILED(x) | Indique si l'opération a échoué |
| HRESULT_CODE(x) | Renvoie la partie Code du HRESULT |
| HRESULT_FACILITY(x) | Renvoie la facilité du HRESULT |
| HRESULT_SEVERITY(x) | Renvoie la sévérité du HRESULT |
Les macros suivantes peuvent également être utiles :
Tableau 2. Codes d'erreurs standard
| Code | Signification |
|---|---|
| S_OK, NO_ERROR | Valent 0. Indique que tout va bien. |
| S_FALSE | Vaut 1. L'opération s'est bien déroulée et le code d'erreur vaut TRUE. |
Toutes ces macros, ainsi que les macros qui représentent les principaux codes d'erreurs, sont également définies dans le fichier d'en-tête winerror.h.
Note: DCOM utilise le code d'erreur HRESULT pour quasiment toutes ses interfaces, à quelques exceptions près. L'exception la plus notable est bien entendu l'interface IUnknown. En fait, les méthodes des interfaces définies par le programmeur ne doivent pas toutes retourner une valeur de type HRESULT. Les méthodes asynchrones, qui ne peuvent renvoyer aucune valeur doivent renvoyer void. Il est possible d'utiliser d'autres types pour les valeurs de retour des fonctions, mais les objets qui gèrent ces interfaces doivent dès lors gérer leur propre marshalling (voir le paragraphe concernant le marshalling des interfaces pour plus de détails à ce sujet.).
Les codes HRESULT constituent le seul moyen de signaler une erreur. En particulier, DCOM ne supporte pas les exceptions, ce qui est l'un de ses plus grands points faibles.
Il est relativement facile d'utiliser un composant. Il suffit de définir les interfaces que l'on va utiliser et d'initialiser des pointeurs sur ces interfaces. L'essentiel pour un client est donc d'obtenir une interface. La technique à utiliser est décrite ci-dessous.
Avant toute chose, un programme qui veut utiliser DCOM doit appeler la fonction CoInitialize :
dont le premier paramètre est réservé et doit toujours être 0. Si le programme désire utiliser OLE en plus de DCOM, il devra appeler OleInitialize à la place de CoInitialize : dont le premier paramètre doit également être 0. Cette fonction appelle en interne la fonction CoInitialize, si bien que l'appel à OleInitialize suffit. Cette fonction initialise également la librairie OLE.La fonction CoInitialize est déclarée dans le fichier d'en-tête objbase.h et la fonction OleInitialize est déclarée dans le fichier ole2.h. Elles sont toutes les deux définies dans la librairie ole32.lib.
Une fois les initialisations réalisées, il faut obtenir un pointeur sur une interface d'une instance du composant. Certaines fonctions de l'API permettent d'obtenir directement un pointeur sur une interface, et donc de l'utiliser directement. Cependant, la méthode générale est d'utiliser la fonction CoCreateInstance :
HRESULT STDAPICALLTYPE CoCreateInstance
( REFCLSID rClsId , LPUNKNOWN pOuterUnknown , DWORD dwClsContext ,
REFIID rIId , LPVOID *ppInterface );
Cette fonction est déclarée dans le fichier d'en-tête objbase.h et définie dans la librairie ole32.lib.
Le premier paramètre est une référence sur le CLSID du composant dont on cherche à créer une instance. Le deuxième paramètre n'est utilisé que pour les agrégats de composants. Il peut être nul dans le cas des clients simples. Le troisième paramètre indique le contexte dans lequel l'objet devra être créé. Les différents contextes possibles sont les suivants :
CLSCTX_INPROC_SERVER, qui permet de demander que le serveur soit une DLL ;
CLSCTX_INPROC_HANDLER, qui permet de demander que le serveur soit une DLL utilisant un serveur exécutable pour certaines de ses fonctions ;
CLSCTX_LOCAL_SERVER, qui permet de demander que le serveur soit un exécutable fonctionnant sur la même machine que celle du client ;
CLSCTX_REMOTE_SERVER, qui permet de demander que le serveur fonctionne sur une autre machine que celle du client ;
CLSCTX_SERVER, qui permet de demander que le serveur soit quelconque, mais pas un HANDLER ;
CLSCTX_ALL, qui permet d'indiquer que l'on n'a aucune préférence sur le contexte d'exécution du serveur.
Ces constantes sont définies dans le fichier d'en-tête wtypes.h.
Le quatrième paramètre contient l'IID de l'interface demandée. À moins que l'on soit sûr que l'interface demandée est bien gérée par le composant, il faut demander l'interface IUnknown (qui est toujours gérée). Enfin, le dernier paramètre est l'adresse du pointeur sur l'interface désirée en retour. Si l'interface demandée n'est pas gérée, ce pointeur contiendra la valeur 0.
Lorsque l'on a obtenu un pointeur sur une interface, il est possible d'appeler les fonctions de cette interface, exactement comme on appelle les fonctions membres d'une classe.
Si l'on désire obtenir une nouvelle interface sur le même objet, on doit utiliser la fonction QueryInterface (on ne peut pas réutiliser CoCreateInstance, car cette fonction ne fait pas que donner un pointeur sur une interface, elle crée également un objet). Par exemple, si pUnknown est un pointeur sur l'interface IUnknown d'un objet et que l'on cherche à obtenir un pointeur sur l'interface IStorage, on procédera comme suit :
Enfin, il ne faut surtout pas oublier d'appeler la méthode Release sur les pointeurs dont on ne se servira plus, afin de détruire l'objet lorsqu'il sera inutilisé :
Avant de se terminer, les programmes doivent appeler respectivement CoUninitialize ou OleUninitialize selon la fonction qui a été appelée pour l'initialisation. La signature de ces fonctions est donnée ci-dessous :
L'exemple ci-dessous montre un programme complet qui utilise un composant DCOM. On suppose que le nom de la variable contenant le CLSID du composant est « CLSID_Calculator », et que le nom de la variable contenant le GUID de l'interface IAdder est « IID_IAdder ».
Exemple 1. Programme client simple
#include <objbase.h>
#include <stdio.h>
#include "adder.h"
int main(void)
{
if (SUCCEEDED(CoInitialize(0)))
{
IAdder *pAdder;
if (SUCCEEDED(CoCreateInstance(CLSID_Calculator, NULL,
CLSCTX_ALL, IID_Adder, (void **) &pAdder)))
{
long lResult;
pAdder->Add(2, 3, &lResult);
pAdder->Release();
printf("2+3=%d\n", lResult);
}
CoUninitialize();
}
return 0;
}Il est un peu plus difficile de réaliser un composant que d'en utiliser un, parce qu'il faut implémenter un certain nombre de services de base qui sont exigés par DCOM. En particulier, tout composant doit nécessairement implémenter l'interface IUnknown pour la gestion de la durée de vie de ses instances.
En pratique, on aura tout intérêt à utiliser les mécanismes d'héritage et de fonctions virtuelles du C++ pour créer les composants. En effet, le mécanisme de fonctions virtuelles correspond exactement à celui des interfaces d'une part, et les objets qui devront implémenter des interfaces pourront simplement hériter de ces dernières et définir les méthodes virtuelles pures ainsi héritées.
Par exemple, pour créer un composant qui implémente les interfaces IUnknown et IAdder, on peut créer une classe « CAdder » qui hérite de l'interface IAdder (et donc de l'interface IUnknown via IAdder) :
class CAdder : public IAdder
{
// Compte de références sur l'objet :
unsigned long m_ulRefCount;
public:
// Méthodes de l'interface IUnknown :
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
REFIID iid, void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
// Méthodes de l'interface IAdder :
virtual HRESULT STDMETHODCALLTYPE Add(long i, long j, long *iResult);
virtual HRESULT STDMETHODCALLTYPE Sub(long i, long j, long *iResult);
// Constructeur et autres fonctions de gestion de la classe :
CAdder(void);
HRESULT Init(void);
};
Note: Il est très important d'indiquer les conventions d'appel des méthodes des interfaces à l'aide de la macro STDMETHODCALLTYPE. En effet, une discordance des conventions d'appels entre le composant et ses clients peut être très difficile à détecter et provoquer des erreurs très étranges.
L'implémentation des méthodes propres à l'interface IAdder ne pose pas de problèmes particuliers. C'est dans ces méthodes que se trouvent les fonctionnalités du composant, cependant, il est nécessaire d'implémenter les méthodes de IUnknown pour respecter les conventions de DCOM. Nous allons détailler la manière d'implémenter ces méthodes dans les paragraphes suivants.
Pour un composant simple comme celui que l'on est en train d'écrire, il n'y a aucune difficulté réelle. Avant tout, le constructeur de la classe doit initialiser le compteur de références sur les objets à 0 :
Ensuite, la méthode AddRef doit se contenter d'incrémenter ce compteur :
Note: Il est très important de préciser les conventions d'appels lors de l'implémentation des méthodes des interfaces à l'aide de la macro STDMETHODCALLTYPE. En effet, certains compilateurs ne sont pas capables de différencier les méthodes déclarées avec certaines conventions d'appels et implémentées avec d'autres conventions d'appel. Avec ce type de compilateurs, aucune erreur n'est signalée, cependant, les erreurs dues au conflit de conventions d'appel entre les clients et le composant seront tout de même présentes.
Bien que les clients ne puissent apporter aucun crédit à la valeur retournée par la méthode AddRef, cette fonction doit retourner la valeur du compteur de références. DCOM est susceptible de l'utiliser à titre interne ou à des fins de débogage.
La méthode Release doit, quant à elle, se charger de la destruction de l'objet si toutes les références sur celui-ci ont été détruites. Le code type est donné ci-dessous :
ULONG STDMETHODCALLTYPE CAdder::Release(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
delete this; // Destruction de l'objet.
return 0; // Ne pas renvoyer m_ulRefCount (il n'existe plus).
}
Dans ce code, on voit que le compteur est décrémenté. Si ce compteur est nul, l'objet est détruit. Dans tous les cas, le nombre de références est renvoyé.
Enfin, la méthode QueryInterface se charge de renvoyer les pointeurs sur les interfaces gérées. Si l'interface demandée n'est pas gérée, le pointeur nul doit être retourné. En revanche, si l'interface est géré, QueryInterface augmente le compte de références de une unité :
HRESULT STDMETHODCALLTYPE CAdder::QueryInterface(REFIID iid, void **ppvObject)
{
*ppvObject=0; // Toujours initialiser le pointeur renvoyé.
if (iid==IID_IUnknown)
*reinterpret_cast<IUnknown **>(ppvObject)=
static_cast<IUnknown *>(this);
else if (iid==IID_IAdder)
*reinterpret_cast<IAdder **>(ppvObject)=
static_cast<IAdder *>(this);
if (*ppvObject==0) return E_NOINTERFACE;
AddRef(); // On incrémente le compteur de références.
return NOERROR;
}
Une fois le composant créé, il faut fournir le code de création pour que les clients puissent l'utiliser. Typiquement, ce code de création demande l'IID de l'interface demandée pour le composant et renvoie un pointeur cette l'interface :
HRESULT CreateAdder(REFIID iid, void **ppvObject)
{
CAdder *pAdder=new CAdder;
if (pAdder==0) return E_OUTOFMEMORY;
if (SUCCEEDED(pAdder->Init()))
return pAdder->QueryInterface(iid, ppvObject);
delete pAdder;
return E_FAIL;
}
Bien que ce code fonctionne parfaitement dans le cadre des serveurs in-process si la fonction de création est exportée, il ne convient pas pour les clients qui ne connaissent pas le nom de cette fonction. Il ne convient pas non plus pour les serveurs exécutables, puisque le pointeur renvoyé n'est valide que dans l'espace d'adressage du serveur, pas dans celui des clients. Il faut donc souvent recourir à un mécanisme de création standard. Ce mécanisme est décrit par COM, il utilise la notion de fabrique de classe. Nous verrons ce mécanisme en détail plus loin.
La mémoire est gérée ainsi :
les paramètres en entrée seule sont alloués et restitués par l'appelant ;
les paramètres en sortie seule sont alloués par l'appelé et libérés par l'appelant ;
les paramètres en entrée/sortie sont alloués par l'appelant, libérés par l'appelé, ou éventuellement réalloués par l'appelé et libérés par l'appelant.
Ces règles permettent de préciser clairement qui doit allouer et qui doit libérer les blocs mémoire. Cependant, il faut également savoir quel mécanisme utiliser pour uniformiser la gestion de la mémoire. En effet, il faut bien se rendre compte du fait que les blocs mémoires peuvent être créés par différents composants, qui ne fonctionnent pas forcément tous dans le même processus.
Le cas le plus compliqué et le plus lent est bien entendu celui où un bloc mémoire est alloué sur une machine et est passé en paramètre à un autre processus fonctionnant sur une autre machine. Dans ce genre de situation, DCOM et les couches réseaux qu'il utilise se chargent de transférer le bloc mémoire. Ceci signifie qu'un autre bloc est créé dans l'espace d'adressage du processus qui doit recevoir le bloc de mémoire et les données sont recopiées d'un bloc à l'autre.
Note: On constate ici que le fait de préciser quels sont les paramètres qui sont en entrée seule, en sortie seule et ceux qui sont en entrée/sortie constitue une optimisation de taille. En effet, les paramètres en entrée seule ne sont copiés que du client vers le serveur, ceux en sortie seule ne sont copiés que du serveur vers le client, et ceux qui sont en entrée/sortie sont copiés dans les deux sens, à l'appel et au retour de la fonction appelée par le client.
Ces mécanismes impliquent que les clients et les serveurs doivent tous les deux utiliser les mêmes techniques d'allocation que DCOM. C'est pour cela que DCOM fournit un allocateur de mémoire pour chaque processus. Cet allocateur doit être impérativement utilisé pour transférer des blocs de mémoire (donc des pointeurs) dans les appels de méthodes entre composants, ce quelle que soit la nature des composants (in-process ou exécutable), puisque ni le client ni le serveur ne peuvent savoir la nature l'un de l'autre. Cet allocateur implémente l'interface IMalloc, dont la déclaration est donnée ci-dessous :
struct IMalloc : public IUnknown
{
virtual void * STDMETHODCALLTYPE Alloc(ULONG cb)=0;
virtual void * STDMETHODCALLTYPE Realloc(void *pv, ULONG cb)=0;
virtual void STDMETHODCALLTYPE Free(void *pv)=0;
virtual ULONG STDMETHODCALLTYPE GetSize(void *pv)=0;
virtual int STDMETHODCALLTYPE DidAlloc(void *pv)=0;
virtual void STDMETHODCALLTYPE HeapMinimize(void)=0;
};
Les méthodes de cette interface sont classiques. Alloc permet d'allouer un bloc de mémoire, Free de le libérer et Realloc de changer sa taille. GetSize permet de déterminer la taille d'un bloc de mémoire, et DidAlloc permet d'indiquer si un bloc mémoire a été alloué par l'allocateur dont on appelle cette méthode. La méthode HeapMinimize permet de compacter le tas des blocs mémoire et de rendre l'espace inutilisé au système d'exploitation.
La fonction de l'API OLE qui permet d'obtenir un pointeur sur l'allocateur mémoire est déclarée ci-dessous :
Cette déclaration est placée dans le fichier d'en-tête objbase.h. Le premier paramètre de cette fonction est obsolète et doit toujours valoir MEMCTX_TASK. Le deuxième paramètre est l'adresse du pointeur sur l'interface IMalloc de l'allocateur mémoire. Une fois ce pointeur obtenu, on peut utiliser les méthodes de cette interface. Lorsque l'on n'a plus besoin de l'allocateur, il faut appeler la méthode Release sur le pointeur de l'interface pour libérer l'allocateur.
Note: Le fait de libérer l'allocateur mémoire ne détruit pas les blocs mémoire alloués par cet allocateur. En fait, l'allocateur n'est pas détruit, seule l'interface sur cet allocateur est libérée. On peut donc libérer les blocs mémoires alloués ultérieurement, sans avoir à conserver le pointeur sur l'interface IMalloc pendant toute la durée de vie des blocs de mémoire allouée.
Afin de simplifier l'utilisation de l'allocateur mémoire d'OLE, les fonctions suivantes ont été définies dans l'API. Elles s'utilisent exactement comme les fonctions de la librairie C, et ne font qu'encapsuler les appels à CoGetMalloc/méthode IMalloc::Release :
Ces fonctions sont toutes déclarées dans le fichier d'en-tête objbase.h.
D'une manière générale, les règles données au début de ce paragraphe suffisent lorsque l'on utilise l'allocateur de DCOM. En particulier, les pointeurs sur les blocs mémoire qui sont utilisés en tant que valeur de retour voient le bloc mémoire sur lequel ils pointent détruit automatiquement par DCOM. Ce comportement de DCOM permet d'utiliser les pointeurs d'une manière classique dans les appels de fonctions. Cependant, il faut faire attention à ne pas conserver en interne de tels pointeurs d'un appel à l'autre, car dans ce cas leurs valeurs ne seraient plus valides. Autrement dit, il est interdit de faire des alias de pointeurs ou de références. Ceci implique aussi que l'on ne doit pas passer en paramètre l'adresse d'un objet alloué statiquement dans un appel de fonction d'un composant.
L'implémentation des objets disposant de plusieurs interfaces pose problème. Les trois techniques recommandées sont les suivantes :
définition de classes héritant des interfaces différentes, et d'une classe implémentant l'objet et implémentant les fonctions de IUnknown. L'objet maintient des liens sous formes de pointeurs avec les objets définissant les interfaces ;
définition d'une classe pour l'objet ayant des sous classes pour les interfaces de cet objet, l'objet lui-même contient les sous objets implémentant les interfaces ;
héritage multiple pour ne créer qu'un objet disposant de toutes les interfaces.
Cette dernière solution est la solution qui utilise le mieux le C++, c'est donc la plus facile à programmer. Cependant, son principal défaut est qu'elle ne permet pas d'implémenter des objets dont deux interfaces au moins contiennent deux fonctions de même nom et de même signature. De plus, il est impossible de réaliser un décompte des références interface par interface avec cette méthode.
Note: On pourrait penser que le fait que l'interface de base IUnknown soit dupliquée dans l'objet est un défaut, mais il n'en est rien. En effet, cette interface doit de toutes façons être dupliquée dans chacune des interfaces dont dispose le composant, et les trois premières entrées des tables de fonctions virtuelles doivent être réservées pour les méthodes de IUnknown. Ceci a pour principale conséquence qu'il ne faut pas rendre virtuelle l'interface IUnknown.
L'exemple suivant montre comment implémenter un objet disposant de plusieurs interfaces par héritage multiple.
Exemple 2. Implémentation de plusieurs interfaces par héritage multiple
// Définition de l'interface IOpposite :
struct IOpposite : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Opposite(long i, long *pResult)=0;
};
// Implémentation d'un objet gérant les deux interfaces
// par héritage multiple :
class CAdder : public IAdder, public IOpposite
{
unsigned long m_ulRefCount; // Le compteur de références.
public:
// Les méthodes de IUnknown sont communes à toutes les interfaces :
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
REFIID iid, void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
// Méthodes de l'interface IAdder :
virtual HRESULT STDMETHODCALLTYPE Add(long i, long j, long *pResult);
virtual HRESULT STDMETHODCALLTYPE Sub(long i, long j, long *pResult);
// Méthodes de l'interface IOpposite :
virtual HRESULT STDMETHODCALLTYPE Opposite(long i, long *pResult);
// Constructeurs et fonctions d'initialisation :
CAdder(void);
HRESULT Init(void);
};
// Implémentation :
CAdder::CAdder(void) : m_ulRefCount(0)
{
return ;
}
HRESULT CAdder::Init(void)
{
return NOERROR;
}
// IUnknown :
HRESULT STDMETHODCALLTYPE CAdder::QueryInterface(REFIID iid,
void **ppvObject)
{
*ppvObject=0;
if (iid==IID_IUnknown)
*reinterpret_cast<IUnknown **>(ppvObject)=
static_cast<IAdder *>(this);
else if (iid==IID_IAdder)
*reinterpret_cast<IAdder **>(ppvObject)=
static_cast<IAdder *>(this);
else if (iid==IID_IOpposite)
*reinterpret_cast<IOpposite **>(ppvObject)=
static_cast<IOpposite *>(this);
if (*ppvObject==0) return E_NOINTERFACE;
AddRef();
return NOERROR;
}
ULONG STDMETHODCALLTYPE CAdder::AddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdder::Release(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
delete this;
return 0;
}
// IAdder :
HRESULT STDMETHODCALLTYPE CAdder::Add(long i, long j, long *pResult)
{
*pResult=i+j;
return ;
}
HRESULT STDMETHODCALLTYPE CAdder::Sub(long i, long j, long *pResult)
{
*pResult=i-j;
return ;
}
// IOpposite :
HRESULT STDMETHODCALLTYPE CAdder::Opposite(long i, long *pResult)
{
*pResult=-i;
return ;
}La seule différence par rapport à l'implémentation par héritage simple réside dans la fonction QueryInterface. Dans le cas où l'interface demandée est l'interface IUnknown, le pointeur this est transtypé en un pointeur sur une des interfaces gérées par le composant. On est obligé de renvoyer un pointeur sur une autre interface que l'interface IUnknown parce qu'il existe plusieurs interfaces IUnknown dans ce composant (une pour chaque interface gérée), ce qui provoque une ambiguïté. En fait, le choix de l'interface que l'on renvoie n'est absolument pas déterminant, puisque du point de vue du client, seules les trois premières entrées dans la table des fonctions de l'interface comptent (or ces trois premières entrées sont toujours prises par les fonctions de l'interface IUnknown). On remarquera au passage que l'on a eu besoin d'implémenter qu'une seule interface IUnknown. Donc l'impossibilité d'implémenter plusieurs fonctions membres de même nom et de même signature dans des interfaces différentes peut également être considéré ici comme un avantage.
Les composants peuvent être réutilisés par d'autres composants à l'aide de deux techniques.
La première technique consiste à utiliser le composant réutilisable comme tout autre composant et à implémenter les interfaces de celui-ci. Les interfaces ainsi implémentées ne font cependant rien d'autre que d'appeler les fonctions des interfaces du composant réutilisable. Cette technique est appelée la délégation. Bien que simple, la délégation souffre d'un très gros défaut : si le composant à réutiliser dispose de beaucoup d'interface, beaucoup de code doit être écrit simplement pour déléguer les appels des interfaces de ce composant.
La deuxième technique est l'agrégation. Cette technique est plus simple au niveau des interfaces : le composant qui utilise le composant réutilisable transmet directement à ses clients les interfaces de ce dernier. En revanche, la gestion des interfaces IUnknown est nettement plus complexe, puisque les interfaces IUnknown des composants agrégés doivent se comporter comme l'interface IUnknown de leur conteneur. Le composant réutilisable doit donc être prévu pour utiliser l'interface IUnknown des composants qui l'utilisent. Une telle interface IUnknown est dite externe, puisque le composant agrégé ne la gère pas directement. Par ailleurs, la gestion de la durée de vie du composant réutilisable est reportée dans la gestion de la durée de vie du composant conteneur. En effet, le composant réutilisé reste en vie tant que son conteneur est lui-même vivant. Ceci implique que le conteneur est en charge de la gestion de la durée de vie des composants qu'il utilise. Il doit donc disposer de pointeurs sur leurs interfaces IUnknown internes (c'est à dire les interfaces IUnknown que les composants réutilisés exposeraient s'ils n'étaient pas agrégés).
Bien que plus compliquée, l'agrégation profite du fait que l'interface IUnknown est bien connue et spécifiée une fois pour toutes. La conséquence est qu'au lieu de réécrire toutes les interfaces des composants réutilisés, on ne modifie que les trois fonctions de leurs interfaces IUnknown pour gérer l'agrégation. La quantité de travail est donc fixe, et peut être faite une fois pour toutes.
L'agrégation souffre cependant d'un autre défaut majeur : elle ne peut pas être utilisée lorsque l'agrégat et l'objet agrégé ne fonctionnent pas dans le même appartement (voir plus loin la définition de la notion d'appartement). Ceci signifie en particulier qu'il est impossible d'écrire des composants agrégeables dans des serveurs out-of-process.
Note: Cette restriction n'est pas justifiée par un impératif technique à première vue. Le principal problème avec l'agrégation est de transmettre l'interface IUnknown de l'agrégat au composant agrégé. DCOM n'est actuellement pas capable de réaliser cette tâche dès que le processus de marshalling standard entre en jeu (voir le paragraphe concernant le marshalling des interfaces pour plus de détails à ce sujet.). Cependant, il pourrait être réalisé un jour. Il est donc recommandé de réaliser malgré tout des composants capables de gérer l'agrégation (après tout, qui peut le plus peut le moins).
Les règles à respecter lors de l'agrégation des objets sont les suivantes :
l'objet réutilisable doit implémenter une interface IUnknown interne (comprenant les fonctions AddRef, Release et QueryInterface) qui est différente de l'interface IUnknown externe. L'interface IUnknown interne permet de contrôler la durée de vie de l'objet agrégé, et n'est utilisé que par les conteneurs ;
l'objet réutilisable doit être capable de stocker un pointeur sur l'interface IUnknown externe de l'agrégat (ou son interface IUnknown classique si celui-ci ne supporte pas l'agrégation) ;
les fonctions de l'interface externe IUnknown, ainsi que les fonctions de IUnknown qui sont intégrées dans les autres interfaces de l'objet agrégé, ne font rien d'autre que de déléguer leur travail aux fonctions de l'interface IUnknown externe de l'agrégat ;
l'agrégat doit demander un pointeur sur l'interface interne de l'objet agrégé lors de la création de ce dernier. Ce pointeur lui permet de contrôler la durée de vie de l'objet agrégé, et il ne doit en aucun cas le communiquer à l'extérieur. L'objet agrégé doit impérativement faire avorter sa création si l'agrégat ne lui demande pas son interface IUnknown interne ;
l'agrégat doit donner le pointeur sur sa propre interface IUnknown externe aux objets agrégés qu'il contient lors de leur création. Lorsqu'un objet agrégé reçoit le pointeur sur l'interface IUnknown externe de l'agrégat, il ne doit pas appeler la fonction AddRef par l'intermédiaire de ce pointeur. Ceci est logique, puisque l'objet agrégé a une durée de vie incluse dans celle de l'agrégat. Si l'objet agrégé appelait AddRef, le compteur de référence de l'agrégat serait incrémenté d'une unité, et ne pourrait pas tomber à zéro tant que l'objet agrégé existerait. L'agrégat serait donc immortel (cas particulier de références circulaires) ;
de même, si, pour une raison ou une autre, l'agrégat demande à l'un des objets agrégés qu'il contient un pointeur sur une interface et qu'il stocke ce pointeur pour un usage ultérieur, il doit appeler la fonction Release de sa propre interface IUnknown externe. En effet, lorsqu'il demande ce pointeur, la fonction QueryInterface de l'objet agrégé concerné effectue un appel à la fonction AddRef de son interface IUnknown externe, soit l'interface IUnknown externe de l'agrégat. Par conséquent, le compteur de référence de l'agrégat est augmenté de un et ne peut être décrémenté tant que le pointeur obtenu n'est pas relâché. Si l'on ne corrigeait pas ce compteur, on serait en présence d'une référence circulaire qui rendrait l'agrégat immortel ;
il résulte de la règle précédente que pour détruire un pointeur sur une interface d'un objet agrégé, l'agrégat doit appeler la fonction AddRef de sa propre interface IUnknown externe afin de rétablir le compte des références à sa juste valeur. La seule exception à cette règle est bien entendu le pointeur sur l'interface IUnknown interne des objets agrégés, qui ne gère que le compte de référence de ces objets sans délégation vers l'interface IUnknown externe de l'agrégat ;
la fonction QueryInterface de l'agrégat ne doit pas déléguer son travail à la fonction QueryInterface de l'objet agrégé. Elle doit contrôler que les interfaces demandées sont correctes. Ceci permet d'éviter l'évolution imprévue des spécifications de l'agrégat après une mise à jour de l'objet agrégé (celui-ci pourrait accepter de nouvelles interfaces que l'agrégat ne serait pas capable de gérer).
D'autres règles interviennent dans certaines situations. Les conditions d'application de ces règles ne sont pas toujours vérifiées, cependant, elles sont d'une importance capitale :
si l'agrégat construit les objets agrégés dans son propre code de construction, il doit se protéger d'une destruction prématurée en appelant la méthode AddRef de son interface IUnknown externe. En effet, les objets agrégés sont susceptibles de demander une interface à leur agrégat et de la libérer avant la fin de leur code de création. Un tel comportement produit le passage du compteur de référence de l'agrégat de 0 à 1, puis de 1 à 0 et donc sa destruction, ce qui n'est pas un comportement sain. La fonction de création de l'agrégat doit donc appeler explicitement la méthode Release de l'agrégat une fois que la fonction QueryInterface a été appelée pour obtenir la première interface, car le compteur de référence vaut 2 alors qu'une seule interface a été obtenue ;
si un pointeur sur l'un des constituants de l'agrégat est stocké à usage interne et doit être détruit, l'agrégat doit d'abord appeler sa propre fonction AddRef, puisque Release a été appelé juste après que l'agrégat ait obtenu ce pointeur ;
la fonction Release de l'agrégat doit protéger le code de destruction d'une réentrance à l'aide d'un compte artificiel. Ceci est impératif si l'agrégat stocke un pointeur sur une des interfaces d'un des objets agrégés et ne le libère que dans son code de destruction. En effet, comme on l'a vu, la libération de ce type de pointeur nécessite l'appel de la fonction AddRef de l'interface IUnknown de l'agrégat avant l'appel de la fonction Release sur ce pointeur. Cette séquence AddRef de l'agrégat / Release sur un pointeur interne, fait passer le compteur de références de l'agrégat de 0 à 1 puis de 1 à 0 (provoquant ainsi une destruction récursive si aucune protection n'est implémentée). Contrairement à ce que semble indiquer le SDK d'OLE, ce type de situation ne peut arriver que dans le cas où un agrégat contient un pointeur interne sur l'objet agrégé et ne le libère que dans son code de destruction. Malgré cela, il est recommandé de toujours protéger son code de destruction contre les réentrances pour prévoir d'éventuelles modifications ultérieures.
L'exemple suivant démontre comment ces règles doivent être appliquées.
Exemple 3. Composant gérant l'aggrégation
// Définition de l'interface IAdder :
struct IAdder : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Add(long i, long j,
long *pResult)=0;
virtual HRESULT STDMETHODCALLTYPE Sub(long i, long j,
long *pResult)=0;
};
// Définition de l'interface IOpposite :
struct IOpposite : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Opposite(long i, long *pResult)=0;
};
// Définition de l'interface IInnerUnknown :
struct IInnerUnknown
{
virtual HRESULT STDMETHODCALLTYPE InnerQueryInterface(REFIID iid,
void **ppvObject)=0;
virtual ULONG STDMETHODCALLTYPE InnerAddRef(void)=0;
virtual ULONG STDMETHODCALLTYPE InnerRelease(void)=0;
};
// Implémentation d'un objet gérant l'agrégation
// et plusieurs interfaces par héritage multiple :
class CAdder : public IInnerUnknown, public IAdder, public IOpposite
{
IUnknown *m_pUnknown; // Pointeur sur l'interface IUnknown à utiliser.
unsigned long m_ulRefCount;
public:
// Les méthodes de IInnerUnknown :
virtual HRESULT STDMETHODCALLTYPE InnerQueryInterface(REFIID iid,
void **ppvObject);
virtual ULONG STDMETHODCALLTYPE InnerAddRef(void);
virtual ULONG STDMETHODCALLTYPE InnerRelease(void);
// Les méthodes de IUnknown :
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid,
void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
// Méthodes de l'interface IAdder :
virtual HRESULT STDMETHODCALLTYPE Add(long i, long j,
long *pResult);
virtual HRESULT STDMETHODCALLTYPE Sub(long i, long j,
long *pResult);
// Méthodes de l'interface IOpposite :
virtual HRESULT STDMETHODCALLTYPE Opposite(long i, long *pResult);
// Constructeurs et fonctions d'initialisation :
CAdder(IUnknown *pUnknown);
HRESULT Init(void);
};
// Implémentation :
CAdder::CAdder(IUnknown *pUnknown) : m_ulRefCount(0)
{
// On détermine l'interface IUnknown à utiliser :
if (pUnknown!=0)
// Interface externe de l'agrégat
// (passé en paramètre lors de la construction) :
m_pUnknown=pUnknown;
else
// Interface IUnknown interne du composant :
m_pUnknown=reinterpret_cast<IUnknown *>(
static_cast<IInnerUnknown *>(this));
return ;
}
HRESULT CAdder::Init(void)
{
return NOERROR;
}
// Les méthodes de IUnknown appellent les fonctions de l'interface interne
// ou celles de l'interface externe de l'agrégat :
HRESULT STDMETHODCALLTYPE CAdder::QueryInterface(REFIID iid, void **ppvObject)
{
return m_pUnknown->QueryInterface(iid, ppvObject);
}
ULONG STDMETHODCALLTYPE CAdder::AddRef(void)
{
return m_pUnknown->AddRef();
}
ULONG STDMETHODCALLTYPE CAdder::Release(void)
{
return m_pUnknown->Release();
}
// Les méthodes de IInnerUnknown gèrent la durée de vie du composant :
HRESULT STDMETHODCALLTYPE CAdder::InnerQueryInterface(REFIID iid,
void **ppvObject)
{
// Initialisation du pointeur :
*ppvObject=0;
// Obtient le pointeur sur l'interface demandée :
// Le test suivant ne peut être vérifié que dans deux cas :
// - soit l'objet n'est pas aggrégé ;
// - soit il est aggrégé et la fonction de création demande
// l'interface IUnknown interne pour l'aggrégat.
// Dans les deux cas on doit renvoyer un pointeur
// sur l'interface IInnerUnknown :
if (iid==IID_IUnknown)
*reinterpret_cast<IInnerUnknown **>(ppvObject)=
static_cast<IInnerUnknown *>(this);
// Les tests suivants ne sont exécutés que lorsque l'objet
// n'est pas agrégé ou lorsque la fonction QueryInterface
// de l'agrégat délègue son travail :
else if (iid==IID_IAdder)
*reinterpret_cast<IAdder **>(ppvObject)=
static_cast<IAdder *>(this);
else if (iid==IID_IOpposite)
*reinterpret_cast<IOpposite **>(ppvObject)=
static_cast<IOpposite *>(this);
if (*ppvObject==0) return E_NOINTERFACE;
// Si l'interface est gérée, fixe le compte de références. Dans le
// cas des objets agrégés, on doit appeler AddRef pour l'agrégat.
// Dans les autres cas, ainsi que dans le cas de la création de
// l'objet agrégé dans un agrégat, on doit appeler AddRef de l'objet
// agrégé. Dans tous les cas, on peut appeler directement AddRef sur
// l'interface qui a été renvoyée :
reinterpret_cast<IUnknown *>(*ppvObject)->AddRef();
return NOERROR;
}
ULONG STDMETHODCALLTYPE CAdder::InnerAddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdder::InnerRelease(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
delete this;
return 0;
}
// Les méthodes des autres interfaces restent inchangées :
HRESULT STDMETHODCALLTYPE CAdder::Add(long i, long j, long *pResult)
{
*pResult=i+j;
return ;
}
HRESULT STDMETHODCALLTYPE CAdder::Sub(long i, long j, long *pResult)
{
*pResult=i-j;
return ;
}
HRESULT STDMETHODCALLTYPE CAdder::Opposite(long i, long *pResult)
{
*pResult=-i;
return ;
}
// Le code de création peut être implémenté de la manière suivante :
HRESULT CreateAdder(REFIID iid, IUnknown *pOuterUnknown, void **ppvObject)
{
// Initialisation du pointeur retourné :
*ppvObject=0;
// Vérification des paramètres pour l'agrégation :
if (pOuterUnknown!=0 && iid!=IID_IUnknown)
return CLASS_E_NOAGGREGATION;
// Création de l'objet :
CAdder *pAdder=new CAdder(pOuterUnknown);
pAdder->Init();
// Demande de l'interface désirée. On ne peut pas appeler
// QueryInterface directement parce que cette fonction appellerait
// la fonction QueryInterface de l'agrégat dans le cas de
// l'agrégation. On ne renverrait donc pas l'interface IUnknown
// interne de l'objet en cours de création :
return pAdder->InnerQueryInterface(iid, ppvObject);
}Heureusement, il est bien plus facile d'utiliser un composant qui gère l'agrégation que d'en écrire un. L'agrégat doit donner le pointeur sur son interface IUnknown externe (s'il est lui-même agrégé) au composant qu'il compte agréger lors de la création de celui-ci.
L'exemple suivant montre comment réaliser un agrégat qui gère lui-même l'agrégation.
Exemple 4. Aggrégation d'un composant dans un conteneur
// Implémentation de l'agrégat :
// Définition de l'interface IMultiplier de l'agrégat :
struct IMultiplier : public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Mul(long i, long j,
long *pResult)=0;
};
// Classe implémentant l'agrégat :
class CCalculator : public IInnerUnknown, public IMultiplier
{
IUnknown *m_pUnknown;
unsigned long m_ulRefCount;
// Pointeurs sur l'objet agrégé :
IUnknown *m_pAdderInnerUnknown;
public:
// Les méthodes de IInnerUnknown :
virtual HRESULT STDMETHODCALLTYPE InnerQueryInterface(REFIID iid,
void **ppvObject);
virtual ULONG STDMETHODCALLTYPE InnerAddRef(void);
virtual ULONG STDMETHODCALLTYPE InnerRelease(void);
// Les méthodes de IUnknown :
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid,
void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
// La méthode de IMultiplier :
virtual HRESULT STDMETHODCALLTYPE Mul(long i, long j, long *pResult);
// Constructeur, destructeur et autres fonctions d'initialisation :
CCalculator(IUnknown *pUnknown);
~CCalculator(void);
HRESULT Init(void);
};
CCalculator::CCalculator(IUnknown *pUnknown) : m_ulRefCount(0)
{
if (pUnknown!=0) m_pUnknown=pUnknown;
else m_pUnknown=reinterpret_cast<IUnknown *>(
static_cast<IInnerUnknown *>(this));
return ;
}
HRESULT CCalculator::Init(void)
{
// Protection contre les destructions prématurées
m_pIUnknown->AddRef();
// La fonction d'initialisation construit l'objet Adder
// que le Multiplier va utiliser :
return CreateAdder(IID_IUnknown, m_pUnknown,
reinterpret_cast<void **>(&m_pAdderInnerUnknown));
}
CCalculator::~CCalculator(void)
{
// Destruction des pointeurs stockés à usage interne.
// Ceci se fait en appelant la méthode AddRef de sa propre
// interface IUnknown externe pour fixer le compte de références
// à sa valeur correcte avant d'appeler Release sur le pointeur
// à libérer. Par exemple, si m_pAdder était un pointeur
// à usage interne, on écrirait :
// m_pUnknown->AddRef();
// m_pAdder->Release();
// Puis, on détruit le pointeur sur l'interface IUnknown interne
// de l'objet agrégé :
m_pAdderInnerUnknown->Release();
return ;
}
HRESULT STDMETHODCALLTYPE CCalculator::QueryInterface(REFIID iid,
void **ppvObject)
{
return m_pUnknown->QueryInterface(iid, ppvObject);
}
ULONG STDMETHODCALLTYPE CCalculator::AddRef(void)
{
return m_pUnknown->AddRef();
}
ULONG STDMETHODCALLTYPE CCalculator::Release(void)
{
return m_pUnknown->Release();
}
HRESULT STDMETHODCALLTYPE CCalculator::InnerQueryInterface(REFIID iid,
void **ppvObject)
{
*ppvObject=0;
if (iid==IID_IUnknown)
*reinterpret_cast<IInnerUnknown **>(ppvObject)=
static_cast<IInnerUnknown *>(this);
else if (iid==IID_IMultiplier)
*reinterpret_cast<IMultiplier **>(ppvObject)=
static_cast<IMultiplier *>(this);
// Si l'interface est gérée, on appelle AddRef et on retourne*
// NOERROR :
if (*ppvObject!=0)
{
reinterpret_cast<IUnknown *>(*ppvObject)->AddRef();
return NOERROR;
}
// Si l'interface n'est pas gérée directement, déléguer l'appel
// à QueryInterface des objets dont on est éventuellement constitué
// en tant qu'agrégat.
// Les tests sur les interfaces doivent être réalisés malgré tout
// afin d'éviter l'évolution incontrôlée des spécifications
// de l'agrégat avec l'évolution de ses constituants.
if (iid==IID_IAdder || iid==IID_IOpposite)
return m_pAdderInnerUnknown->QueryInterface(IID_IAdder,
ppvObject);
// Toutes les autres interfaces sont non reconnues :
return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE CCalculator::InnerAddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CCalculator::InnerRelease(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
// On doit prévenir les réentrances éventuelles avant le code de
// destruction. Ceci se fait classiquement en incrémentant
// artificiellement le compteur de références :
m_pUnknown->AddRef();
delete this;
return 0;
}
// Implémentation de IMultiplier :
HRESULT STDMETHODCALLTYPE CCalculator::Mul(long i, long j,
long *pResult)
{
*pResult=i*j;
return ;
}
// Fonction de création de l'agrégat :
HRESULT CreateCalculator(REFIID iid, IUnknown *pOuterUnknown,
void **ppvObject)
{
// Initialisation du pointeur retourné :
*ppvObject=0;
// Vérification des paramètres pour l'agrégation :
if (pOuterUnknown!=0 && iid!=IID_IUnknown)
return CLASS_E_NOAGGREGATION;
// Création de l'objet :
CCalculator *pCalculator=new CCalculator(pOuterUnknown);
pCalculator->Init();
// Demande de l'interface désirée :
HRESULT hResult=pCalculator->InnerQueryInterface(iid, ppvObject);
// Fixe le compteur de référence (supprime le AddRef de la fonction
// Init() :
pCalculator->Release();
return hResult;
}Dans l'exemple précédent, la fonction membre Init de CCalculator passe le pointeur sur son interface externe IUnknown à la fonction de création de l'objet agrégé :
En règle générale, lorsque l'on veut créer un composant dans le cadre de l'association en utilisant les mécanismes de DCOM, on passera le pointeur sur son interface IUnknown externe en deuxième paramètre de la fonction CoCreateInstance. La signature de cette fonction est rappelée ci-dessous :
HRESULT STDAPICALLTYPE CoCreateInstance
( REFCLSID rClsId , LPUNKNOWN pOuterUnknown , DWORD dwClsContext ,
REFIID rIId , LPVOID *ppInterface );
Les autres paramètres restent inchangés. On notera que d'après les règles d'agrégation, il est nécessaire que l'interface demandée soit l'interface IUnknown. Si ce n'est pas le cas, et que pOuterUnknown n'est pas nul, CoCreateInstance échouera.
Note: Cet exemple ne démontre pas la manière dont un agrégat peut stocker un pointeur interne sur un de ses constituants. Si une des méthodes devait créer un tel pointeur, elle devrait immédiatement appeler la fonction Release sur l'interface IUnknown externe de l'agrégat avec le code suivant :
Ceci impliquerait d'appeler la fonction AddRef avant la destruction de ce pointeur interne :
Cependant, la protection du code de destruction est quand même implémentée dans la fonction Release de l'interface IUnknown interne de l'agrégat.
On remarquera également que l'on ne peut pas appeler Release après avoir créé un pointeur à usage interne sur l'un de ses constituants dans la fonction d'initialisation Init, car dans ce cas le compteur de références viendrait tout juste de prendre la valeur 1. C'est pour cette raison, et pour ne pas nuire à la lisibilité du code de construction, que cet exemple ne démontre pas l'usage de ce type de pointeurs.
Enfin, on prendra garde au fait que tous les composants ne sont pas forcément capables d'être agrégés. Comme le choix de la technique à utiliser (agrégation ou délégation) doit être fait lors de l'écriture du conteneur, il est bon de vérifier que les composants que l'on utilise gèrent l'agrégation. De même, si vous voulez faciliter la vie de ceux pour qui utiliseront vos composants, pensez tout de suite à gérer l'agrégation.
Il n'est possible d'utiliser un composant que si l'on est capable d'en instancier au moins un objet et d'obtenir un pointeur sur une interface de ce composant. Dans le cas des serveurs in-process, ces deux opérations peuvent être réalisées facilement en écrivant une fonction globale du programme dans le serveur. Cette fonction peut créer une instance du composant et renvoyer un pointeur sur une de ces interfaces. Cette fonction devra simplement être exportée par la DLL pour que les clients puissent l'appeler.
Cependant, cette technique souffre de quelques défauts. Premièrement, elle ne fonctionne pas pour les serveurs exécutables, puisqu'il est impossible d'amener un objet créé dans un serveur exécutable dans l'espace d'adressage du processus client. Deuxièmement, cette technique n'est pas standard et ne peut pas être utilisée par les clients qui ne connaissent pas le nom de la fonction permettant de créer une instance d'un composant. Et troisièmement, les serveurs in-process ne peuvent tout simplement pas être distribués si l'on utilise cette technique.
DCOM spécifie donc un moyen standard de créer des objets, par l'intermédiaire de ce qu'on appelle une fabrique de classes. Une fabrique de classes est un composant qui est capable de créer des objets d'un composant particulier. En fait, le terme « fabrique de classes » n'est pas très approprié, puisqu'on ne fabrique absolument pas des composants, mais des instances de ces composants (rappelons que les composants DCOM sont aussi appelés des classes).
Chaque fabrique de classes ne sait créer des objets que d'un composant particulier. Il est donc nécessaire d'implémenter une fabrique de classes pour chaque composant que l'on écrit, ce qui est un peu fastidieux. Heureusement, la programmation des fabriques de classes est assez simple.
Les fabriques de classes implémentent toutes l'interface IClassFactory, qui donne les méthodes nécessaires à la création des objets du composant qu'elles représentent. Cette interface étant bien définie, DCOM dispose d'un moyen standard de créer des objets pour les composants disposant d'une fabrique de classe.
Il est également possible pour une fabrique de classes d'implémenter l'interface IClassFactory2, qui permet non seulement de créer des objets pour un composant, mais également de vérifier la license d'utilisation de ce composant. IClassFactory2 permet aussi d'obtenir une licence à partir d'un composant enregistré (si, bien entendu, le composant le veut bien).
La spécification des fabriques de classes n'est pas suffisante pour que DCOM puisse créer des instances d'un composant. En effet, il lui faut également spécifier comment il obtient ces fabriques de classes. Tous les composants DCOM qui implémentent une fabrique de classe devront donc s'enregistrer au niveau du système selon un protocole bien défini par DCOM. La technique employée pour parvenir à ce but dépend de la nature du serveur : DLL ou exécutable.
Bien entendu, il n'est absolument pas nécessaire de créer une fabrique de classe si le composant est destiné à l'usage privé d'un autre composant. En général dans ce cas, une fonction globale ou une méthode