FAQ sur DCOM/OLE

Christian Casteyde

casteyde.christian@free.fr

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.

1. Qu'est-ce qu'OLE ?
2. Qu'est-ce que COM ?
3. Qu'est-ce que DCOM ?
4. Quels sont les services fournis par DCOM ?
5. Quels sont les services fournis par OLE ?
6. Comment utilise-t-on un objet OLE dans une application ?
7. Comment utilise-t-on un objet OLE avec l'automation ?
8. Qu'est-ce qu'un serveur in-process ?
9. Qu'est-ce qu'un serveur out-of-process ?
10. Qu'est-ce qu'un GUID et un UUID ?
11. Quel est l'intérêt des GUID ?
12. Qu'est-ce qu'une interface ?
13. Comment obtient-on un pointeur sur une interface ?
14. À quoi sert l'interface IUnknown ?
15. Comment gère-t-on le cycle de vie des composants ?
16. Comment interpréter les codes HRESULT ?
17. Comment utilise-t-on un composant par programmation ?
18. Que faut-il faire pour créer un composant ?
19. Comment gère-t-on la mémoire des composants ?
20. Comment faire un composant qui gère plusieurs interfaces ?
21. Comment réutiliser des composants ?
22. Comment faire un composant qui gère l'agrégation ?
23. Comment utiliser un composant qui gère l'agrégation ?
24. Qu'est-ce qu'une fabrique de classe ?
25. Comment utilise-t-on une fabrique de classe ?
26. Comment réalise-t-on une fabrique de classe ?
27. Comment rend-on accessibles les fabriques de classes à DCOM ?
28. Comment gère-t-on la durée de vie des serveurs ?
29. Un exemple complet pour faire le point…
30. Comment compiler un composant ?
31. Comment le multithreading est-il géré ?
32. Qu'est-ce qu'un appartement ?
33. Qu'est-ce qu'un composant apartment threaded ?
34. Qu'est-ce qu'un composant free-threaded ?
35. Comment DCOM effectue-t-il les appels de méthodes entre deux appartements ?
36. Qu'est-ce que le marshalling ?
37. Que sont les stublets et les facelets ?
38. Qu'est-ce qu'une type library ?
39. Comment écrire un fichier IDL ?
40. Comment compiler les fichiers IDL ?
41. Comment compiler les fichiers produits par MIDL ?
42. Comment pour enregistrer un composant ?
43. Comment écrire un fichier .REG ?
44. Quelles sont les entrées à rajouter dans la base de registres pour enregistrer un composant ?
45. Quelles sont les entrées à rajouter dans la base de registre pour enregistrer une type library ?
46. Quels sont les mécanismes mis en ?uvre lors du marshalling ?
47. Comment les interfaces sont-elles marshallées ?
48. Comment est géré le compte des références sur les interfaces en cours de marshalling ?
49. Comment utiliser son propre marshalling ?
50. Comment marshaller une interface d'un thread à un autre ?
51. Quelles sont les registres à rajouter pour enregistrer les proxies et les stubs ?
52. Comment les interblocages sont-ils gérés ?
53. Peut-on rendre le système instable avec DCOM ?
54. Comment DCOM distribue-t-il les composants ?
55. Comment peut-on distribuer un composant ?
56. Quelles sont les entrées à rajouter de la base de registres pour distribuer un composant ?
57. Que faut-il faire pour configurer le système pour les composants distribués ?
58. Comment implémenter un surrogate process ?
59. Que sont les objets connectables ?
60. Qu'est-ce que OLE Automation ?
61. Qu'est-ce qu'une dispinterface ?
62. Qu'est-ce qu'un DISPID ?
63. À quoi sert l'interface IDispatch ?
64. Qu'est-ce qu'un VARIANT ?
65. Qu'est-ce qu'un BSTR ?
66. Qu'est-ce qu'un SAFEARRAY ?
67. Qu'est-ce qu'une interface duale ?
68. Quelles sont les limitations des dispinterfaces et des interfaces duales ?
69. Peut-on créer un composant disposant de plusieurs dispinterfaces ?
70. Quel type d'interface dois-je utiliser pour obtenir le maximum de performances ?
71. Je n'arrive pas à m'abonner à une interface événementielle duale à partir de Visual Basic…
72. Running Object Table, Monikers, Principales interfaces de OLE (ITypeLib, IClassFactory2, IStream, IPropertyNotifySink, etc…)
73. Dieu que c'est compliqué !
74. Mais c'est inutilisable !
75. Moi je fais un additionneur en 2 lignes en C…
76. Il n'y a pas plus simple ?
77. Que sont les ATL ?
78. Qu'est-ce qu'une classe template ?
79. Comment faire un composant simple avec les ATL ?
80. Comment utiliser un composant simple avec Visual Basic ?
81. Comment faire un composant simple avec Visual Basic ?
82. Comment utiliser un composant simple avec les ATL ?
83. Comment utiliser simplement un composant du même projet avec les ATL ?
84. Comment réalise-t-on un composant disposant de propriétés avec les ATL ?
85. Comment faire un composant gérant des événements avec les ATL ?
86. Comment ajouter la gestion des interfaces événementielles à un composant existant ?
87. Comment capter les événements d'un composant dans Visual Basic ?
88. Comment faire un composant contenant des propriétés en Visual Basic ?
89. Comment faire un composant gérant des événements en Visual Basic ?
90. Comment capter les événements automation d'un composant avec les ATL ?
91. Comment capter les événements sur une interface duale ?
92. Peut-on recevoir des événements automation sur une interface duale ?
93. Comment implémenter l'interface ISupportErrorInfo ?
94. Comment implémenter un point de connexion pour l'interface IPropertyNotifySink ?
95. Quels sont les problèmes que l'on peut rencontrer avec les ATL ?
96. J'ai trouvé une bogue dans les ATL !
97. Il me manque des définitions sur des constantes DIID_XXX ou IID_XXX !
98. Le compilateur n'arrive pas à récupérer l'adresse d'une constante DIID_XXX ou IID_XXX !
99. Le compilateur MIDL n'a pas généré les définitions des constantes des identificateurs d'interfaces !
100. Le code généré par le Wizard ne compile pas !
101. Je n'arrive pas à compiler le fichier d'un point de connexion !
102. Je n'arrive pas à instancier une classe parce qu'elle a des méthodes virtuelles pures, et pourtant toutes mes méthodes d'interfaces sont définies !
103. L'édition de liens de mon programme échoue avec des unresolved symbols, et pourtant toutes mes méthodes d'interfaces sont définies !
104. Mon programme plante dès que j'effectue une notification d'événement !
105. Les paramètres que je reçois dans mes méthodes d'interfaces événementielles sont complètement faux !
106. Certains des paramètres de mes méthodes d'interfaces événementielles sont corrompus !
107. AtlAdvise échoue, et pourtant je ne vois pas d'erreur !
108. Tout est correct, et pourtant je ne reçoit toujours aucune notification d'événement automation sur mon interface événementielle !
109. Les méthodes de mon interface duale ne sont pas appelées lorsqu'un client m'envoie une requête automation sur cette interface !
110. Je n'arrive pas à créer une page de propriété pour un de mes composants !
111. J'ai une erreur lors de l'enregistrement de mon composant !
112. L'enregistrement de mon composant échoue avec une erreur sur un ordinal introuvable dans la DLL ATL.DLL !
113. J'ai un conflit de version avec la librairie ATL.DLL !
114. Je n'arrive pas à créer un composant distribué en DCOM !
115. Je n'arrive pas à créer un composant distribué en Visual Basic, mais j'y arrive en C++ !
116. Je n'arrive pas à m'abonner aux événements d'un composant distribué 

1. Qu'est-ce qu'OLE ?

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.

2. Qu'est-ce que COM ?

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.

3. Qu'est-ce que DCOM ?

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.

4. Quels sont les services fournis par 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.

5. Quels sont les services fournis par OLE ?

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.

6. Comment utilise-t-on un objet OLE dans une application ?

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.

7. Comment utilise-t-on un objet OLE avec l'automation ?

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.

8. Qu'est-ce qu'un serveur in-process ?

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.

9. Qu'est-ce qu'un serveur out-of-process ?

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).

10. Qu'est-ce qu'un GUID et un UUID ?

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 :

6B29FC40-CA47-1067-B31D-00DD010662DA

Lorsque ces GUID sont stockés dans la base de registres, ils apparaissent entre accolades :

{6B29FC40-CA47-1067-B31D-00DD010662DA}

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.

11. Quel est l'intérêt des GUID ?

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.

12. Qu'est-ce qu'une interface ?

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.

13. Comment obtient-on un pointeur sur une interface ?

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).

14. À quoi sert l'interface IUnknown ?

15. Comment gère-t-on le cycle de vie des 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).

16. Comment interpréter les codes HRESULT ?

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.

17. Comment utilise-t-on un composant par programmation ?

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 :

HRESULT STDAPICALLTYPE CoInitialize ( LPVOID pReserved );

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 :

HRESULT STDAPICALLTYPE OleInitialize ( LPVOID pReserved );

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 :

IStorage *pStorage;
pUnknown->QueryInterface(IID_IStorage, &pStorage);

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é :

pUnknown->Release();
pStorage->Release();

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 :

void STDAPICALLTYPE CoUninitialize (void);

void STDAPICALLTYPE OleUninitialize (void);

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;
}

18. Que faut-il faire pour créer un composant ?

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 &colon; public IAdder
{
// Compte de références sur l'objet :
unsigned long m_ulRefCount;
public&colon;
// 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 :

CAdder&colon;&colon;CAdder(void) : m_ulRefCount(0)
{
return ;
}

Ensuite, la méthode AddRef doit se contenter d'incrémenter ce compteur :

ULONG STDMETHODCALLTYPE CAdder&colon;&colon;AddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}

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&colon;&colon;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&colon;&colon;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.

19. Comment gère-t-on la mémoire des composants ?

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 &colon; 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 :

HRESULT STDAPICALLTYPE CoGetMalloc ( DWORD dwReserved , LPMALLOC ppMalloc );

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&colon;&colon;Release :

void * STDAPI CoTaskMemAlloc ( ULONG cb );

void STDAPI CoTaskMemFree ( void * pv );

void * STDAPI CoTaskMemRealloc ( void *pv , ULONG cb );

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.

20. Comment faire un composant qui gère plusieurs interfaces ?

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 &colon; 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 &colon; public IAdder, public IOpposite
{
unsigned long m_ulRefCount;      // Le compteur de références.
public&colon;
// 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&colon;&colon;CAdder(void) : m_ulRefCount(0)
{
return ;
}
HRESULT CAdder&colon;&colon;Init(void)
{
return NOERROR;
}
// IUnknown :
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;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&colon;&colon;AddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdder&colon;&colon;Release(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
delete this;
return 0;
}
// IAdder :
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;Add(long i, long j, long *pResult)
{
*pResult=i+j;
return ;
}
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;Sub(long i, long j, long *pResult)
{
*pResult=i-j;
return ;
}
// IOpposite :
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;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.

21. Comment réutiliser des composants ?

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).

22. Comment faire un composant qui gère l'agrégation ?

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 &colon; 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 &colon; 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 &colon; public IInnerUnknown, public IAdder, public IOpposite
{
IUnknown *m_pUnknown;	// Pointeur sur l'interface IUnknown à utiliser.
unsigned long m_ulRefCount;
public&colon;
// 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&colon;&colon;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&colon;&colon;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&colon;&colon;QueryInterface(REFIID iid, void **ppvObject)
{
return m_pUnknown->QueryInterface(iid, ppvObject);
}
ULONG STDMETHODCALLTYPE CAdder&colon;&colon;AddRef(void)
{
return m_pUnknown->AddRef();
}
ULONG STDMETHODCALLTYPE CAdder&colon;&colon;Release(void)
{
return m_pUnknown->Release();
}
// Les méthodes de IInnerUnknown gèrent la durée de vie du composant :
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;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&colon;&colon;InnerAddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdder&colon;&colon;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&colon;&colon;Add(long i, long j, long *pResult)
{
*pResult=i+j;
return ;
}
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;Sub(long i, long j, long *pResult)
{
*pResult=i-j;
return ;
}
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;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);
}

23. Comment utiliser un composant qui gère l'agrégation ?

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 &colon; public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE Mul(long i, long j,
long *pResult)=0;
};
// Classe implémentant l'agrégat :
class CCalculator &colon; public IInnerUnknown, public IMultiplier
{
IUnknown *m_pUnknown;
unsigned long m_ulRefCount;
// Pointeurs sur l'objet agrégé :
IUnknown *m_pAdderInnerUnknown;
public&colon;
// 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&colon;&colon;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&colon;&colon;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&colon;&colon;~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&colon;&colon;QueryInterface(REFIID iid,
void **ppvObject)
{
return m_pUnknown->QueryInterface(iid, ppvObject);
}
ULONG STDMETHODCALLTYPE CCalculator&colon;&colon;AddRef(void)
{
return m_pUnknown->AddRef();
}
ULONG STDMETHODCALLTYPE CCalculator&colon;&colon;Release(void)
{
return m_pUnknown->Release();
}
HRESULT STDMETHODCALLTYPE CCalculator&colon;&colon;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&colon;&colon;InnerAddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CCalculator&colon;&colon;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&colon;&colon;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é :

CreateAdder(IID_IUnknown, m_pUnknown,
reinterpret_cast<void **>(&m_pAdderInnerUnknown));

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 :

m_pAdderInnerUnknown->QueryInterface(IID_IAdder, (void **) &m_pAdder);
m_pUnknown->Release();

Ceci impliquerait d'appeler la fonction AddRef avant la destruction de ce pointeur interne :

m_pUnknown->AddRef();
m_pAdder->Release();

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.

24. Qu'est-ce qu'une fabrique de classe ?

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 d'une interface d'un autre composant est fournie pour que les clients puissent instancier ce composant et obtenir une interface. Ce type de technique est tout à fait valide mais restreint sérieusement la portée des composants.

Note: Il est nécessaire d'implémenter une fabrique de classe et une seule pour chacun des composants dont vous voulez autoriser la création par l'intermédiaire de DCOM. Cependant, un même serveur peut implémenter plusieurs composants. Le protocole utilisé par DCOM pour accéder aux fabriques de classes des serveurs peut gérer un nombre arbitraire de fabriques de classes.

25. Comment utilise-t-on une fabrique de classe ?

Toutes les fabriques de classes implémentent l'interface IClassFactory. Cette interface est déclarée comme suit :

struct IClassFactory &colon; public IUnknown
{
virtual HRESULT STDMETHODCALLTYPE CreateInstance(
IUnknown *pOuterUnknown, REFIID iid, void **ppvObject)=0;
virtual HRESULT STDMETHODCALLTYPE LockServer(BOOL bLock)=0;
};

La méthode la plus importante dans cette interface est CreateInstance, parce que c'est la méthode qui est appelée pour créer une nouvelle instance du composant que la fabrique gère. Le premier paramètre est le pointeur sur l'interface IUnknown externe passé par l'agrégat à ses constituants dans le cadre de l'agrégation. Le deuxième paramètre doit recevoir le IID de l'interface que l'on cherche à obtenir sur l'objet que la fabrique de classe va instancier. Enfin, le troisième paramètre est l'adresse du pointeur qui recevra le pointeur sur l'interface obtenue.

La deuxième méthode de l'interface IClassFactory permet de contrôler la durée de vie du serveur qui implémente la fabrique de classe. En général, un serveur reste chargé en mémoire tant qu'il existe des références sur des objets de ce serveur. La seule exception à cette règle concerne les fabriques de classes : le fait de posséder un pointeur sur l'interface d'une fabrique de classe ne garantit pas la durée de vie de serveur. La raison de cette exception est détaillée dans le paragraphe qui traite de la gestion de la durée de vie des serveurs exécutables. Quoi qu'il en soit, il est possible de maintenir un serveur vivant même si aucun objet ne dispose de référence sur l'un de ses objets. Pour cela, il suffit d'appeler la fonction LockServer sur une des fabriques de classes de ce serveur avec la valeur TRUE pour le paramètre. Pour libérer le serveur, il suffit d'appeler LockServer avec la valeur FALSE.

Les fabriques de classes peuvent remplacer avantageusement la fonction CoCreateInstance dans les cas où l'on cherche à créer plusieurs instances d'un même composant. On peut en effet s'adresser directement à la méthode CreateInstance pour créer les instances. En fait, même CoCreateInstance utilise une fabrique de classe pour instancier un composant.

Pour pouvoir utiliser une fabrique de classe, il faut d'abord obtenir un pointeur sur l'interface IClassFactory. Cette opération est réalisée en appelant la fonction CoGetClassObject de DCOM, dont la déclaration est donnée ci-dessous :

HRESULT STDAPICALLTYPE CoGetClassObject ( REFCLSID clsid , DWORD dwClsContext , COSERVERINFO *pServerInfo , REFIID iid , LPVOID *ppvObject );

Le premier paramètre de cette fonction est une référence sur le CLSID du composant dont on cherche à créer un objet. Comme les fabrique de classe ne peuvent instancier qu'un seul type de composant, et que les composants enregistrés au niveau de DCOM doivent tous disposer d'une fabrique de classe, il y a bijection entre les fabriques de classe et les composants qu'elles peuvent instancier. Il suffit donc de donner le CLSID du composant pour que DCOM puisse retrouver la fabrique de classe à utiliser. Le deuxième paramètre indique le contexte d'exécution du composant. Ce paramètre peut prendre exactement les mêmes valeurs que celles données pour le paramètre correspondant de la fonction CoCreateInstance. Le troisième paramètre permet de spécifier sur quel ordinateur le composant doit être exécuté. Ce paramètre peut être nul, dans ce cas, certaines entrées de la base de registre donnent les options par défaut que DCOM doit utiliser pour ce paramètre. Le quatrième paramètre est la référence sur l'IID de l'interface que l'on cherche à obtenir sur la fabrique de classe. En pratique, on demandera toujours IUnknown, IClassFactory ou IClassFactory2. Enfin, le dernier paramètre est l'adresse du pointeur qui recevra le pointeur sur l'interface de la fabrique de classe demandée.

La structure COSERVERINFO est définie comme suit :

typedef struct COSERVERINFO
{
DWORD dwReserved1;
LPWSTR pwszName;
COAUTHINFO  *pAuthInfo;
DWORD dwReserved2;
} COSERVERINFO;

Dans le cadre d'une utilisation normale, seul le deuxième paramètre est utilisé, tous les autres paramètres doivent être nuls. Ce paramètre contient le nom de l'ordinateur qui exécutera le serveur. Ce nom doit être exprimé en Unicode.

L'exemple suivant démontre l'utilisation des fabriques de classes par un client.

Exemple 5. Utilisation d'une fabrique de classe

#include <objbase.h>
#include <stdio.h>
#include "adder.h"
int main(void)
{
if (SUCCEEDED(CoInitialize(0)))
{
IClassFactory *pFactory;
if (SUCCEEDED(CoGetClassObject(CLSID_Calculator, CLSCTX_ALL,
NULL, IID_IClassFactory, (void **) &pFactory)))
{
IAdder *pAdder;
pFactory->CreateInstance(NULL, IID_IAdder, (void **) &pAdder);
pFactory->Release();
long lResult;
pAdder->Add(2, 3, &lResult);
pAdder->Release();
printf("2+3=%d\n", lResult);
}
CoUninitialize();
}
return 0;
}

26. Comment réalise-t-on une fabrique de classe ?

Pour implémenter une fabrique de classe, il suffit de créer un composant qui gère l'interface IClassFactory. Cette opération est extrêmement simple, puisque les fabriques de classes sont des composants simples, qui ne gèrent pas l'agrégation et qui peuvent être implémentées sans avoir à gérer plusieurs interfaces (l'interface IClassFactory2 complète l'interface IClassFactory, qui elle-même étend l'interface IUnknown).

L'exemple donné ci-dessous montre comment on peut implémenter une fabrique de classe pour le composant additionneur vu ci dessus.

Exemple 6. Réalisation d'une fabrique de classe

// Fabrique de classe pour le composant Adder :
static unsigned long ulObjectCount=0;
static unsigned long ulLockCount=0;
class CAdderFactory &colon; public IClassFactory
{
unsigned long m_ulRefCount;
public&colon;
// Les méthodes de IUnknown :
virtual HRESULT STDMETHODCALLTYPE QueryInterface(
REFIID iid, void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
// Les méthodes de IClassFactory :
virtual HRESULT STDMETHODCALLTYPE CreateInstance(
IUnknown *pOuterUnknown, REFIID iid, void **ppvObject);
virtual HRESULT STDMETHODCALLTYPE LockServer(BOOL bLock);
CAdderFactory(void);
};
CAdderFactory&colon;&colon;CAdderFactory(void) : m_ulRefCount(0)
{
return ;
}
HRESULT STDMETHODCALLTYPE CAdderFactory&colon;&colon;QueryInterface(
REFIID iid, void **ppvObject)
{
*ppvObject=0;
if (iid==IID_IUnknown) *reinterpret_cast<IUnknown **>(ppvObject)=
static_cast<IUnknown *>(this);
else if (iid==IID_IClassFactory)
*reinterpret_cast<IClassFactory **>(ppvObject)=
static_cast<IClassFactory *>(this);
if (*ppvObject==0) return E_NOINTERFACE;
AddRef();
return NOERROR;
}
ULONG STDMETHODCALLTYPE CAdderFactory&colon;&colon;AddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdderFactory&colon;&colon;Release(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
delete this;
return 0;
}
// Les méthodes de IClassFactory :
HRESULT STDMETHODCALLTYPE CAdderFactory&colon;&colon;CreateInstance(
IUnknown *pOuterUnknown, REFIID iid, void **ppvObject)
{
// Initialise la valeur de retour :
*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);
ulObjectCount++;
if (pAdder==0)
{
// Détruit le serveur si nécessaire :
ulObjectCount—;
return E_OUTOFMEMORY;
}
// Initialise l'objet et demande l'interface désirée :
HRESULT hResult=E_FAIL;
if (SUCCEEDED(pAdder->Init()))
{
hResult=pAdder->InnerQueryInterface(iid, ppvObject);
// Supprime l'éventuel AddRef appelé dans Init() :
// pAdder->Release();
}
if (FAILED(hResult))
{
delete pAdder;
ulObjectCount—;
}
return hResult;
}
HRESULT STDMETHODCALLTYPE CAdderFactory&colon;&colon;LockServer(BOOL bLock)
{
if (bLock) ulLockCount++;
else
{
if (ulLockCount!=0) ulLockCount—;
}
return NOERROR;
}

Cet exemple montre clairement comment la fonction CreateInstance est semblable au code de la fonction CreateAdder. Les seules différences proviennent d'une meilleure gestion des erreurs et de la mise à jour du compteur global ulObjetCount. Cette similitude n'est pas l'?uvre du hasard : en fait, la fonction méthode CreateInstance est prévue pour remplacer purement et simplement la fonction CreateAdder. Le fait que cette fonction soit accessible par une interface bien définie par DCOM donne la possibilité de créer une instance de l'additionneur indépendamment de tout contexte sur le serveur et sur le nom du composant.

Note: La gestion des erreurs de la fonction CreateAdder a été volontairement simplifiée pour ne pas nuire à la clarté de l'exemple précédent. En fait, les erreurs doivent être gérées comme le fait la méthode CreateInstance.

Les variables globales ulObjectCount et ulLockCount ne sont utilisées que pour contrôler la durée de vie du serveur. La méthode Release de l'interface IInnerUnknown de CAdder doit également être modifiée pour décrémenter ulObjectCount dans le code de destruction, puisque cette valeur augmente de un à chaque création d'un objet.

Pour les serveurs multithreadés, les modifications de ces variables globales doivent être atomiques. On pourra utiliser les fonctions de l'API Windows pour assurer cette condition, ou les protéger dans une section critique.

La méthode LockServer permet d'augmenter et de diminuer la valeur du compteur ulLockCount, selon la valeur passée en paramètre (TRUE ou FALSE).

27. Comment rend-on accessibles les fabriques de classes à DCOM ?

Le fait d'encapsuler le code de création des composants dans les fabriques de classes a permis de rendre cette création indépendante de la nature du composant. Cependant, cela n'est pas suffisant, puisqu'il faut fournir à DCOM le moyen d'accéder aux fabriques de classes. Ce moyen dépend de la nature du serveur.

Pour les serveurs in-process, c'est DCOM qui demande la fabrique de classe lorsqu'un client appelle CoGetClassObject. Il le fait en appelant la fonction DllGetClassObject, que tout serveur in-process doit exporter. Cette fonction est déclarée comme suit :

HRESULT STDAPICALLTYPE DllGetClassObject ( REFCLSID clsid , REFIID iid , LPVOID *ppvFactory );

Le premier paramètre de cette fonction est le CLSID du composant dont on veut créer une instance. Le deuxième paramètre est l'IID de l'interface que l'on veut obtenir sur la fabrique de classe, interface dont l'adresse est retournée dans le pointeur référencé par ppvFactory. Les seules interfaces que l'on peut demander sont IUnknown, IClassFactory et IClassFactory2.

En revanche, pour les serveurs exécutables, aucune fonction ne peut être exportée. Il faut donc que ce soit le serveur qui enregistre ses fabriques de classes auprès de DCOM. Il doit donc instancier une fabrique de classe pour chacun des composants qu'il peut gérer, et appeler la fonction CoRegisterClassObject pour chacune de ces fabriques dans son code d'initialisation. Cette fonction est déclarée ainsi dans le fichier d'en-tête objbase.h :

HRESULT STDAPICALLTYPE CoRegisterClassObject ( REFCLSID clsid , IUnknown *pUnknown , DWORD dwClsContext , DWORD dwFlags , LPDWORD *pKey );

Le premier paramètre est bien entendu le CLSID du composant dont on est en train d'enregistrer la fabrique de classe. Le deuxième paramètre est le pointeur sur l'interface IUnknown de cette fabrique de classe. Le troisième paramètre est le contexte dans lequel ce composant peut fonctionner. Les différentes valeurs correspondent aux valeurs que le client passe à CoCreateInstance. Le quatrième paramètre est donne des attributs au composant. Ce paramètre peut prendre principalement les valeurs REGCLS_SINGLEUSE et REGCLS_MULTIPLEUSE. La première valeur indique qu'un serveur doit être démarré pour chaque composant créé, et la deuxième qu'un même serveur peut gérer plusieurs composants. Enfin, le dernier paramètre est une clé qui identifie la fabrique de classe dans les tables interne de DCOM. Cette valeur doit être conservée pour pouvoir la supprimer lors de la terminaison du serveur.

Note: On fera attention avec le paramètre dwClsContext. Pour les postes Windows 95 sur lesquels DCOM n'est pas installé, il est impossible d'utiliser l'option CLSCTX_REMOTE_SERVER. Ceci provoque une erreur et le composant n'est pas enregistré. Ceci est une bogue grave de COM, puisque les composants distribués doivent être recompilés et réinstallés lorsque l'on installe DCOM.

Dans le code de terminaison des serveurs exécutables, il est nécessaire de retirer ces fabriques des tables internes de DCOM. Ceci est réalisé à l'aide de la fonction CoRevokeClassObject, dont la signature est donnée ci-dessous :

HRESULT STDAPICALLTYPE CoRevokeClassObject ( DWORD dwKey );

Le paramètre de cette fonction est la clé que DCOM a renvoyé lors de l'enregistrement de la fabrique de classe avec CoRegisterClassObject.

Note: Rien n'empêche un serveur de gérer plusieurs composants. Pour cela, il suffit de faire le test sur le CLSID du composant dans DllGetClassObject, ou d'enregistrer plusieurs composants à l'aide de plusieurs appels à CoRegisterClassObject.

28. Comment gère-t-on la durée de vie des serveurs ?

Dans l'exemple précédent, l'introduction des variables globales ulObjectCount et ulLockCount a été justifiée par la gestion de la durée de vie des serveurs.

Nous avons vu que dans le cas des serveurs exécutables, les fabriques de classes sont créées et enregistrées dans le système par l'intermédiaire de la fonction CoRegisterClassObject. Ceci signifie que DCOM tient une référence en permanence sur les fabriques de classes des serveurs exécutables. Ces références ne sont libérées que lors de l'appel de CoRevokeClassObject, qui elle-même n'est appelée que lors de la destruction du serveur. Par conséquent, il faut que les serveurs exécutables ne prennent pas en compte les fabriques de classes lorsqu'ils déterminent s'ils peuvent se terminer ou non. En effet, s'ils le faisaient, on serait en présence d'une référence circulaire, et ils deviendraient immortels.

C'est essentiellement pour cette raison que seuls les composants autres que les fabriques de classes sont comptabilisés dans la variable globale ulObjectCount. Cependant, il peut être intéressant à certains moments de maintenir un serveur en vie même si plus aucun objet de ce serveur n'est en vie, ne serait-ce que pour diminuer le nombre de chargement de processus par l'ordinateur. C'est ce à quoi la méthode LockServer de l'interface IClassFactory est dédiée. Elle permet de maintenir un compte artificiel de références sur les fabriques de classes, au niveau de la variable globale ulLockCount.

Il aurait été possible de réaliser un compteur commun pour les objets et le blocage des fabriques de classes, mais ce n'est pas une technique sûre. En effet, il aurait été possible, par appels successifs de LockServer, de forcer la terminaison d'un serveur contenant encore des objets actifs (appartenant éventuellement à d'autres clients de surcroît). Il est donc recommandé de ne pas utiliser cette technique. En fait, il est même conseillé d'utiliser une variable de compte de blocages pour chacune des fabriques de classes que le serveur contient.

Pour les serveurs in-process, DCOM ne maintient pas de références sur les fabriques de classes des serveurs. Il est donc possible dans ce cas de compter les références sur les fabriques de classe avec les références sur les objets, ce qui rend facultatif l'emploi de la méthode LockServer. Cependant, cette méthode existe et doit être implémentée. La variable ulLockCount est donc implémentée, même dans les serveurs in-process.

Quel que soit le type de serveur, la détermination de la fin de celui-ci est donc exclusivement basée sur les valeurs des deux variables globales ulObjectCount et ulLockCount. Un serveur sait qu'il peut se terminer quand ces deux valeurs sont nulles.

Pour les serveurs exécutables, ce test est effectué à chaque appel de la méthode LockServer de IClassFactory avec FALSE pour paramètre, et à chaque destruction d'objet. S'il s'avère que le serveur doit se terminer, il lui suffit de poster le message WM_CLOSE à sa procédure de fenêtre.

Pour les serveurs in-process, le mécanisme de gestion de la durée de vie est plus compliqué. En effet, le serveur est lié au processus en tant que DLL, et ne peut a priori pas se décharger de lui-même. Le principe de déchargement est donc le suivant :

  • DCOM appelle régulièrement la fonction DllCanUnloadNow, que doit exporter tout serveur in-process ;

  • cette fonction détermine si la DLL peut être déchargée en fonction des objets encore créés et donne la réponse à DCOM ;

  • celui-ci décharge ou non la DLL.

Aucun test n'est donc réalisé lors de la modification des compteurs, en revanche, ces tests sont effectués à l'initiative de DCOM.

La signature de la fonction DllCanUnloadNow est donnée ci-dessous :

HRESULT STDAPICALLTYPE DllCanUnloadNow (void);

Cette fonction doit être exportée. Elle renvoie S_OK si le serveur peut être détruit, S_FALSE dans le cas contraire. Son implémentation type est donnée ci-dessous :

HRESULT STDAPICALLTYPE DllCanUnloadNow(void)
{
if (ulObjectCount==0 && ulLockCount==0) return S_OK;
else return S_FALSE;
}

Bien que les mécanismes de terminaison des serveurs dépendent légèrement de la nature du serveur, il est possible d'implémenter les objets et leurs fabriques de classes d'une manière totalement uniforme.

La technique à utiliser est simple : il suffit d'écrire une fonction globale ObjectDestroyed qui est appelée à chaque fois qu'un objet est détruit. Cette fonction décrémente le compteur ulObjectCount. Pour les serveurs in-process, elle se contente de cette action, mais pour les serveurs exécutables, elle effectue le test sur ulObjectCount et sur ulLockCount, et poste éventuellement le message WM_CLOSE à la procédure de fenêtre principale du serveur. Le code générique de ObjectDestroyed est donné ci-dessous :

static void ObjectDestroyed(void)
{
ulObjectCount—;
#ifdef EXESERVER
// S'il n'y a pas de blocage et plus d'objet, on détruit le serveur :
if (ulObjectCount==0 && ulLockCount==0)
PostMessage(hWnd, WM_CLOSE, 0, 0);
#endif
return ;
}

Le code de destruction des objets du serveur doit être modifié pour appeler ObjectDestroyed. Afin de rendre le code de ces objets indépendants de cette fonction, on pourra stocker un pointeur sur ObjectDestroyed dans les objets et déréférencer ce pointeur. Le code typique de Release pour les objets est donc le suivant :

ULONG STDMETHODCALLTYPE CCalculator&colon;&colon;InnerRelease(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
// Appel de la fonction ObjectDestroyed :
m_pfnDestroyed();
// Protection contre la réentrance :
m_pUnknown->AddRef();
delete this;
return 0;
}

Le pointeur m_pfnDestroyed pourra être initialisé lors de la création de l'objet en passant en paramètre l'adresse de ObjectDestroyed au constructeur.

La méthode LockServer doit également être adaptée pour utiliser la fonction ObjectDestroyed. En effet, pour les serveurs exécutables, il faut tester si le serveur peut se terminer à chaque fois que le compteur de blocage est décrémenté. Ceci peut être forcé par la séquence suivante :

ulObjectCount++;
ObjectDestroyed();

qui ne modifie pas le compteur d'objet mais appelle malgré tout ObjectDestroyed.

29. Un exemple complet pour faire le point…

L'exemple qui est donné dans ce paragraphe regroupe toutes les notions que l'on a pu voir jusqu'à présent. Les commentaires ont été simplifiés, la plupart des explications ayant déjà été données. Dans les exemples qui suivent, les fichiers contenant les déclarations et les définitions des interfaces portent le nom xxx.h et xxx_i.c, où xxx représente le nom du composant. Comme nous le verrons plus loin, ces fichiers peuvent être générés automatiquement. Ces fichiers ont été placés dans un répertoire dont le nom se termine par Prx (comme « Proxy », nous verrons plus loin la signification de ce terme). Le source de ces fichiers n'est donc pas donné, mais il ne contient aucune spécificité.

Voici le fichier source d'en-tête déclarant un composant Adder et sa fabrique de classe :

Exemple 7. Fichier d'en-tête du composant Adder

#ifndef __ADDER_H__
#define __ADDER_H__
// Vérification de la nature du serveur :
#if !defined(EXESERVER) && !defined(DLLSERVER)
#error "Error : EXESERVER or DLLSERVER macro must be defined !"
#endif
// Prototype de la fonction de signal de destruction d'objet :
typedef void (&ObjectDestroyedFunction)(void);
// Définition de l'interface IInnerUnknown pour l'agrégation :
struct IInnerUnknown
{
virtual HRESULT STDMETHODCALLTYPE InnerQueryInterface(REFIID iid,
void **ppvObject)=0;
virtual ULONG STDMETHODCALLTYPE InnerAddRef(void)=0;
virtual ULONG STDMETHODCALLTYPE InnerRelease(void)=0;
};
// Déclaration de la classe implémentant le composant.
// Cette classe hérite des interfaces que le composant gère :
class CAdder &colon; public IInnerUnknown, public IAdder, public IOpposite
{
// Données membres de gestion du composant :
// Pointeur sur l'interface IUnknown à utiliser :
IUnknown *m_pUnknown;
// Fonction de signal de destruction d'objet :
ObjectDestroyedFunction m_ObjectDestroyed;
// Compteur de références :
unsigned long m_ulRefCount;
public&colon;
// 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, ObjectDestroyedFunction ObjectDestroyed);
HRESULT Init(void);
};
// Déclaration de la classe implémentant la fabrique de classe :
class CAdderFactory &colon; public IClassFactory
{
unsigned long m_ulRefCount;
public&colon;
// Les méthodes de IUnknown :
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid,
void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
// Les méthodes de IClassFactory :
virtual HRESULT STDMETHODCALLTYPE CreateInstance(
IUnknown *pOuterUnknown, REFIID iid, void **ppvObject);
virtual HRESULT STDMETHODCALLTYPE LockServer(BOOL bLock);
// Le constructeur :
CAdderFactory(void);
};
#endif

Voici le fichier source contenant l'implémentation de ce composant Adder :

Exemple 8. Implémentation C du composant Adder

#include <windows.h>
#include <objbase.h>
#include "AdderPrx/Adder.h"
#include "adder.h"
//////////////////////////////////////////////////////////
//      Implémentation du composant Adder               //
//////////////////////////////////////////////////////////
// Constructeur :
CAdder&colon;&colon;CAdder(IUnknown *pUnknown, ObjectDestroyedFunction
ObjectDestroyed) : m_ObjectDestroyed(ObjectDestroyed),
m_ulRefCount(0)
{
// On détermine l'interface IUnknown à utiliser :
if (pUnknown!=0)
// Interface externe de l'agrégat
// (passée 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 ;
}
// La fonction d'initialisation ne fait rien ici :
HRESULT CAdder&colon;&colon;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&colon;&colon;QueryInterface(REFIID iid,
void **ppvObject)
{
return m_pUnknown->QueryInterface(iid, ppvObject);
}
ULONG STDMETHODCALLTYPE CAdder&colon;&colon;AddRef(void)
{
return m_pUnknown->AddRef();
}
ULONG STDMETHODCALLTYPE CAdder&colon;&colon;Release(void)
{
return m_pUnknown->Release();
}
// Les méthodes de IInnerUnknown gèrent la durée de vie du composant :
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;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&colon;&colon;InnerAddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdder&colon;&colon;InnerRelease(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
// Signal de destruction de l'objet :
m_ObjectDestroyed();
// Protection contre les réentrances et destruction :
m_pUnknown->AddRef();
delete this;
return 0;
}
// Implémentation des méthodes des autres interfaces :
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;Add(long i, long j, long *pResult)
{
*pResult=i+j;
return NOERROR;
}
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;Sub(long i, long j, long *pResult)
{
*pResult=i-j;
return NOERROR;
}
HRESULT STDMETHODCALLTYPE CAdder&colon;&colon;Opposite(long i, long *pResult)
{
*pResult=-i;
return NOERROR;
}
//////////////////////////////////////////////////////////
//        Fabrique de classe pour le composant          //
//////////////////////////////////////////////////////////
// Compteurs pour la durée de vie du composant :
static unsigned long ulObjectCount=0;        // Nombre d'objets
static unsigned long ulLockCount=0;          // Nombre de blocages.
#ifdef EXESERVER
// Handle de la fenêtre principale du serveur dans le cas
// des serveurs exécutables :
static HWND hServerWindow;
#endif
// La fonction de signalisation de destruction d'un objet :
static void ObjectDestroyed(void)
{
// Décrémente le compteur d'objet :
ulObjectCount—;
#ifdef EXESERVER
// Dans le cas des serveurs exécutables, effectue le test
// de destruction du serveur :
if (ulObjectCount==0 && ulLockCount==0)
PostMessage(hServerWindow, WM_CLOSE, 0,0);
#endif
return ;
}
CAdderFactory&colon;&colon;CAdderFactory(void) : m_ulRefCount(0)
{
return ;
}
HRESULT STDMETHODCALLTYPE CAdderFactory&colon;&colon;QueryInterface(REFIID iid,
void **ppvObject)
{
*ppvObject=0;
if (iid==IID_IUnknown)
*reinterpret_cast<IUnknown **>(ppvObject)=
static_cast<IUnknown *>(this);
else if (iid==IID_IClassFactory)
*reinterpret_cast<IClassFactory **>(ppvObject)=
static_cast<IClassFactory *>(this);
if (*ppvObject==0) return E_NOINTERFACE;
AddRef();
return NOERROR;
}
ULONG STDMETHODCALLTYPE CAdderFactory&colon;&colon;AddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdderFactory&colon;&colon;Release(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
delete this;
return 0;
}
// Les méthodes de IClassFactory :
HRESULT STDMETHODCALLTYPE CAdderFactory&colon;&colon;CreateInstance(
IUnknown *pOuterUnknown, REFIID iid, void **ppvObject)
{
// Initialise la valeur de retour :
*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, ObjectDestroyed);
ulObjectCount++;
if (pAdder==0)
{
// Détruit le serveur si nécessaire :
ObjectDestroyed();
return E_OUTOFMEMORY;
}
// Initialise l'objet et demande l'interface désirée :
HRESULT hResult=E_FAIL;
if (SUCCEEDED(pAdder->Init()))
hResult=pAdder->InnerQueryInterface(iid, ppvObject);
if (FAILED(hResult))
{
delete pAdder;
ObjectDestroyed();
}
return hResult;
}
HRESULT STDMETHODCALLTYPE CAdderFactory&colon;&colon;LockServer(BOOL bLock)
{
if (bLock) ulLockCount++;
else
{
if (ulLockCount!=0)
{
ulLockCount—;
// Force le test de destruction du serveur :
ulObjectCount++;
ObjectDestroyed();
}
}
return NOERROR;
}
//////////////////////////////////////////////////////////
//        Code d'enregistrement du composant            //
//////////////////////////////////////////////////////////
// Le code d'enregistrement n'est pas le même selon
// la nature du composant :
#ifdef DLLSERVER
// Serveur in-process.
#ifdef INITDLL
BOOL APIENTRY LibMain(HANDLE /* hDLL */,
DWORD    /* dwReason */,
LPVOID    /*pReserved*/)
{
return TRUE;
}
#endif
// Fonction d'autorisation de déchargement :
STDAPI DllCanUnloadNow(void)
{
if (ulObjectCount==0 && ulLockCount==0) return S_OK;
else return S_FALSE;
}
// Fonction d'enregistrement de la fabrique de classe :
STDAPI DllGetClassObject(REFCLSID clsid, REFIID iid, LPVOID *ppvFactory)
{
// On initialise le pointeur retourné :
*ppvFactory=0;
// On vérifie que le serveur gère bien la classe d'objet dont on
// cherche à créer une fabrique :
if (clsid!=CLSID_Adder) return CLASS_E_CLASSNOTAVAILABLE;
// On crée la fabrique d'objets pour cette classe :
CAdderFactory *pFactory=new CAdderFactory();
if (pFactory==0) return E_OUTOFMEMORY;
// On demande l'interface désirée pour la fabrique d'objet
// (normalement, ce doit être IClassFactory ou IUnknown) :
HRESULT hr=pFactory->QueryInterface(iid, ppvFactory);
if (FAILED(hr)) delete pFactory;
return hr;
}
#endif
#ifdef EXESERVER
// Serveur exécutable :
// Procédure de fenêtre principale du serveur :
LRESULT CALLBACK WindowProc(HWND hWindow, UINT dMessage,
WPARAM lParam1, LPARAM lParam2)
{
LRESULT lResult;
switch(dMessage)
{
case WM_DESTROY :
PostQuitMessage(0);
lResult=0;
break ;
default :
lResult=DefWindowProc(hWindow, dMessage, lParam1, lParam2);
break ;
}
return lResult;
}
// Fonction principale du serveur :
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE,
LPSTR lpCmdLine, int iCmdShow)
{
// Vérifie la ligne de commande :
if (lstrcmpiA(lpCmdLine, "-Embedding") &&
lstrcmpiA(lpCmdLine, "/Embedding")) return FALSE;
// Initialise COM :
if (FAILED(CoInitialize(NULL))) return FALSE;
// Crée la fabrique de classe :
static CAdderFactory *pFactory=new CAdderFactory();
if (pFactory==NULL)
{
CoUninitialize();
return FALSE;
}
pFactory->AddRef();
// Enregistre le serveur auprès de COM :
static DWORD dwClassObjectKey;
if (FAILED(CoRegisterClassObject(
CLSID_Adder, static_cast<IUnknown *>(pFactory),
CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER,
/* Supprimer CLSCTX_REMOTE_SERVER sur Windows 95 sans DCOM */
REGCLS_MULTIPLEUSE, &dwClassObjectKey)))
{
pFactory->Release();
CoUninitialize();
return FALSE;
}
// Enregistre la classe de la fenêtre du serveur :
WNDCLASS wc;
wc.style = 0;
wc.lpfnWndProc = (WNDPROC) &WindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon((HINSTANCE) NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor((HINSTANCE) NULL, IDC_ARROW);
wc.hbrBackground = GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = "AdderServerWindowClass";
if (!RegisterClass(&wc))
{
CoRevokeClassObject(dwClassObjectKey);
pFactory->Release();
CoUninitialize();
return FALSE;
}
// Crée la fenêtre principale :
hServerWindow=CreateWindow("AdderServerWindowClass", "Adder Server",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, (HWND) NULL,
(HMENU) NULL, hInstance, (LPVOID) NULL);
if (!hServerWindow)
{
CoRevokeClassObject(dwClassObjectKey);
pFactory->Release();
CoUninitialize();
return FALSE;
}
ShowWindow(hServerWindow, iCmdShow);
UpdateWindow(hServerWindow);
MSG msg;
// Boucle de gestion des messages :
while (GetMessage(&msg, (HWND) NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Désenregistre le serveur auprès de COM :
CoRevokeClassObject(dwClassObjectKey);
// Détruit la fabrique de classe :
pFactory->Release();
// Termine COM :
CoUninitialize();
// Renvoie le code d'erreur :
return msg.wParam;
}
#endif

Voici le fichier source d'en-tête déclarant un composant Calculator et sa fabrique de classe. Ce composant utilise le composant Adder en tant qu'objet agrégé :

Exemple 9. Fichier d'en-tête du composant Calculator

// Fichier d'en-tête du composant CALCULATOR :
#ifndef __CALCULATOR_H__
#define __CALCULATOR_H__
// Vérification de la nature du serveur :
#if !defined(EXESERVER) && !defined(DLLSERVER)
#error "Error : EXESERVER or DLLSERVER macro must be defined !"
#endif
// Prototype de la fonction de signal de destruction d'oobjet :
typedef void (&ObjectDestroyedFunction)(void);
// Définition de l'interface IInnerUnknown pour l'agrégation :
struct IInnerUnknown
{
virtual HRESULT STDMETHODCALLTYPE InnerQueryInterface(REFIID iid,
void **ppvObject)=0;
virtual ULONG STDMETHODCALLTYPE InnerAddRef(void)=0;
virtual ULONG STDMETHODCALLTYPE InnerRelease(void)=0;
};
// Déclaration de la classe implémentant le composant :
class CCalculator &colon; public IInnerUnknown, public IMultiplier
{
// Données de gestion du composant :
IUnknown *m_pUnknown;
ObjectDestroyedFunction m_ObjectDestroyed;
unsigned long m_ulRefCount;
// Pointeurs sur l'objet agrégé :
IUnknown *m_pAdderInnerUnknown;
public&colon;
// 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,
ObjectDestroyedFunction ObjectDestroyed);
~CCalculator(void);
HRESULT Init(void);
};
// Déclaration de la classe implémentant la fabrique de classe :
class CCalculatorFactory &colon; public IClassFactory
{
unsigned long m_ulRefCount;
public&colon;
// Les méthodes de IUnknown :
virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid,
void **ppvObject);
virtual ULONG STDMETHODCALLTYPE AddRef(void);
virtual ULONG STDMETHODCALLTYPE Release(void);
// Les méthodes de IClassFactory :
virtual HRESULT STDMETHODCALLTYPE CreateInstance(
IUnknown *pOuterUnknown, REFIID iid, void **ppvObject);
virtual HRESULT STDMETHODCALLTYPE LockServer(BOOL bLock);
// Le constructeur :
CCalculatorFactory(void);
};
#endif

Voici le fichier source contenant l'implémentation de ce composant Calculator :

Exemple 10. Implémentation C du composant Calculator

#include <windows.h>
#include <objbase.h>
#include "CalcPrx/Calculator.h"
#include "../Adder/AdderPrx/Adder.h"
#include "calculator.h"
//////////////////////////////////////////////////////////
//        Implémentation d'un composant agrégat         //
//////////////////////////////////////////////////////////
CCalculator&colon;&colon;CCalculator(IUnknown *pUnknown,
ObjectDestroyedFunction ObjectDestroyed) :
m_ObjectDestroyed(ObjectDestroyed), m_ulRefCount(0)
{
if (pUnknown!=0) m_pUnknown=pUnknown;
else m_pUnknown=
reinterpret_cast<IUnknown *>(static_cast<IInnerUnknown *>(this));
return ;
}
HRESULT CCalculator&colon;&colon;Init(void)
{
// Protection contre les destructions prématurées :
m_pUnknown->AddRef();
// Création du composant agrégé :
return CoCreateInstance(CLSID_Adder, m_pUnknown, CLSCTX_ALL,
IID_IUnknown, reinterpret_cast<void **>(&m_pAdderInnerUnknown));
}
CCalculator&colon;&colon;~CCalculator(void)
{
// La destruction des pointeurs stockés à usage interne
// devrait se faire ici.
// La méthode à utiliser est la suivante : appel de la méthode AddRef
// de sa propre interface IUnknown externe pour fixer le compte de
// références à sa valeur correcte, puis appel de la méthode Release
// sur le pointeur à libérer.
// Par exemple :
// 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&colon;&colon;QueryInterface(REFIID iid,
void **ppvObject)
{
return m_pUnknown->QueryInterface(iid, ppvObject);
}
ULONG STDMETHODCALLTYPE CCalculator&colon;&colon;AddRef(void)
{
return m_pUnknown->AddRef();
}
ULONG STDMETHODCALLTYPE CCalculator&colon;&colon;Release(void)
{
return m_pUnknown->Release();
}
HRESULT STDMETHODCALLTYPE CCalculator&colon;&colon;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);
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 pour les 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&colon;&colon;InnerAddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CCalculator&colon;&colon;InnerRelease(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
// Signal de destruction de l'objet :
m_ObjectDestroyed();
// 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&colon;&colon;Mul(long i, long j, long *pResult)
{
*pResult=i*j;
return NOERROR;
}
//////////////////////////////////////////////////////////
//          Fabrique de classe du composant             //
//////////////////////////////////////////////////////////
// Compteurs pour la durée de vie du composant :
static unsigned long ulObjectCount=0;  // Nombre d'objets
static unsigned long ulLockCount=0;    // Nombre de blocages.
#ifdef EXESERVER
// Handle de la fenêtre principale du serveur dans le cas
// des serveurs exécutables :
static HWND hServerWindow;
#endif
// La fonction de signalisation de destruction d'un objet :
static void ObjectDestroyed(void)
{
// Décrémente le compteur d'objet :
ulObjectCount—;
#ifdef EXESERVER
// Dans le cas des serveurs exécutables, effectue le test
// de destruction du serveur :
if (ulObjectCount==0 && ulLockCount==0)
PostMessage(hServerWindow, WM_CLOSE, 0,0);
#endif
return ;
}
CCalculatorFactory&colon;&colon;CCalculatorFactory(void) : m_ulRefCount(0)
{
return ;
}
HRESULT STDMETHODCALLTYPE CCalculatorFactory&colon;&colon;QueryInterface(REFIID iid,
void **ppvObject)
{
*ppvObject=0;
if (iid==IID_IUnknown)
*reinterpret_cast<IUnknown **>(ppvObject)=
static_cast<IUnknown *>(this);
else if (iid==IID_IClassFactory)
*reinterpret_cast<IClassFactory **>(ppvObject)=
static_cast<IClassFactory *>(this);
if (*ppvObject==0) return E_NOINTERFACE;
AddRef();
return NOERROR;
}
ULONG STDMETHODCALLTYPE CCalculatorFactory&colon;&colon;AddRef(void)
{
m_ulRefCount++;
return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CCalculatorFactory&colon;&colon;Release(void)
{
m_ulRefCount—;
if (m_ulRefCount!=0) return m_ulRefCount;
delete this;
return 0;
}
// Les méthodes de IClassFactory :
HRESULT STDMETHODCALLTYPE CCalculatorFactory&colon;&colon;CreateInstance(
IUnknown *pOuterUnknown, REFIID iid, void **ppvObject)
{
// Initialise la valeur de retour :
*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,
ObjectDestroyed);
ulObjectCount++;
if (pCalculator==0)
{
// Détruit le serveur si nécessaire :
ObjectDestroyed();
return E_OUTOFMEMORY;
}
// Initialise l'objet et demande l'interface désirée :
HRESULT hResult=E_FAIL;
if (SUCCEEDED(pCalculator->Init()))
{
// Demande l'interface :
hResult=pCalculator->InnerQueryInterface(iid, ppvObject);
// Supprime la référence artificielle incluse dans
// CCalculator&colon;&colon;Init() :
pCalculator->Release();
}
if (FAILED(hResult))
{
delete pCalculator;
ObjectDestroyed();
}
return hResult;
}
HRESULT STDMETHODCALLTYPE CCalculatorFactory&colon;&colon;LockServer(BOOL bLock)
{
if (bLock) ulLockCount++;
else
{
if (ulLockCount!=0)
{
ulLockCount—;
// Force le test de destruction du serveur :
ulObjectCount++;
ObjectDestroyed();
}
}
return NOERROR;
}
//////////////////////////////////////////////////////////
//        Code d'enregistrement du composant            //
//////////////////////////////////////////////////////////
// Le code d'enregistrement n'est pas le même
// selon la nature du composant :
#ifdef DLLSERVER
// Serveur in-process.
#ifdef INITDLL
BOOL APIENTRY LibMain(HANDLE /* hDLL */,
DWORD    /* dwReason */,
LPVOID    /*pReserved*/)
{
return TRUE;
}
#endif
// Fonction d'autorisation de déchargement :
STDAPI DllCanUnloadNow(void)
{
if (ulObjectCount==0 && ulLockCount==0) return S_OK;
else return S_FALSE;
}
// Fonction d'enregistrement de la fabrique de classe :
STDAPI DllGetClassObject(REFCLSID clsid, REFIID iid,
LPVOID *ppvFactory)
{
// On initialise le pointeur retourné :
*ppvFactory=0;
// On vérifie que le serveur gère bien la classe d'objet dont on
// cherche à créer une fabrique :
if (clsid!=CLSID_Calculator) return CLASS_E_CLASSNOTAVAILABLE;
// On crée la fabrique d'objets pour cette classe :
CCalculatorFactory *pFactory=new CCalculatorFactory();
if (pFactory==0) return E_OUTOFMEMORY;
// On demande l'interface désirée pour la fabrique d'objet
// (normalement, ce doit être IClassFactory ou IUnknown) :
HRESULT hr=pFactory->QueryInterface(iid, ppvFactory);
if (FAILED(hr)) delete pFactory;
return hr;
}
#endif
#ifdef EXESERVER
// Serveur exécutable :
// Procédure de fenêtre principale du serveur :
LRESULT CALLBACK WindowProc(HWND hWindow, UINT dMessage,
WPARAM lParam1, LPARAM lParam2)
{
LRESULT lResult;
switch(dMessage)
{
case WM_DESTROY :
PostQuitMessage(0);
lResult=0;
break ;
default :
lResult=DefWindowProc(hWindow, dMessage, lParam1, lParam2);
break ;
}
return lResult;
}
// Fonction principale du serveur :
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE,
LPSTR lpCmdLine, int iCmdShow)
{
// Vérifie la ligne de commande :
if (lstrcmpiA(lpCmdLine, "-Embedding") &&
lstrcmpiA(lpCmdLine, "/Embedding")) return FALSE;
// Initialise COM :
if (FAILED(CoInitialize(NULL))) return FALSE;
// Crée la fabrique de classe :
static CCalculatorFactory *pFactory=new CCalculatorFactory();
if (pFactory==NULL)
{
CoUninitialize();
return FALSE;
}
pFactory->AddRef();
// Enregistre le serveur auprès de COM :
static DWORD dwClassObjectKey;
if (FAILED(CoRegisterClassObject(CLSID_Calculator,
static_cast<IUnknown *>(pFactory),
CLSCTX_LOCAL_SERVER | CLSCTX_REMOTE_SERVER,
/* Supprimer CLSCTX_REMOTE_SERVER sur Windows 95 sans DCOM. */
REGCLS_MULTIPLEUSE, &dwClassObjectKey)))
{
pFactory->Release();
CoUninitialize();
return FALSE;
}
// Enregistre la classe de la fenêtre du serveur :
WNDCLASS wc;
wc.style = 0;
wc.lpfnWndProc = (WNDPROC) &WindowProc;
wc.cbClsExtra = 0;
wc.cbWndExtra = 0;
wc.hInstance = hInstance;
wc.hIcon = LoadIcon((HINSTANCE) NULL, IDI_APPLICATION);
wc.hCursor = LoadCursor((HINSTANCE) NULL, IDC_ARROW);
wc.hbrBackground = GetStockObject(WHITE_BRUSH);
wc.lpszMenuName = NULL;
wc.lpszClassName = "CalculatorServerWindowClass";
if (!RegisterClass(&wc))
{
CoRevokeClassObject(dwClassObjectKey);
pFactory->Release();
CoUninitialize();
return FALSE;
}
// Crée la fenêtre principale :
hServerWindow=CreateWindow("CalculatorServerWindowClass",
"Calculator Server",
WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT,
CW_USEDEFAULT, CW_USEDEFAULT, (HWND) NULL,
(HMENU) NULL, hInstance, (LPVOID) NULL);
if (!hServerWindow)
{
CoRevokeClassObject(dwClassObjectKey);
pFactory->Release();
CoUninitialize();
return FALSE;
}
ShowWindow(hServerWindow, iCmdShow);
UpdateWindow(hServerWindow);
MSG msg;
// Boucle de gestion des messages :
while (GetMessage(&msg, (HWND) NULL, 0, 0))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
// Désenregistre le serveur auprès de COM :
CoRevokeClassObject(dwClassObjectKey);
// Détruit la fabrique de classe :
pFactory->Release();
// Termine COM :
CoUninitialize();
// Renvoie le code d'erreur :
return msg.wParam;
}
#endif

Enfin, voici le fichier source du programme client :

Exemple 11. Programme client du composant Calculator

#include <stdio.h>
#include "../Adder/AdderPrx/Adder.h"
#include "../Calc/CalcPrx/Calculator.h"
int main(void)
{
CoInitialize(0);
IAdder *pAdder;
IMultiplier *pMultiplier;
long i;
// Utilisation de l'additionneur sans agrégation :
IClassFactory *pFactory;
CoGetClassObject(CLSID_Adder, CLSCTX_ALL, NULL,
IID_IClassFactory, (void **) &pFactory);
pFactory->CreateInstance(0, IID_IAdder, (void **) &pAdder);
pAdder->Add(2,3, &i);
printf("%d\n", i);
pAdder->Release();
// Utilisation de la calculatrice :
CoCreateInstance(CLSID_Calculator, 0, CLSCTX_ALL, IID_IMultiplier,
(void **) &pMultiplier);
pMultiplier->Mul(2,3, &i);
printf("%d\n", i);
pMultiplier->QueryInterface(IID_IAdder, (void **) &pAdder);
pMultiplier->Release();
pAdder->Add(2,3, &i);
printf("%d\n", i);
pAdder->Release();
CoUninitialize();
return 0;
}

Note: Pour que le client fonctionne correctement, il est nécessaire que le serveur du composant Adder soit in-process. En effet, dans le cas contraire, il servait impossible de créer le composant Calculator, puisque ce dernier utilise l'agrégation.

30. Comment compiler un composant ?

Tous les serveurs doivent être compilés avec les options de multithreading. Le système cible pour les composants dépend du système sur lequel ils fonctionneront, en général, il s'agit du système WIN32.

Pour les composants implémentés in-process, un certain nombre de fonctions doivent également être exportées. Il s'agit des fonctions DllGetClassObject, DllCanUnloadNow. Les fonctions DllRegisterServer et DllUnregisterServer peuvent également être implémentées et exportées si le composant est capable de s'enregistrer automatiquement.

Note: Lors de la génération des composants in-process, un certain nombre de problèmes peuvent apparaître. Il s'agit le plus souvent de problèmes de conventions d'appel et d'édition de lien. Les fonctions exportées doivent utiliser impérativement les mêmes conventions d'appel que celles que Windows emploiera pour les appeler. Il faudra donc veiller à ce que les mots clé comme STDMETHODCALLTYPE soient utilisés à bon escient pour garantir des conventions d'appel correctes. Par ailleurs, les fonctions exportées doivent l'être avec le nom qu'elles portent dans le fichier source, sans la décoration ajoutée par les compilateurs. Certains éditeurs de liens ne suppriment pas cette décoration automatiquement. Dans ce cas, il est nécessaire de préciser les noms sous lesquels ces fonctions sont exportées lors de la phase d'édition de lien.

31. Comment le multithreading est-il géré ?

DCOM est multithreadé. Ceci signifie qu'un grand nombre de problèmes de synchronisations doivent être gérés. DCOM gère deux modèles différents pour résoudre les problèmes de threading. Le plus ancien, dit single threaded apartment (ou, par abus de langage, apartment threaded), permet de contrôler les accès concurrents aux composants. Cependant, ce modèle implique un mécanisme assez lourd et relativement peu performant. En revanche, le deuxième modèle, plus récent (introduit à partir de Windows 95 et de NT4), permet de laisser les composants gérer eux-mêmes les accès concurrents des clients et les synchronisations. Les composants écrits pour ce modèle sont plus efficaces, mais plus difficiles à programmer. Ce modèle est classiquement nommé free threaded, du fait qu'il laisse les composants gérer eux-même le multithreading.

Les principaux problèmes auxquels DCOM doit faire face sont les suivants :

  • accès concurrent de plusieurs clients à un même objet ou à une même méthode d'interface ;

  • résolution des interblocages et des réentrances ;

  • gestion de threads d'exécutions répartis sur plusieurs processus ;

  • gestion de plusieurs de composants basés sur des modèles de gestion des problèmes de threading différents ;

  • gestion de la transparence des appels entre les clients et les serveurs dont le modèle des problèmes de threading est différent.

Le premier problème apparaît dans le cas des serveurs in-process utilisés par des applications multithreadées et dans le cas des serveurs out-of-process utilisés par plusieurs applications en même temps. Les interblocages apparaissent lorsque deux objets sont respectivement clients et serveurs l'un de l'autre (directement ou indirectement) et que chacun attend la fin de l'exécution d'un méthode de l'autre pour terminer sa propre méthode. Le troisième problème est d'identifier quel thread a effectué quelle requête dans un processus qui ne contient pas le thread en question. Il apparaît dès qu'un serveur est out-of-process. Les deux derniers problèmes sont de maintenir le système stable et ce d'une manière transparente lorsque des composants qui utilisent différentes techniques de contrôle pour les problèmes précédents coexistent.

Historiquement, les composants n'étaient pas capable de gérer les accès concurrents eux-mêmes. Le modèle apartment threaded avait donc pour but de répondre à tous ces problèmes de manière transparente. Le modèle free threaded est plus souple, mais impose au programmeur de gérer ces problèmes lui-même.

32. Qu'est-ce qu'un appartement ?

33. Qu'est-ce qu'un composant apartment threaded ?

34. Qu'est-ce qu'un composant free-threaded ?

Quel que soit le modèle de threading utilisé pour un composant, DCOM utilise la notion d'appartement pour résoudre les problèmes de threading. Un appartement est un groupement logique de threads dans lesquels zéro ou plusieurs objets peuvent vivre. Chaque objet ne peut vivre que dans un et un seul appartement. L'appartement dans lequel un objet est créé est donc celui dans lequel il passera toute sa vie : un objet ne peut jamais changer d'appartement.

La règle fondamentale utilisée par DCOM est la suivante : les méthodes des objets ne peuvent être appelées directement qu'à partir de l'appartement dans lequel ces objets se trouvent. Ceci signifie que lorsqu'un thread d'un appartement essaie d'appeler une méthode d'un objet créé dans un autre appartement, un processus de marshalling doit avoir lieu, même si les deux threads se trouvent dans le même processus ! Un cas particulier évident de ce type de situation est celui de l'appel d'un objet out-of-process, qui appartient bien entendu à un autre appartement.

Autrement dit, les objets d'un appartement ne peuvent être accédés que par les threads de cet appartement. Réciproquement, les threads d'un appartement ne peuvent utiliser que les objets qu'ils ont créés eux-même ou qu'un autre thread du même appartement a créé.

La notion d'appartement étant maintenant définie, il est possible de décrire plus en détail les deux modèles de threading que gère DCOM. Le premier modèle est le modèle des appartements monothreadés, couramment appelé « single threaded apartment ». Ce type d'appartement a la particularité qu'un seul thread peut y résider. C'est le modèle par défaut, qui permet à DCOM d'assurer une certaine sécurité pour les composants qui ne prennent pas en charge le multithreading.

Le deuxième modèle est le modèle d'appartement multithreadé, encore appelé « free threaded ». Dans ce modèle, plusieurs threads peuvent coexister dans un même appartement. DCOM n'intervient alors plus dans les appels de méthodes vers les objets créés par un autre thread que celui qui fait l'appel, pourvu que ces deux threads soient dans le même appartement. Par conséquent, les appels sont plus rapides, mais il faut que les composants soient capables de gérer ce modèle et de contrôler eux-même les accès concurrents sur leurs méthodes.

Note: Le modèle d'appartement monothreadé est souvent dénommé « apartment threaded ». C'est un abus de langage, car en réalité, tous les composants sont dans un appartement. La différence porte uniquement sur le fait qu'il puisse y avoir plusieurs threads dans le même appartement, ou non.

En réalité, les appartements monothreadés ne sont qu'un cas particulier d'appartements multithreadé, pour lesquels il n'y a qu'un seul thread…

La déclaration de l'appartenance à un appartement pour un thread se fait lors de l'initialisation de DCOM dans ce thread. En effet, chaque thread qui compte utiliser DCOM doit appeler soit CoInitialize, soit CoInitializeEx. Cette dernière fonction permet de spécifier le modèle de threading utilisé par ce thread. Elle est déclarée dans le fichier objbase.h de la manière suivante :

HRESULT STDAPICALLTYPE CoInitializeEx ( LPVOID pReserved , DWORD dwCoInit );

Le premier paramètre est réservé et doit toujours prendre la valeur nulle. Le deuxième paramètre indique le modèle de threading utilisé. Les différents modèles sont caractérisés par les constantes suivantes, définies dans le fichier objbase.h :

  • COINIT_APARTMENTTHREADED, pour utiliser l'appartement multithreadé ;

  • COINIT_MULTITHREADED, pour créer un nouvel appartement monothreadé.

Tous les threads d'une application qui gèrent le modèle multithreadé appartiennent au même appartement (il est inutile dans leur cas de les séparer dans plusieurs appartements). En revanche, chaque thread utilisant le modèle monothreadé se voit attribué un appartement qui lui est propre. Il est possible de réaliser des applications mixtes, possédant un appartement multithreadé et un ou plusieurs appartements monothreadés.

Note: La fonction CoInitialize, que l'on a utilisé jusqu'à présent, appelle en fait la fonction CoInitializeEx en interne avec l'option COINIT_APARTMENTTHREADED, ce qui implique qu'ils sont par défaut dans un appartement monothreadé. Ce choix permet de maintenir la compatibilité avec les anciennes versions de Windows NT. Il implique cependant que les threads qui utilisent OLE, et qui donc appellent OleInitialize, sont eux-aussi monothreadés, parce qu'OleInitialize appelle CoInitialize en interne. Tous les composants OLE doivent donc être écrits de manière à gérer le modèle de threading monothreadé (ils peuvent gérer les deux modèles bien entendu s'ils le souhaitent, mais c'est inutile).

La documentation de Windows indique par endroit qu'il est impossible de faire coexister les deux modèles dans une même application. Cette assertion est fausse, la documentation n'est dans ce cas pas à jour.

Il est évident que les serveurs in-process, qui n'appellent pas CoInitializeEx, ne peuvent pas indiquer les modèles de threading qu'ils gèrent. Pour ces serveurs, le modèle de threading est décrit dans la base de registres, par une valeur écrite dans la clé InprocServer32 et portant le nom ThreadingModel. Les différentes valeurs possibles sont données ci-dessous :

Tableau 3. Modèles de threading des composants

Apartment Modèle monothreadé.
Free Modèle multithreadé
Both Les deux modèles sont gérés (le serveur s'adapte au modèle de ses clients).

Lorsqu'un thread client demande la création d'un composant d'un serveur in-process mais qui utilise un autre modèle de threading, DCOM crée automatiquement un thread pour séparer le composant du client. Dans ce cas, le serveur est placé dans un appartement distinct de celui du thread client. Ainsi, lorsqu'un thread d'un appartement multithreadé charge un serveur in-process ne gérant que les appartements monothreadés, ce serveur est chargé dans un thread contrôlé par DCOM. De même, lorsqu'un thread d'un appartement monothreadé charge un serveur in-process utilisant le modèle d'appartement free-threaded, DCOM lance un thread dans l'appartement multithreadé de l'application (et crée ainsi cet appartement si nécessaire). Lorsque cette situation se produit, les mécanismes de marshalling prennent place entre le client et le serveur. Il est donc important d'implémenter ces mécanismes même pour les serveurs in-process. Le modèle de threading Both signifie simplement que le serveur est capable de s'adapter au modèle de threading de ses clients. Ceci implique qu'il est capable de supporter des appels concurrents provenant de plusieurs threads, et donc qu'il est multithreadé. Mais ceci implique également qu'il n'appellera jamais ses clients monothreadés sur deux threads différents. En particulier, les notifications effectuées par le serveur se feront toujours dans le thread du client si celui-ci est monothreadé.

Note: D'après ce qui vient d'être dit, les composants qui ne gèrent que le modèle Free ne peuvent pas être utilisés directement à partir d'un appartement monothreadé. En théorie, les composants qui gèrent le modèle multithreadé pourraient être utilisés directement par les threads des appartements monothreadés, car qui peut le plus peut le moins. Cependant, certains composants utilisent un marshalling personnalisé, et le proxy de ces serveurs peut effectuer des opérations particulières qui imposent de l'utiliser. Ce cas est très rare mais impose d'utiliser le marshalling même pour l'accès aux objets d'un appartement multithreadé à partir d'un appartement monothreadé. De plus, le composant multithreadé n'est pas tenu de rappeler les interfaces de rappel des clients sur le même thread. Les clients monothreadés ne peuvent tolérer ce comportement, ce qui impose encore une fois l'utilisation du marshalling entre les composants serveurs multithreadés et les clients monothreadés. Les composants multithreadés qui garantissent que les appels sur les interfaces de rappel des clients monothreadés ne se font que dans le contexte du client n'ont pas besoin d'utiliser le mécanisme du marshalling. Dans ce cas, il peuvent être déclaré Both.

Les serveurs in-process qui utilisent le modèle de threading Both sont les plus performants, puisque les mécanismes de marshalling ne sont pas nécessaires entre leurs composants et leurs clients. Tous les appels de méthodes sont donc réalisés directement. Cependant, leur écriture est nettement plus technique, car il faut s'assurer qu'aucun thread ne fait des notifications sur des composants monothreadés qui n'utilisent pas ce thread. Les composants qui ne créent pas de threads additionnels sont à ranger dans cette catégorie, puisque le seul thread qu'ils peuvent utiliser est celui de leur client monothreadé.

Notez que les composants qui utilisent des données spécifiques aux threads (« TLS », abréviation de « Thread Local Storage ») ne peuvent pas utiliser le modèle de threading Both. En effet, ils ne sont pas capables d'exécuter des requêtes sur des threads pour lesquels ils n'ont pas initialisé les données spécifiques. Il faut donc les déclarer comme utilisant le modèle de threading Free s'ils sont multithreadés.

La documentation de Windows indique parfois qu'il n'est pas possible de charger un serveur in-process dont le modèle d'appartement est différent de celui du client. Cette assertion est fausse, la documentation n'est pas à jour dans ce cas.

Si la clé ThreadingModel n'est pas renseignée pour un composant ou est absente, DCOM considérera que le composant est apartment threaded et le placera automatiquement dans un appartement monothreadé. Si aucun appartement monothreadé n'existe dans l'application lors de la création de l'objet, il en créera un pour l'occasion. Sinon, il prendra le premier appartement monothreadé que l'application aura créé. On appelle couramment cet appartement le main apartment, ou appartement monothreadé principal.

Note: Il existe une exception à cette règle. Si un thread d'un appartement multithreadé crée un objet de ce type pour son propre compte ou pour le compte d'un autre thread que celui de l'appartement principal, cet objet sera placé dans un autre appartement monothreadé que l'appartement principal. En effet, DCOM s'efforce d'exécuter toutes les opérations au nom de la même tâche dans le système. La création d'un objet par un thread pour le compte d'un autre thread que celui de l'appartement principal implique donc l'utilisation d'un autre appartement, que DCOM créera automatiquement si nécessaire.

35. Comment DCOM effectue-t-il les appels de méthodes entre deux appartements ?

La technique utilisée par DCOM pour contrôler les concurrences d'accès entre appartements se base sur les files de messages du système sous-jacent. Ce que DCOM cherche à assurer avant tout, c'est que dans chaque appartement monothreadé, il ne s'exécute que le thread qui a créé cet appartement. Pour y parvenir, DCOM poste un message dans la boucle des messages de ce thread pour chaque appel en provenance des autres threads. Ainsi, les appels sont tous sérialisés par l'intermédiaire de la boucle des messages du thread de l'appartement. Ceci impose que tous les threads des appartements monothreadés implémentent une boucle des messages.

En fait, DCOM crée en interne une fenêtre invisible pour chaque appartement monothreadé, dont la procédure de fenêtre récupère les requêtes d'exécution de méthodes d'objets et les satisfait. On notera que le thread de l'appartement peut toujours appeler directement des méthodes des objets de cet appartement, puisque lorsqu'il le fait, il ne se trouve pas dans la procédure de fenêtre installée par DCOM. Il n'y a donc qu'un seul thread en cours d'exécution dans l'appartement à chaque instant.

Par contre, DCOM ne garantit pas qu'il n'y ait qu'une seule requête en cours d'exécution à chaque instant, même dans un appartement monothreadé. En effet, les méthodes des composants peuvent parfaitement être appelées de manière réentrante. Ces appels peuvent être faits de manière interne par le composant, ou provenir du traitement d'un autre message de la boucle des messages. Ce dernier cas suppose que le thread de l'appartement retourne dans cette boucle, situation qui se produit dès qu'un appel à destination d'un objet d'un autre appartement est fait. Dans ce cas, le thread de l'appartement peut parfaitement recevoir le message provenant d'une requête d'exécution d'une autre méthode, ou de la même méthode qu'il était en train de traiter. DCOM ne contrôle pas cette situation, et tous les composants DCOM doivent donc être réentrants.

Pour les appartements multithreadés, tout est beaucoup plus simple. En effet, les appels des méthodes des objets sont effectués directement, sans contrôle, soit par les threads de l'appartement, soit par DCOM. Ceci est réalisable parce que tous les objets créés dans cet appartement sont supposés être capables de gérer eux-mêmes les concurrences d'accès. Pour les threads de cet appartement, il n'est donc pas nécessaire d'écrire une boucle des messages, et aucune fenêtre invisible n'est créée par DCOM.

Les règles à respecter pour le bon fonctionnement du multithreading sont donc les suivantes :

  • appeler CoInitialize ou CoInitializeEx au début de chaque thread qui utilise DCOM ;

  • pour chaque thread s'exécutant dans un appartement monothreadé, il faut écrire une boucle des messages si l'on veut que les objets créés dans ce thread puisse recevoir des appels provenant d'un autre appartement ;

  • aucun pointeur sur une interface ne doit être communiquée directement à un thread d'un autre appartement, quel qu'il soit. Si un pointeur sur une interface d'un appartement monothreadé doit être utilisé en dehors de cet appartement, il faut soit l'obtenir à l'aide des mécanismes standard de DCOM, soit le marshaller manuellement ;

  • pour les objets des appartement multithreadés, il faut s'assurer de la gestion correcte des accès concurrent. Ceci se fait grâce événements, aux sections critiques, aux mutex ou aux sémaphores du système.

Note: DCOM ne protège contre les accès concurrents que les composants d'un appartement monothreadé, pas les serveurs eux-mêmes. Ceci signifie que même les serveurs utilisant le modèle d'appartement monothreadé doivent malgré tout être capables de gérer les concurrences d'accès. En effet, ils peuvent être appelés par plusieurs threads de plusieurs appartements, et permettre ainsi la création d'objets dans plusieurs appartements. Les points sur lesquels il faudra faire attention sont les variables globales (décomptes de références et de blocage du serveur, accessibles par plusieurs threads du client), la gestion de création des objets, et l'implémentation de DllCanUnloadNow (le serveur ne doit pas être détruit tant qu'un objet existe dans un quelconque appartement).

36. Qu'est-ce que le marshalling ?

37. Que sont les stublets et les facelets ?

Lorsque le composant utilisé par un client est situé dans un autre appartement que le client, et a fortiori lorsque le composant est dans un serveur out-of-process, les appels des méthodes de ce composant ne peuvent pas être faits directement. Les paramètres des méthodes doivent en effet être transférés du client vers le composant serveur par l'intermédiaire d'un moyen de communication quelconque. Cette opération de transfert des paramètres est appelée le marshalling.

Microsoft a défini un protocole standard de marshalling des paramètres pour DCOM. Ce protocole utilise les RPC (Remote Procedure Call) pour effectuer les appels des méthodes des serveurs. Il est capable de transférer les paramètres dans le sens de l'appel comme dans le sens du retour des fonctions. Il est également capable de gérer les passages de pointeurs, à un niveau quelconque d'imbrication, ce qui offre une grande flexibilité.

Les mécanismes de marshalling sont les suivants. DCOM charge une DLL dans l'espace d'adressage du client, que l'on appelle le proxy. Ce proxy est capable de simuler le composant distant auprès du client, et en rend ainsi l'accès complètement transparent. Le proxy établit en réalité un canal de communication avec un autre objet, que l'on nomme le stub, et qui lui se trouve dans l'espace d'adressage du composant utilisé par le client. Le stub est donc l'objet qui va communiquer avec le proxy pour effectuer les appels que le client a demandé sur les méthodes du composant qu'il gère. Autrement dit, le proxy simule les interfaces du composant que le client désire utiliser, et tous les appels des méthodes de ces interfaces sont en fait traités par le proxy. Celui-ci transmet les valeurs des paramètres dans un format indépendant de la machine au stub du serveur, via les couches RPC (et donc via les couches réseaux). Lorsque le stub reçoit ces informations, il reconstruit les paramètres et appelle la méthode désirée sur la bonne interface du composant. Les paramètres en retour sont traités exactement de la même manière. Le résultat net de ce mécanisme est que le client ne se rend pas compte du fait que le composant avec lequel il communique ne fonctionne pas dans le même appartement (voire même pas dans sur la même machine). Pour lui, les communications ne se réalisent que par simples appels de fonctions.

Il va de soi que pour que les proxy et les stubs puissent interpréter correctement les paramètres des méthodes des interfaces, il faut qu'ils connaissent la sémantique de ces interfaces. Si les composants gèrent eux-même leur marshalling, ceci ne pose pas de problème, car ils connaissent bien entendu leurs propres interfaces. Les proxies qu'ils utilisent sont dans ce cas écrits spécifiquement pour eux, et connaissent également ces interfaces.

Si, en revanche, les mécanismes standards de DCOM sont utilisés, il est nécessaire de décrire les interfaces utilisées par les composants afin que DCOM puisse effectuer le marshalling automatiquement. C'est pour cette raison que Microsoft a défini un langage de description des interfaces, le langage IDL (« Interface Definition Language »). Une fois les interfaces définies en IDL, il est possible de générer automatiquement les fichiers sources de petits composants capables de marshaller les interfaces et de communiquer avec les proxies et stubs standard de DCOM. Ces composants sont appelés les facelets et les stublets

Note: Le langage IDL de Microsoft est un dérivé du langage IDL des RPC DCE. Bien qu'ayant également pour vocation de décrire des interfaces et des fonctions distantes, ce n'est pas le même langage que le langage IDL de CORBA.

Par conséquent, à moins qu'on n'implémente soi-même le marshalling pour chaque composant, ainsi que les proxies capables de communiquer avec ces composants, il est impératif de définir complètement les interfaces de ses composants dans le langage IDL. Ce langage sera décrit plus loin.

38. Qu'est-ce qu'une type library ?

Le but fondamental des technologies à composant est de réutiliser les morceaux de programmes déjà réalisés. En fait, les langages à objets permettaient déjà de réutiliser du code au sein même d'une application, notamment par le mécanisme d'héritage. Les technologies à composants vont au-delà, en brisant la barrière des applications et du code source. Les composants peuvent être réutilisés par d'autres applications que celles pour laquelle ils ont été initialement conçus.

Il est évident que ceux qui écrivent des composants savent parfaitement comment les utiliser. Dans le cas de DCOM, ceci signifie qu'ils connaissent parfaitement les GUID des composants et des interfaces, ainsi que les paramètres et la sémantique des méthodes des interfaces. S'ils documentent leurs composants, d'autres programmeurs peuvent également utiliser ces composants, en brisant cette fois la barrière des projets ou des sociétés dans lesquels les composants sont écrits. Il est donc nécessaire (mais pas obligatoire) de documenter les composants et leurs interfaces.

En fait, l'idéal est tout simplement que les composants soient capables de renseigner directement leurs clients sur la manière de les utiliser. DCOM a prévu des interfaces spécifiques qui permettent à un client de demander à un composant sa description. Ces interfaces seront également décrites plus loin.

Bien entendu, le fait de rendre utilisable un composant ne nuit pas aux droits d'auteurs, puisque DCOM gère les licences d'utilisation par l'intermédiaire de l'interface IClassFactory2 pour les fabriques de classes. Cette interface sera décrite plus loin.

Quoi qu'il en soit, DCOM spécifie un format standard pour les informations permettant de décrire les composants. Initialement, les types libraries étaient générées à l'aide du langage ODL (Object Description Language), qui était très proche du langage IDL. Un compilateur spécifique prenait en entrée les fichiers écrits en ODL et produisaient des fichiers portant l'extension .TLB (pour Type LiBrary) contenant les informations de la librairie. Depuis, le langage ODL a été inclus dans le langage IDL. Il est donc possible à présent d'écrire un seul fichier IDL qui décrit les interfaces pour la génération automatique des facelets et des stublets, et qui permet la génération des type libraries.

39. Comment écrire un fichier IDL ?

Ce paragraphe n'a pas la prétention de donner une description complète du langage IDL, ce qui dépasserait le cadre de cette FAQ. Cependant, il explique un peu la syntaxe du langage et a pour but de permettre la rédaction de fichiers IDL de plus en plus complexes à l'aide de la référence du langage.

Le compilateur utilisé pour traiter les fichiers IDL est nommé MIDL.EXE (pour Microsoft IDL). MIDL est un outil complexe, qui permet de générer non seulement les stublets et les facelets, mais également les type libraries. Ce paragraphe ne présente que les fonctions élémentaires de MIDL, vous trouverez la référence complète de MIDL dans le SDK de Windows.

Le langage IDL est un langage qui ressemble au C++, en particulier pour la syntaxe de l'héritage. Il accepte également les directives du préprocesseur C, tout comme le C++. Les types de données sont exprimés avec une syntaxe équivalente à celle du C, et les types de base portent le même nom. En revanche, IDL possède un certain nombre de mots-clés qui permettent de décrire les interfaces, et il intègre également les mots clés du langage ODL afin de permettre la génération des type libraries.

Note: En fait, MIDL utilise le préprocesseur C du compilateur Microsoft (CL.EXE). Il est donc nécessaire que CL.EXE ou qu'un programme de même nom qui l'émule soit accessible dans le path de votre ordinateur. Si ce n'est pas le cas, il est possible d'utiliser l'option /cpp_cmd pour indiquer le nom du préprocesseur que MIDL doit utiliser.

Le type int n'est pas accepté tel quel par MIDL. En effet, ce n'est pas un type portable, et IDL est supposé décrire les interfaces de manière indépendante de la plate-forme. Il faut donc toujours utiliser une version soit courte, soit longue de int dans les fichiers IDL.

Les types de base utilisés par IDL sont décrits ci-dessous :

  • boolean, type de base pour les nombres booléens. La représentation est faite sur 8 bits, de manière non signée. Les deux seules valeurs possibles sont TRUE et FALSE. Ce type correspond au type bool du C++ ;

  • byte, type générique stocké sur 8 bits. Il n'a pas de notion de valeur ou de signe associée, les données de ce type sont transmises telles quelles sur le réseau. L'interprétation de ces données doit être faite au niveau des bits, les nombres stockées dans ce type peuvent changer de valeur selon la représentation interne de la machine cible. Ce type de donnée n'a pas d'équivalent direct en C/C++ ;

  • char, type de base pour les caractères, stocké sur 8 bits. Les caractères sont toujours considérés comme non signés par IDL. Ce type de donnée est équivalent au type unsigned char du C/C++ ;

  • wchar_t, type de base pour les caractères longs, stocké sur 16 bits. Les caractères sont toujours considérés comme non signés par IDL. Ce type de donnée est équivalent au type unsigned wchar_t du C++ ;

  • short, small, type de donnée permettant de stocker des entiers courts (16 bits). Par défaut, les valeurs stockées sont signées. Il est possible d'utiliser le mot-clé unsigned pour les rendre non signées. Ce type n'a pas d'équivalent direct en C/C++ (la taille des types n'est pas normalisée) ;

  • int, long, type de donnée permettant de stocker des entiers normaux (32 bits). Par défaut, le type int est considéré comme égal au type long sur les plateformes 32 bits, cependant, sa taille est variable (tout comme en C/C++). Il est donc fortement recommandé de ne jamais l'utiliser sans qualification de taille. Les valeurs stockées sont signées, mais il est possible de les rendres non signées à l'aide du mot-clé unsigned. Ce type n'a pas d'équivalent en C/C++ ;

  • hyper, type de données permettant de stocker un entier long (64 bits). Par défaut, les valeurs stockées sont signées, mais elles peuvent devenir non signées si l'on utilise le mot-clé unsigned. Ce type est équivalent au type __int64 des compilateurs C/C++ pour Windows ;

  • float, type de données permettant de stocker un nombre à virgule flottante sur 32 bits. Ce type n'a pas d'équivalent direct en C/C++ (le format des nombres à virgule flottante n'est pas normalisé) ;

  • double, type de données permettant de stocker un nombre à virgule flottante sur 64 bits. Ce type n'a pas d'équivalent direct en C/C++ ;

  • void *, type de donnée permettant de stocker un pointeur sur un contexte d'exécution 32 bits ;

  • handle_t, type de donnée primitif permettant de stocker un handle.

Note: Les mots-clés short, small, long et hyper sont en réalité des qualificateurs de taille du type int. Ils peuvent cependant être utilisés directement, sans le mot-clé int.

Les mots-clés short et small sont synonymes.

Il est possible de définir des structures et des types complexes à l'aide du mot-clé typedef du C. Par exemple :

typedef struct
{
long int i;
float *pf;
} s;

Note: La syntaxe du C++ pour la définition des classes et des structures n'est pas acceptée par MIDL. Il faut utiliser le mot-clé typedef.

Tous les types prédéfinis de Windows et toutes les interfaces de base de DCOM doivent être définies au début du fichier IDL. Ces déclarations sont données dans le fichier unknwn.idl, cependant, ce fichier ne doit pas être inclus à l'aide de la directive #include du préprocesseur. En effet, ceci aurait pour conséquence d'inclure les déclarations des prototypes de fonctions des interfaces de base de DCOM, ce qui forcerait la création de stublets et de facelets pour ces fonctions. Or ceux-ci sont fournis par DCOM lui-même, et le fichier IDL ne pourrait donc pas être compilé. Pour résoudre ce problème, le mot-clé import a été défini dans IDL. Ce mot-clé permet d'inclure les déclarations d'un fichier IDL, mais pas les définitions. Sa syntaxe est la suivante :

import "fichier.idl";

Note: On veillera surtout à ne pas oublier le point-virgule à la fin de la directive import. La syntaxe n'est donc pas la même que celle de la directive #include.

En fait, le mot-clé import permet également de lancer une nouvelle instance du préprocesseur sur le fichier importé. Ceci signifie que les macros définies dans ce fichier sont traitées indépendamment de celles du fichier qui l'importe, et qu'elles n'y sont donc plus définies.

Les interfaces sont définies à l'aide du mot-clé interface. Ce mot-clé doit être immédiatement suivi du nom de l'interface, puis d'une interface dont cette dernière hérite (elle hérite au moins de IUnknown), et enfin de la déclaration des méthodes de l'interface. La syntaxe utilisée est exactement celle du C++ pour la définition des classes, à ceci près que le mot-clé class a été remplacé par le mot-clé interface et que toutes les méthodes sont bien évidemment publiques :

interface nom : base
{
// Description des méthodes.
};

Note: IDL ne supporte pas l'héritage multiple.

Il faut obligatoirement spécifier une interface de base.

La description des méthodes est réalisée avec une syntaxe similaire de la déclaration des fonctions en C. En fait, la syntaxe est exactement la même, à ceci près que des informations additionnelles sont données pour renseigner MIDL sur la manière dont ces fonctions seront utilisées.

D'une manière générale, un certain nombre d'éléments de la description peuvent recevoir des attributs qui les définissent précisément. Ces attributs précèdent toujours l'élément qu'ils qualifient et sont donnés entre crochets, à l'aide de mots-clés spécialisés. Lorsque plusieurs attributs sont donnés, ils sont séparés par des virgules :

[
attribut, attribut, etc…
]

L'un des attributs les plus important est introduit par le mot-clé uuid. C'est tout simplement le GUID associé à un objet donné (interface, composant, librairie). Cet attribut doit apparaître en premier, avant les autres. La syntaxe du mot-clé uuid est donnée ci-dessous :

uuid(valeur)
valeur est le GUID représenté en hexadécimal, sans les accolades.

Un autre attribut important est l'attribut qui signale qu'une interface est définie dans le cadre de DCOM. MIDL est capable de générer des proxy et des stubs pour les RPC en plus des facelets et des stublets pour DCOM (les fichiers générés ne portent d'ailleurs pas le même nom). Afin de lui signaler quel type de fichiers sources il doit générer, il faut utiliser le mot-clé object. La syntaxe de ce mot-clé est élémentaire, puisqu'il suffit de l'utiliser comme un simple attribut :

object

Viennent ensuite les attributs qui permettent de spécifier des informations pour l'utilisateur du composant. Ces informations apparaîtront dans les type libraries qui seront générées par MIDL. Le premier de ces attributs est la chaîne de caractères qui donne de l'aide sur un élément particulier. Cette chaîne de caractères est introduite à l'aide du mot-clé helpstring :

helpstring("Description de l'élément qualifié par helpstring")

Comme les type libraries peuvent contenir des messages en langage naturel, elles sont dépendantes du langage utilisé. C'est pour cette raison que le mot-clé lcid a été introduit : il permet de spécifier le langage utilisé pour une librairie de type. Différents codes sont définis dans le fichier d'en-tête olenls.h. Cependant, le code le plus courant est bien entendu 0x0000, qui ne spécifie aucun langage particulier. Le langage utilisé est alors un anglais neutre en général. La syntaxe du mot-clé lcid est donnée ci-dessous :

lcid(code)
code est le code du langage utilisé.

Enfin, le mot-clé version permet de spécifier la version d'une type library. Sa syntaxe est la suivante :

version(numéro)
numéro est le numéro de version. Ce numéro doit être constitué de deux nombres séparés par un point, par exemple 1.0.

Les attributs présentés ci-dessus servent uniquement à titre de renseignement, cependant, la plupart des attributs restants sont utilisés pour préciser la sémantique des méthodes des interfaces et de leurs paramètres. Ils permettent de renseigner MIDL sur la manière dont le code de marshalling doit être généré dans les facelets et les stublets. En particulier, les indications données suffisent à déterminer quels sont les paramètres en entrée, les paramètres en sortie d'une méthode, et la description des types complexes (tableaux, listes chaînées, etc…).

Les attributs in et out permettent de spécifier le sens dans lequel les paramètres sont passés entre l'appelant et l'appelé. Il est possible qu'un paramètre soit spécifié à la fois en tant que paramètre in et out. Il est très important de bien donner ces informations, car elles permettent à MIDL de ne copier les valeurs de ces paramètres que lorsque cela est nécessaire. Un paramètre in ne voit sa valeur copiée que de l'espace d'adressage du client vers celui du serveur avant l'appel de la fonction du serveur. Inversement, un paramètre out voit sa valeur copiée de l'espace d'adressage du serveur vers celui du client en retour de fonction. Enfin, les paramètres in, out sont copiés à la fois à l'appel et au retour de fonction. Ces copies pouvant se faire à travers un réseau, il est nécessaire de ne copier que les paramètres pour lequel ceci est vraiment nécessaire.

L'attribut async permet d'indiquer à MIDL qu'une méthode d'une interface doit être exécutée de manière asynchrone. Ceci signifie que le client poursuit son exécution juste après avoir effectué l'appel à cette méthode, sans attendre que celle-ci ne se termine. Bien entendu, dans ce cas, la méthode ne peut pas renvoyer de code d'erreur, le client ne l'utiliserait d'ailleurs pas. Elle doit donc renvoyer void. De même, tous ses paramètres doivent être in seulement, aucun paramètre ne peut être modifié en retour par le serveur.

L'attribut string permet d'indiquer à MIDL que le paramètre qualifié est une chaîne de caractère. Le paramètre en question doit être un tableau d'éléments du type char, wchar_t, byte ou un type équivalent, ou un pointeur sur un tableau d'éléments de ce type. Lorsque cet attribut est utilisé, la longueur de la chaîne de caractère est déterminée dynamiquement avec la convention du C : la fin de chaîne est marquée par un caractère nul. Cet attribut ne peut donc pas être utilisé avec des langages qui ne traitent pas les chaînes de caractères comme le C. Si l'on veut transmettre une chaîne de caractères de taille fixe, il est préférable de décrire cette chaîne de caractères comme un tableau.

Les tableaux sont déclarés avec la syntaxe du C/C++ dans MIDL. MIDL utilise la même sémantique que le C/C++ pour les tableaux : la borne inférieure doit toujours être 0, et les tableaux de dimension deux ou plus sont traités comme des tableaux de tableaux, et il n'est pas obligatoire de préciser la taille de la dernière dimension. Cependant, dans ce cas, la borne supérieure doit pouvoir être déterminée lors de l'exécution du programme. Pour cela, il faut indiquer le nom d'un identificateur qui contiendra la taille du tableau. Lorsque le tableau est défini à l'intérieur d'une structure, cet identificateur doit être l'un des champs de cette structure, et lorsque le tableau est un des paramètres d'une fonction, cet identificateur doit être un autre paramètre de cette fonction. Pour spécifier quel identificateur doit être utilisé, il faut utiliser le mot-clé size_is. Sa syntaxe est la suivante :

[size_is(identificateur)] tableau[]

Lorsque l'on utilise des tableaux de pointeurs, il est nécessaire que chacun des éléments du tableau soit initialisé et pointent sur de la mémoire valide.

La manipulation des pointeurs est une opération difficile à réaliser pour MIDL, pour diverses raisons. La première est que lorsqu'un pointeur est passé en paramètre, il est nécessaire d'effectuer les opérations de marshalling pour les données pointées en plus du pointeur. La deuxième raison est que les pointeurs et les références conduisent souvent à la création d'alias dans le code, c'est à dire à la possibilité d'accéder de plusieurs manières différentes à une même variable donnée (par exemple, l'identificateur qui la représente et par son adresse). Afin de faciliter la tâche à MIDL, un certain nombre d'attributs sont disponibles pour qualifier les pointeurs et préciser leurs propriétés.

Si le pointeur utilisé ne dispose d'aucune propriété particulière, MIDL doit effectuer toutes les vérifications avant d'effectuer le marshalling de ce pointeur. En particulier, il doit vérifier la présence d'alias et de cycles de pointeurs éventuels dans les structures chaînées par pointeurs. Ce type de pointeurs qui demandent le plus de calculs, peut être déclaré à l'aide du qualificateur ptr. Les propriétés de ces pointeurs sont les suivantes :

  • ils peuvent avoir la valeur nulle à l'appel ;

  • le serveur peut changer leur valeur ;

  • les données pointées peuvent être accédées par d'autres identificateurs accessibles par le serveur.

Si le pointeur utilisé est le seul moyen dont dispose le serveur pour accéder aux données qu'il référence, ou autrement dit s'il n'y a pas d'alias sur ces données, ce pointeur est dit unique. Les pointeurs uniques peuvent être qualifiés à l'aide du mot-clé unique. Les pointeurs uniques possèdent les propriétés suivantes :

  • ils peuvent avoir la valeur nulle à l'appel ;

  • le serveur peut changer leur valeur.

Enfin, si le pointeur n'est utilisé que pour accéder à une donnée par indirection, c'est à dire si la donnée peut être passée directement par valeur lors de l'appel, le pointeur est dit pointeur par référence. Les pointeurs de ce type sont qualifiés par le mot-clé ref. Les pointeurs par références doivent respecter les contraintes suivantes :

  • ils doivent être initialisés lors de l'appel, et pointer sur des données valides (autrement dit, ils ne peuvent pas contenir la valeur nulle) ;

  • ils ne doivent pas changer de valeur au cours de l'exécution du code du serveur ;

  • ils ne doivent pas être des alias d'autres identificateurs.

Il est évident que les pointeurs par références sont les plus efficaces. En pratique, on utilisera souvent les pointeurs uniques cependant, car ils conviennent dans la plupart des situations.

Note: Lors de la manipulation des pointeurs, un certain nombre de problèmes peuvent se poser, essentiellement lorsqu'une allocation mémoire doit avoir lieu. Une des règles à respecter est que pour les pointeurs qui ne sont pas par références, les données pointées doivent obligatoirement être allouées, parce que le serveur est susceptible de désallouer cette mémoire. De plus, les allocations et libérations de mémoire doivent toujours se faire avec l'allocateur de DCOM, par l'intermédiaire de l'interface IMalloc. Enfin, il faut savoir que de la mémoire peut être perdue lorsqu'un pointeur passé en paramètre contient une adresse valide sur un bloc alloué et que le serveur met à la valeur nulle ce pointeur. Si l'adresse n'est pas stockée au niveau du client dans une variable annexe, le bloc de mémoire est perdu. Il est également bon de savoir que lorsque le serveur change la valeur d'un pointeur en une autre valeur, les données pointées par la nouvelle valeur sont supposée être du même type que celles pointées par l'ancienne valeur. Ceci implique qu'elles ont la même taille, et que dans le cas des pointeurs out, les données sont recopiées au retour de la fonction dans le même bloc mémoire que celui qui a été utilisé par le client pour les passer. Le pointeur ne change donc pas de valeur du côté du client.

Nous avons vu la manière de décrire les interface, les méthodes et les paramètres des méthodes dans les paragraphe suivant. Mais MIDL permet de faire bien plus que cela, puisque depuis la version 3 de MIDL, il est possible d'inclure la définition des type libraries dans les fichiers IDL. Ceci permet d'éviter d'avoir à redéfinir les interfaces à la fois dans les fichiers IDL et dans les fichiers ODL : une seule définition suffit à présent. Nous allons maintenant voir comment les type libraries sont définies dans les fichiers IDL.

La génération d'une type library se fait à l'aide du mot-clé library. Ce mot-clé s'utilise avec la syntaxe suivante :

library nom_librairie
{
coclass nom_composant
{
interface I1;
interface I2;
etc…
}
};
nom_librairie est le nom de la type librairy que MIDL va générer (Attention ! Ce n'est pas forcément le nom du fichier .TLB généré), nom_composant est le nom d'un des composants décrits par cette librairie et I1, I2, etc  sont des interfaces déjà définies.

Note: La syntaxe donnée ci-dessus est simplifiée. C'est la syntaxe minimale pour générer une type library.

Les attributs des différents éléments n'ont pas été précisés. En réalité, il faut donner au moins un GUID pour la type library et un CLSID pour le composant.

Pour illustrer ce qui a été dit jusqu'à présent, un fichier IDL simple est fournit ci-dessous. Cet exemple complet est le fichier IDL utilisé pour la description du composant Adder. Ce fichier est commenté à chaque étape de la description et sa lecture ne devrait pas poser de problèmes.

Exemple 12. Fichier IDL pour le composant Adder

// Ce fichier contient la description de l'interface IAdder.
// Il sert pour la génération des stublets/facelets,
// et pour la génération des librairies de types.
// Toutes les définitions de bases doivent être incluses, mais
// il ne faut pas inclure les prototypes de fonctions. Le mot-clé
// import est donc utilisé à la place de la directive #include :
import "unknwn.idl";
// Définition de l'interface IAdder :
// Tous les éléments d'un fichier IDL peuvent avoir des attributs.
// Ces attributs sont donnés avant l'élément, entre crochets :
[
// Les GUID sont indiqués avec le mot-clé uuid. Ils doivent impérativement
// apparaître en premier :
uuid(e3261620-0ded-11d2-86cc-444553540000),
// Le mot-clé object permet d'indiquer à MIDL qu'il doit générer
// les fichiers des facelets/stublets pour une interface DCOM, et non pas
// les fichiers pour le RPC de DCE :
object,
// Le mot-clé helpstring permet de donner une description de l'objet
// défini :
helpstring("Definition of the IAdder interface")
]
interface IAdder : IUnknown
{
[
helpstring("Adds two integers and returns the result")
]
// Les paramètres des fonctions peuvent être également qualifiés.
// Les options les plus utiles sont in, out et in out, qui indiquent
// le sens de passage des paramètres :
HRESULT Add([in] long i, [in] long j, [out] long *pResult);
[
helpstring("Substracts two integers and returns the result")
]
HRESULT Sub([in] long i, [in] long j, [out] long *pResult);
};
// Définition de l'interface IOpposite :
[
uuid(e3261621-0ded-11d2-86cc-444553540000), object,
helpstring("Definition of the IOpposite interface")
]
interface IOpposite : IUnknown
{
[
helpstring("Calculates the opposite of an integer")
]
HRESULT Opposite([in] long i, [out] long *pResult);
};
// Définition de la type library pour le composant ADDER :
[
uuid(128abb80-0e9a-11d2-86cc-444553540000),
helpstring("Adder Type Library"),
// Le mot-clé lcid permet d'indiquer un code de langue pour la librairie.
// Le code nul correspond au langage neutre (le langage utilisé pour le
// codage), c'est très souvent l'anglais de base :
lcid(0x0000),
// Le mot-clé version permet d'indiquer le numéro de version de l'objet
// qu'il qualifie :
version(1.0)
]
library AdderTypeLibrary
{
// Le mot-clé coclass permet de définir un composant DCOM
// avec les interfaces qu'il gère :
[
uuid(91e132a0-0df1-11d2-86cc-444553540000),
helpstring("Description of the Adder Component")
]
coclass Adder
{
// Attention à ne pas inclure l'interface IUnknown ici. Elle est
// incluse automatiquement par IDL et ne doit pas l'être à nouveau.
interface IAdder;
interface IOpposite;
}
};

Voici à présent le fichier IDL contenant la description de l'interface du composant Calculator :

Exemple 13. Fichier IDL pour le composant Calculator

import "unknwn.idl";
// Définit l'interface IMultiplier :
[
uuid(e3261622-0ded-11d2-86cc-444553540000), object,
helpstring("Definition of the IMultiplier interface")
]
interface IMultiplier : IUnknown
{
[
helpstring("Multiplies two integers and returns the result")
]
HRESULT Mul([in] long i, [in] long j, [out] long *pResult);
};
// Importe la définition des interfaces de Adder :
import "..\..\Adder\AdderPrx\adder.idl";
[
uuid(128abb81-0e9a-11d2-86cc-444553540000),
helpstring("Calculator Type Library"),
lcid(0x0000),
version(1.0)
]
library CalculatorTypeLibrary
{
[
uuid(91e132a1-0df1-11d2-86cc-444553540000),
helpstring("Description of the Calculator Component")
]
coclass Calculator
{
interface IAdder;
interface IOpposite;
interface IMultiplier;
}
};

40. Comment compiler les fichiers IDL ?

Les fichiers IDL sont compilés avec le programme MIDL, fourni dans le kit de développement pour Windows. La syntaxe de MIDL est relativement simple :

MIDL /app_config fichier.idl

Où fichier est le nom du fichier IDL. L'option /app_config permet de simplifier l'écriture des fichiers IDL. Sans cette option, il est nécessaire de générer un fichier ACF (Application Configuration File) en plus du fichier IDL. Ce fichier contient toutes les définitions dépendantes du système. L'option /app_config permet d'inclure ces définitions automatiquement.

Note: MIDL utilise le préprocesseur C de Microsoft par défaut pour traiter les directives de préprocesseur du fichier IDL. Si vous ne disposez pas de ce préprocesseur, vous pouvez en indiquer un autre à l'aide de l'option /cpp_cmd. Le préprocesseur indiqué doit malgré tout être dans l'un des répertoires du path de votre ordinateur pour que MIDL fonctionne correctement.

MIDL génère alors les fichiers suivants :

fichier.h

Ce fichier contient la déclaration des interfaces définies dans le fichier IDL. Ce fichier d'en-tête peut être utilisé aussi bien avec un compilateur C qu'avec un compilateur C++ ;

fichier_i.c

Ce fichier contient la définition des CLSID et GUID des composants et de leurs interfaces définis dans le fichier IDL. Le fichier objet obtenu en compilant ce fichier doit être lié avec les clients et avec les serveurs ;

fichier_p.c

Ce fichier contient le code C des stublets et des facelets. Une fois compilé, il permet de générer une DLL unique, qui servira à la fois pour le proxy sur la machine où tourne le client et pour le stub sur la machine où tourne le serveur ;

dlldata.c

Ce fichier contient des données propres aux stublets et aux facelets. Il doit être compilé et lié avec le fichier objet issu de la compilation de fichier_p.c ;

fichier.tlb

Ce fichier est la type library contenant la description des interfaces et des composants définis dans le fichier IDL.

Note: MIDL n'accepte malheureusement pas d'autres types que HRESULT et void pour les valeurs de retour des méthodes des interfaces. Si l'on doit écrire un composant qui utilise une interface dont une des méthodes renvoie un autre type, il faut abandonner le marshalling standard, et faire en sorte que ce composant gère lui-même son propre marshalling.

De plus, les proxies et les stubs générés par MIDL n'acceptent pas le mécanisme d'agrégation. Il est donc impossible de réaliser un agrégat d'objets qui utilisent le marshalling pour communiquer entre eux. Cependant, rien n'interdit d'utiliser les mécanismes d'agrégation pourvu que les composants gèrent eux-même le marshalling de leurs interfaces.

41. Comment compiler les fichiers produits par MIDL ?

Les fichiers produits par MIDL doivent être compilés de la manière suivante :

  • le fichier fichier_i.c doit être compilé indépendamment des autres pour définir les CLSID et GUID des interfaces du composant. Le fichier objet obtenu devra donc être lié aussi bien avec le client qu'avec le composant, ou la DLL contenant les stublets et les facelets ;

  • les fichiers fichier_p.c et dlldata.c doivent être compilés et liés avec le fichier issu de la compilation de fichier_i.c pour générer une DLL. Cette DLL contiendra les facelets et les stublets pour le proxy et le stub standard, pour toutes les interfaces des composants décrits dans le fichier IDL.

On prendra garde lors de la compilation à respecter les consignes suivantes :

  • définir la macro REGISTER_PROXY_DLL, qui permet de générer le code nécessaire pour l'enregistrement automatique de la DLL dans la base de registre par DCOM ;

  • compiler les fichiers pour une cible multithreadée ;

  • utiliser un alignement de 8 octets pour les données ;

  • inclure toutes les options nécessaires pour générer une DLL Windows ;

  • supprimer les fonctionnalités spécifiques des compilateurs (par exemple, contrôles de débordement de piles, gestion des exceptions, etc…).

De plus, lors de l'édition de lien, il faudra penser à respecter les règles suivantes :

  • inclure la librairie statique RPCRT4.LIB ;

  • exporter les fonctions DllGetClassObject, DllCanUnloadNow, DllRegisterServer, DllUnregisterServer et GetProxyDllInfo, fonction qui sont définies dans la librairie précédemment citée ;

  • indiquer toutes les options pour générer une DLL Windows.

Note: On fera très attention au processus de compilation et d'édition de lien de ces fichiers, car de nombreux problèmes peuvent survenir, surtout si l'on n'utilise pas un compilateur Microsoft (les compilateurs Microsoft utilisent les options par défaut qui conviennent). Les problèmes les plus courants concernent les conventions d'appel. Il est nécessaire de bien utiliser les macros du type STDMETHODCALLTYPE lors de la définition de ses méthodes d'interfaces afin d'éviter des conflits entre les clients et les proxies d'une part, et entre les stubs et les serveurs d'autre part. Si vous rencontrez des problèmes ou de plantages lors de l'exécution de votre programme, vérifiez toutes les conventions d'appel.

Un autre problème peut apparaître lors de l'exportation des fonctions des DLL générées : certains environnements de développement les exportent avec la décoration du C ou du C++. Windows attend que ces fonctions soient exportées exactement avec le nom qui leur a été donné ci-dessus, sans décoration et en respectant la casse. Si vous rencontrez des problèmes de chargement de DLL, vérifiez le nom des fonctions exportées.

Enfin, il se peut que les options de compilation diffèrent selon que la cible est une DLL ou un exécutable. Dans ce cas, il est nécessaire de compiler deux fois les fichiers utilisés en commun par le proxy et le stub d'une part, et par les clients et les composants d'autre part (par exemple, le fichier fichier_i.c). Il est recommandé en général de les compiler plusieurs fois, dans des répertoires différents, afin d'éviter tout conflit éventuel entre les cibles qui utilisent des fichiers communs.

42. Comment pour enregistrer un composant ?

Que les serveurs soient in-process ou out-of-process, il est nécessaire d'ajouter des entrées dans la base de registres pour que le système puisse les localiser à partir de leur CLSID. De plus, pour les composants out-of-process, il est nécessaire de définir, pour chacune des interfaces, le composant in-process capable d'effectuer le marshalling de cette interface (c'est à dire le composant contenant le proxy pour ce composant, ou la DLL contenant les facelets et les stublets pour les interfaces de ce composant). Enfin, lorsqu'un composant dispose d'une type library, il faut également enregistrer cette librairie dans la base de registres.

Tous ces paramètres sont écrits sous la forme de clés et de valeurs, qui sont toutes regroupées dans la sous clé HKEY_CLASSES_ROOT de la base de registres du système. Pour cela, il existe deux possibilités. La première consiste à écrire un fichier .REG contenant les informations nécessaires pour enregistrer ce composant. Cette technique est facile, mais elle est peu pratique, parce que les fichiers ainsi écrits doivent être modifiés selon l'emplacement des serveurs dans le système. La deuxième méthode est de faire en sorte que les serveurs soient capables de s'enregistrer eux-même. Elle est plus fiable, mais nettement plus compliquée, puisqu'il faut écrire des fonctions pour enregistrer les serveurs.

Les serveurs out-of-process peuvent être enregistrés automatiquement en regardant leur ligne de commande. S'ils reçoivent l'option /REGSERVER ou -REGSERVER (pas forcément écrites en majuscules), c'est qu'ils doivent s'enregistrer. Ils devront donc utiliser les fonctions du système pour écrire dans la base de registres. En revanche, s'ils reçoivent l'option /UNREGSERVER ou -UNREGSERVER, c'est qu'ils doivent se désenregistrer et retirer les entrées qui les concernent dans la base de registres.

Les serveurs in-process procèdent différemment. Ils doivent exporter les fonctions DllRegisterServer et DllUnregisterServer pour respectivement s'enregistrer et se désenregistrer automatiquement.

La syntaxe des fichiers .REG sera utilisée pour décrire les entrées de la base de registres nécessaires à l'enregistrement des composants. Cette syntaxe est décrite dans le paragraphe suivant.

43. Comment écrire un fichier .REG ?

Les fichiers .REG permettent d'ajouter les entrées dans la base de registres pour un composant. Ils sont écrit avec la syntaxe suivante :

REGEDIT4
[clé]
"Nom"=valeur
"Nom"=valeur
…
[clé]
…
…

La première ligne des fichiers .REG identifie la version de l'éditeur de base de registres capable de lire ce fichier. La syntaxe qui est donnée ici est celle de l'éditeur de Windows 95 et de celui de Windows NT, ce qui correspond à la version 4 de ce programme.

Les entrées de la base de registres sont ensuite décrites de la manière suivante : la clé dans laquelle elles apparaissent est donnée avec son chemin complet dans la base de registres, en partant d'une des clés de base (en pratique, HKEY_CLASSES_ROOT pour DCOM), entre crochets. Puis, les valeurs contenues dans cette clé sont décrites, une à une, à l'aide d'un couple nom de la valeur (indiqué par le terme Nom dans la syntaxe ci-dessus) entre guillemets, d'un signe d'égalité et de la valeur associée. La valeur de la clé elle-même est référencée par le nom @ dans le fichier .REG, sans les guillemets. Les valeurs de type string doivent être spécifiées entre guillemets. Les sous-clés ne sont pas introduites comme des valeurs. Il faut les définir comme des clés à part entière.

44. Quelles sont les entrées à rajouter dans la base de registres pour enregistrer un composant ?

Les entrées les plus importantes sont placées dans la sous-clé CLSID de la clé HKEY_CLASSES_ROOT. Lorsqu'un client cherche à utiliser un composant, la seule information dont le système dispose est le CLSID de ce composant. Les informations complémentaires sont donc inscrites dans une sous-clé de la clé CLSID, dont le nom est exactement le CLSID du composant, donné entre accolades. Cette clé aura pour valeur le nom du composant, décrit sous un format compréhensible par les êtres humains. Elle aura donc la forme suivante :

[HKEY_CLASSES_ROOT\CLSID\{91e132a0-0df1-11d2-86cc-444553540000}]
@="Adder Component 1.0"

Les informations nécessaires au système pour la localisation des fichiers du serveur, que celui-ci soit un serveur in-process ou exécutable, seront stockées dans cette clé. Ces informations sont stockées dans deux sous-clés différentes, selon la nature du serveur. Pour les serveurs exécutables, la sous-clé utilisée est nommée LocalServer32. Sa valeur est le chemin absolu vers le fichier exécutable que le système doit lancer pour satisfaire la requête du client. Par exemple, pour le composant Adder, cette clé pourrait être comme ceci :

[HKEY_CLASSES_ROOT\CLSID\{91e132a0-0df1-11d2-86cc-444553540000}\LocalServer32]
@="c:\\winnt\\system32\\ExeAdder.exe"

Pour les serveurs in-process, la sous-clé utilisée porte le nom InprocServer32. Sa valeur est, encore une fois, le chemin complet vers le fichier du serveur. Par exemple, encore pour le composant Adder, elle sera définie par :

[HKEY_CLASSES_ROOT\CLSID\{91e132a0-0df1-11d2-86cc-444553540000}\InprocServer32]
@="c:\\winnt\\system32\\Adder.dll"

Les autres informations enregistrées dans la clé du CLSID du composant ne sont pas nécessaires pour le bon fonctionnement du système. Cependant, elles permettent de stocker des informations spécifiques au composant, en particulier, sa description et sa version. Ces informations sont susceptibles d'être utilisées par des outils de développement rapides, tels que Visual Basic par exemple, afin de définir le composant.

Pour ces outils, il est nécessaire de donner un nom au composant, nom qui sera utilisé par les outils. Ce nom peut être défini à l'aide d'une sous-clé nommée ProgId. La valeur de cette sous-clé décrit l'éditeur du logiciel, le composant et la version installée de ce composant. Ces trois paramètres sont séparés par un point. Par exemple, pour le composant Adder, on pourra définir la sous-clé suivante :

[HKEY_CLASSES_ROOT\CLSID\{91e132a0-0df1-11d2-86cc-444553540000}\ProgId]
@="DCOMFAQ.Adder.1.0"

Lorsque l'on définit cette sous-clé dans la base de registre, il est nécessaire de réaliser un chaînage arrière pour permettre de retrouver le CLSID d'un composant à partir de son nom. Ceci se fait en définissant une sous-clé de la clé HKEY_CLASSES_ROOT portant le nom du composant. La valeur de cette sous-clé est un nom lisible par les êtres humains décrivant le composant. Dans cette sous-clé, il faut définir une autre sous-clé, nommée CLSID, et dont la valeur est le CLSID du composant. Par exemple, pour le composant Adder, on a :

[HKEY_CLASSES_ROOT\CCC.Adder.1.0]
@="Adder Component 1.0"
[HKEY_CLASSES_ROOT\CCC.Adder.1.0\CLSID]
@="{91e132a0-0df1-11d2-86cc-444553540000}"

Enfin, il est également possible de donner un nom au composant qui est indépendant de l'éditeur du logiciel et de la version. Pour cela, il faut définir une sous-clé de la clé du CLSID du composant portant le nom VersionIndependantProgId. La valeur de cette clé est le nom du composant, sans les informations sur l'éditeur et sur la version. Par exemple :

[HKEY_CLASSES_ROOT\CLSID\
{91e132a0-0df1-11d2-86cc-444553540000}\VersionIndependentProgId]
@="Adder"

Encore une fois, un chaînage arrière est nécessaire, à l'aide d'une sous-clé de la clé HKEY_CLASSES_ROOT dont le nom est le nom du composant. Cette sous-clé devra contenir, en plus de la sous-clé CLSID, une autre sous-clé nommée CurVer et dont la valeur est le nom complet du composant, éditeur et version compris. Ceci donne, pour le composant Adder :

[HKEY_CLASSES_ROOT\Adder]
@="Adder Component 1.0"
[HKEY_CLASSES_ROOT\Adder\CurVer]
@="CCC.Adder.1.0"
[HKEY_CLASSES_ROOT\Adder\CLSID]
@="{91e132a0-0df1-11d2-86cc-444553540000}"

45. Quelles sont les entrées à rajouter dans la base de registre pour enregistrer une type library ?

Lorsqu'un composant possède une type library, il faut enregistrer cette type library pour que le système puisse la retrouver. Le lien entre le composant et sa type library est fait dans la clé du CLSID du composant. Il suffit de définir une sous-clé nommée TypeLib dont la valeur est le GUID de la type library en question, tel qu'il est défini dans le fichier IDL qui a servi à le générer. De même, pour faire le lien entre les interfaces d'un composant et la type library qui les décrit, il suffit d'ajouter une sous-clé TypeLib dans la clé du GUID de l'interface et dont la valeur est le GUID de la type library. Par exemple, pour le composant Adder, qui gère les interfaces IAdder et IOpposite, les trois entrées suivantes peuvent être ajoutées pour pointer sur la type library du composant :

[HKEY_CLASSES_ROOT\CLSID\{91e132a0-0df1-11d2-86cc-444553540000}\TypeLib]
@="{128abb80-0e9a-11d2-86cc-444553540000}"
[HKEY_CLASSES_ROOT\Interface\{e3261620-0ded-11d2-86cc-444553540000}\TypeLib]
@="{128abb80-0e9a-11d2-86cc-444553540000}"
[HKEY_CLASSES_ROOT\Interface\{e3261621-0ded-11d2-86cc-444553540000}\TypeLib]
@="{128abb80-0e9a-11d2-86cc-444553540000}"

Ces entrées permettent au système de déterminer le GUID de la type library pour un objet donné. Il faut donc qu'il puisse récupérer des informations sur la type library à partir de ce GUID. Ceci est possible en définissant une sous-clé portant comme nom le GUID de la type library dans la sous-clé TypeLib de la clé HKEY_CLASSES_ROOT. Cette sous-clé peut contenir des informations pour plusieurs versions de la même type library. Chacune de ces versions est définie dans une sous-clé dont le nom est la version composée du numéro de version majeur, d'un point et du numéro de version mineure. La valeur de cette sous-clé est une chaîne de caractère décrivant la librairie. Par exemple, pour la type library du composant Adder, on a :

[HKEY_CLASSES_ROOT\TypeLib\{128abb80-0e9a-11d2-86cc-444553540000}]
[HKEY_CLASSES_ROOT\TypeLib\{128abb80-0e9a-11d2-86cc-444553540000}\1.0]
@="Adder Type Library"

Enfin, chaque version peut être décrite dans plusieurs langages. Chaque langage est donné dans une sous-clé dont le nom est l'identificateur de langage en hexadécimal, sans le 0x et sans les 0 non significatifs. Dans cette sous-clé, une sous-clé est encore définie, avec pour nom le nom du système pour laquelle cette librairie a été écrite. En pratique, ce système sera souvent win32. La valeur de cette clé contient le chemin sur le fichier contenant la type library. Pour le composant Adder, la structure des clé identifiant ce fichier est donc celle-ci :

[HKEY_CLASSES_ROOT\TypeLib\{128abb80-0e9a-11d2-86cc-444553540000}\1.0\0\win32]
@="c:\\winnt\\system32\\Adder.tlb"

46. Quels sont les mécanismes mis en ?uvre lors du marshalling ?

47. Comment les interfaces sont-elles marshallées ?

DCOM utilise un mécanisme de marshalling très souple, qui permet aussi bien d'utiliser son propre marshalling que d'utiliser les mécanismes standard de DCOM. Cette souplesse implique une relative complexité dans le marshalling des interfaces. Le mécanisme est décrit un peu plus en détail dans ce paragraphe.

Le mécanisme fondamental du marshalling travaille objet par objet. Ceci signifie que le les proxies et les stubs prennent complètement en charge un objet et un seul. Les opérations effectuées par DCOM sont les suivantes :

  • premièrement, DCOM cherche à déterminer si le composant gère lui-même son propre marshalling. Pour cela, il demande s'il gère l'interface IMarshall. Si ce n'est pas le cas, le marshalling standard de DCOM sera utilisé. Il charge alors un stub générique dans l'espace d'adressage du serveur, qui gère l'interface IMarshall ;

  • ensuite, DCOM essaie de demander au composant le CLSID du proxy qu'il devra charger dans l'espace d'adressage du client (remarquez au passage le nombre de failles dans la sécurité du système qu'un tel mécanisme peut générer…). Si le composant est livré avec un proxy qui lui est spécifique, il peut permettre son utilisation à ce niveau. Dans le cas contraire, DCOM utilisera un proxy générique fourni par le mécanisme de marshalling standard. Quel que soit le proxy utilisé, celui-ci doit impérativement gérer l'interface IMarshall ;

  • DCOM demande au stub ou au composant lui-même s'il gère son propre marshalling les informations qui seront nécessaires au proxy pour se connecter à lui. Ces informations sont récupérées par l'intermédiaire de l'interface IMarshall ;

  • les informations de connexions sont ensuite transmises au code du client qui a demandé une interface sur le composant (ce code peut très bien être CoGetClassObject, dont le client attend la terminaison) ;

  • ce code récupère le CLSID du proxy à utiliser, et le charge dans l'espace d'adressage du client. Il passe ensuite les informations de connexions à ce proxy. Celui-ci établit la connexion avec le stub ou le composant à l'aide de ces informations ;

  • l'interface demandée par le client est ensuite récupérée sur le proxy, et est renvoyée. Le résultat net de l'opération est que le client peut utiliser directement l'interface qui lui est renvoyée : tous les appels seront redirigés vers le proxy. Celui-ci les transmettra au stub, qui réalisera l'appel sur le composant, ou directement au composant, si celui-ci gère son propre marshalling.

Comme on le voit, l'interface IMarshall est au centre du processus de marshalling. Elle doit être implémentée aussi bien du côté client par le proxy, que du côté serveur, que ce soit par le stub générique de DCOM ou par le composant lui-même. IMarshall est déclarée comme suit :

interface IMarshal : IUnknown
{
HRESULT GetUnmarshalClass(
REFIID riid, void *pvInterface,
DWORD dwDestContext, void *pvDestContext,
DWORD mshlflags, CLSID *pclsid);
HRESULT GetMarshalSizeMax(
REFIID riid, void *pvInterface,
DWORD dwDestContext, void *pvDestContext,
DWORD mshlflags, DWORD *pcb);
HRESULT MarshalInterface(
IStream *pS, REFIID riid,
void *pvInterface, DWORD dwDestContext,
void *pvDestContext, DWORD mshlflags);
HRESULT UnmarshalInterface(
IStream *pS, REFIID riid, void **ppvInterface);
HRESULT DisconnectObject(DWORD dwReserved);
HRESULT ReleaseMarshalData(IStream *pS);
};

Seules les trois premières méthodes de cette interface sont utilisées par DCOM pour marshaller une interface. Les dernières méthodes ne sont destinées qu'à l'usage du proxy qui communiquera avec le stub. Le détail des arguments des méthodes de IMarshal n'est pas décrit ici, pour plus de renseignements consulter le SDK d'OLE. Il n'est en général par nécessaire d'utiliser directement l'interface IMarshall pour marshaller une interface. Les opérations de marshalling et de démarshalling sont en effet complètement prises en charge par les fonctions CoMarshallInterface et CoUnmarshallInterface.

CoMarshalInterface commence donc par demander à l'objet concerné s'il gère l'interface IMarshal. Comme on l'a déjà dit, c'est à ce niveau qu'un objet peut gérer son propre marshalling. Si l'objet la gère, le CLSID du proxy à utiliser pour effectuer le démarshalling est demandé par un appel à la méthode GetUnmarshallClass. Le serveur peut ici renvoyer le CLSID d'un composant in-process qui a été écrit spécialement pour lui. Ceci est naturel, puisqu'il gère lui-même son marshalling… En revanche, si l'objet ne gère pas l'interface IMarshall, CoMarhallInterface demande l'interface IPersist, qui permet d'obtenir également le CLSID du proxy à utiliser. Ceci permet de spécifier un proxy spécifique sans implémenter complètement le marshalling du côté du serveur. Si aucune de ces interfaces n'est gérée par le composant, DCOM utilisera le marshalling standard, et chargera le proxy générique, dont le CLSID est CLSID_StdMarshal. Ce proxy est fourni par DCOM lui-même. Dans tous les cas, DCOM dispose du CLSID d'un composant in-process qui servira de proxy.

À ce stade du marshalling, il y a deux possibilités. Soit l'objet est capable de gérer son propre marshalling (il implémente donc l'interface IMarshal), soit l'objet ne le peut pas. Dans ce dernier cas, DCOM charge un stub générique dans l'espace d'adressage du serveur. Ce stub, fourni par DCOM est capable de comprendre les requêtes que le proxy générique peut lui envoyer. Il gère l'interface IMarshall, et va donc être utilisé en lieu et place du composant pour communiquer avec le proxy du client.

Dans tous les cas, DCOM dispose donc du CLSID d'un composant in-process gérant l'interface IMarshall à charger dans l'espace d'adressage du client, et d'un pointeur sur l'interface IMarshall d'un objet du serveur capable de marshaller l'interface du composant (cet objet pouvant être soit le serveur, soit le stub générique). Les opérations effectuées ensuite dans le cadre du marshalling sont absolument génériques, et ne dépendent ni de la nature du proxy utilisé, ni de la capacité du composant à gérer son propre marshalling.

Une fois le CLSID du proxy déterminé, et le stub éventuellement chargé, CoMarshalInterface appelle la méthode GetMarshalSizeMax pour obtenir la taille des informations que DCOM devra transmettre au proxy, afin que celui-ci puisse se connecter au serveur. Cette taille est nécessaire pour l'allocation d'un objet IStream qui recevra ces informations.

L'interface IStream est une interface standard de OLE permettant de manipuler un flux de données de manière uniforme, via les méthodes Write et Read de cette interface.

L'objet IStream ainsi créé est initialisé par appel de la méthode MarshalInterface. Cette dernière méthode stocke les données qui seront nécessaires au proxy pour se connecter à l'objet avec lequel il devra communiquer par la suite (ce sera soit le stub, soit l'instance du composant elle-même). Il est recommandé que les données stockées par l'objet pour le proxy aient un format bien spécifié et indépendant de la plate-forme sur laquelle le proxy s'exécute. En effet, le proxy et le serveur qu'il représente peuvent ne pas fonctionner sur des machines ayant la même architecture. On pourra prendre par exemple la représentation du réseau.

Les données de l'objet IStream sont alors transférées au code du client qui a demandé une interface sur l'objet serveur. Ce transfert est complètement pris en charge par DCOM, et il en résulte la création d'un autre objet IStream contenant les mêmes données du côté du client.

À ce stade du processus, DCOM dispose du CLSID du proxy capable de transférer les appels de méthodes pour l'objet serveur (que ce soit un proxy choisi par le serveur ou le proxy standard) et d'un IStream contenant les informations de connections. DCOM communique donc ces paramètres à la fonction en cours d'exécution dans le client (fonction dont le processus client attend la terminaison). Cette fonction peut être une méthode d'interface d'un proxy dont le rôle serait de créer un nouvel objet ou d'obtenir une nouvelle interface, la fonction CoGetClassObjet ou toute autre méthode nécessitant le marshalling d'une interface.

Quoi qu'il en soit, la fonction qui reçoit ces paramètres appelle avant tout CoCreateInstance pour charger le proxy dont le CLSID a été communiquée par CoMarshalInterface. Que ce soit le proxy standard ou non, ce proxy doit être capable de gérer l'interface IMarshal. DCOM appelle donc la fonction CoUnmarshalInterface, qui elle-même appelle la méthode UnmarshalInterface de l'interface IMarshall du proxy afin de l'initialiser. Cette initialisation se fait à partir du IStream reçu de la part de CoMarhsalInterface. Cette initialisation consiste essentiellement à établir la communication avec le stub ou le composant lui-même dans le serveur. Une fois cette initialisation faite, le proxy renvoie le pointeur sur l'interface demandée par le client, complètement marshallée. Enfin, la méthode ReleaseMarshalData de l'interface IMarshal est appelée pour détruire le IStream alloué par CoMarshalInterface. Le mécanisme de marshalling de l'interface se termine alors, et l'interface renvoyée par le proxy est communiquée au client.

Que le mécanisme de marshalling utilisé soit le mécanisme standard de DCOM ou un marshalling personnalisé, le proxy utilisé par le client devra représenter l'objet serveur dans sa totalité, et être capable de marshaller toutes les interfaces de cet objet. Dans le cas où le composant gère sont propre marshalling, ceci est aisément faisable, puisque c'est le composant lui-même qui indique le CLSID du proxy à utiliser. Il peut donc spécifier un composant in-process qui a été spécialement écrit pour communiquer avec lui. Dans le cas contraire, les choses sont plus difficiles. En effet, le proxy générique ne sait marshaller, a priori, que les interfaces standard de DCOM (dont, bien entendu, IUnknown). Par conséquent, il faut un autre mécanisme pour « apprendre » au proxy standard comment marshaller les requêtes émises par le client.

De même, si le composant gère son propre marshalling, il est évident qu'il sait comment interpréter les données qu'il reçoit de son proxy, puisqu'il a choisi lui-même ce proxy. Mais dans le cas contraire, c'est le stub générique de DCOM qui sera utilisé et qui recevra les données de marshalling du proxy générique. Tout comme ce proxy, le stub devra être capable d'apprendre les interfaces non standard du composant qu'il devra appeler, pour pouvoir satisfaire les requêtes du proxy.

C'est à ce niveau qu'entre en jeu les « facelets » et les « stublets » générés par le compilateur MIDL. En fait, chaque stublet et facelet est gérée par un petit composant, capable de gérer une interface donnée. Ces petits composants sont utilisés à la fois par le proxy et par le stub génériques de DCOM pour marshaller les interfaces qu'ils ne comprennent pas eux-mêmes. Ceci suppose bien entendu que ces composants soient enregistrés dans la base de registres, et qu'une association soit faite entre les UUID des interfaces à marshaller et le composant implémentant le stublet et la facelet correspondants. Cette relation est stockée dans la sous-clé Interface de la clé HKEY_CLASSES_ROOT). Cette sous-clé contient une entrée pour chaque interface, dont le nom est l'UUID de cette interface. Chacune de ces clés contient une sous-clé nommée ProxyStubClsid32, et qui contient le CLSID du composant gérant le stublet et la facelet.

Lorsque le proxy générique ou le stub générique reçoivent une demande concernant une interface non standard, ils commencent donc par rechercher dans la base de registres le CLSID du stublet ou de la facelet qui gère cette interface dans la sous-clé ProxyStubClsid32 de la clé correspondante à l'interface en cours de marshalling. Ils chargent alors ce composant dans l'espace d'adressage du serveur ou du client, et lui communique le pointeur sur l'interface à marshaller. Ainsi, toutes les interfaces non standard peuvent être prises en charge par le proxy et le stub générique de DCOM, pourvu qu'il existe une entrée pour elles dans la sous-clé Interface de HKEY_CLASSES_ROOT.

Note: On notera que le marshalling se fait maintenant interface par interface, et non plus objet par objet (c'est à dire globalement au niveau du proxy et du composant lui-même). Ceci est nécessaire parce que DCOM ne peut pas déterminer a priori toutes les interfaces que l'objet serveur est capable de gérer. Les stublets et facelets sont donc chargés à la demande, lorsqu'une nouvelle interface doit être marshallée.

Il est essentiel que du point de vue du client le proxy se comporte comme un seul objet, à savoir le serveur qu'il représente. Ceci ne pose pas de problèmes dans le cas où le serveur gère son propre marshalling, car le proxy est alors écrit pour ce serveur. En revanche, pour le marshalling standard, il est nécessaire que les facelets soient toutes agrégées dans le proxy. Le proxy standard est donc constitué d'un nombre variable de ces composants (le terme « facelet » a été choisi par opposition au terme « stublet » du stub).

De même, DCOM stipule que deux pointeurs sur la même interface d'un objet doivent être égaux. Il est donc nécessaire que le proxy standard de DCOM réutilise la même facelet pour chaque pointeur sur une interface donnée. Il réalise ce contrôle au sein de sa fonction QueryInterface.

Dans le mécanisme du marshalling, c'est au proxy d'établir la communication avec le serveur, à l'aide des données de marshalling obtenues via l'interface IMarshall du serveur. Toutefois, pour le marshalling standard est géré, cette connexion est prise en charge par la facelet générée par MIDL. Ceci ne change toutefois rien ni pour le client, ni pour le serveur, ni pour le reste de DCOM.

En fait, il n'existe pas réellement de « stub » du coté serveur. En effet, lorsque le serveur est capable de gérer son propre marshalling, il est à la fois serveur et son propre stub. D'autre part, le stub générique de DCOM n'est qu'une structure de donnée utilisé par DCOM en interne, et aucun autre composant que les stublets n'est chargé dans l'espace d'adressage du serveur. Les facelets communiquent donc directement avec les stublets du serveur. Cependant, il est plus simple de considérer qu'il existe réellement un stub pour chaque proxy, et que ceux-ci contiennent respectivement les facelets et les stublets.

48. Comment est géré le compte des références sur les interfaces en cours de marshalling ?

Le résultat du marshalling et du démarshalling est la création d'une nouvelle référence sur un objet, accessible par l'intermédiaire d'une nouvelle interface. Par conséquent, l'objet ainsi référencé voit son compte de référence augmenté d'une unité. Cependant, ce comportement n'est valide que lorsque les données de marshalling ont été démarshallée. Une question importante dans le processus de marshalling est donc de déterminer la politique de gestion du compte de référence entre le moment où une interface est marshallée et le moment où elle est démarshallée.

En général, le fait de marshaller une interface provoque un appel de AddRef sur l'objet possédant cette interface. Ainsi, le client est sûr de pouvoir démarshaller les données de marshalling sans que l'objet n'ait eu le temps d'être détruit par ses éventuels autres clients. Ce comportement convient dans la plupart des cas, puisque les interfaces sont généralement marshallées pour passer un pointeur d'un appartement à un autre. Cependant, il peut arriver des situations dans lesquelles la gestion du compte de référence doit pouvoir être contrôlée directement. En particulier, ceci est nécessaire lorsque l'on désire démarshaller plusieurs fois les mêmes données, afin d'obtenir plusieurs pointeurs d'interfaces successivement sur le même objet.

Si vous regardez la déclaration de la méthode MarshalInterface de l'interface IMarshal, un paramètre mshlFlags est fourni lors du marshalling d'une interface. Ce paramètre permet d'indiquer la raison pour laquelle le marshalling a lieu, et quelle politique de gestion du compte des références et des données de marshalling doit être prise. Les valeurs utilisables les plus importantes sont utilisables :

  • MSHLFLAGS_NORMAL, qui indique que le marshalling a lieu dans le cadre classique du passage d'un pointeur d'interface d'un appartement à un autre. Ce flag signifie que le proxy destination ne pourra utiliser qu'une seule fois ces données, et que la fonction CoReleaseMarshalData (qui elle-même appelle la méthode ReleaseMarshalData de l'interface IMarshal) sera appelée automatiquement. Il est donc inutile de l'appeler soi-même ;

  • MSHLFLAGS_TABLESTRONG, qui indique que les données de marshalling ne seront pas utilisées immédiatement a priori. Elles vont être stockées dans une table globale pour utilisation ultérieure. Ces données peuvent être utilisées plusieurs fois, afin d'obtenir plusieurs pointeurs sur l'interface ainsi marshallée. Ceci implique que l'appel à CoReleaseMarshalData n'est pas fait automatiquement après le démarshalling, c'est donc à l'utilisateur de faire cet appel lorsque les données de marshalling sont supprimées de la table globale. Par ailleurs, l'objet dont l'interface est marshallée est maintenu en mémoire par un appel à sa méthode AddRef, et sa présence est donc garantie tant que CoReleaseMarshalData n'est pas appelée ;

  • MSHLFLAGS_TABLEWEAK, qui permet exactement la même utilisation que MSHLFLAGS_TABLE_STRONG, à ceci près que l'objet source n'est pas bloqué en mémoire. Il est donc possible que les données de marshalling référencent un objet qui n'existe plus lorsqu'elles seront utilisées. Dans ce cas, le démarshalling échouera évidemment, et le client devra prendre les mesures appropriées. Notez que l'appel à CoReleaseMarshalData est toujours nécessaire.

49. Comment utiliser son propre marshalling ?

Bien que le mécanisme de marshalling des interfaces puisse paraître impressionnant, il est parfois utile de définir son propre marshalling. Par exemple, lorsqu'un client appelle une fonction d'un serveur qui est lui-même le proxy d'un autre serveur, les temps de communications s'accroissent inutilement. En effet, les paramètres passent par un proxy de proxy, etc… Le serveur peut donc mettre en contact directement le proxy du client avec le serveur qu'il représente, et se court-circuiter ainsi complètement. Un autre exemple courant est celui des objets transactionnels. Un proxy écrit spécifiquement pour ce type d'utilisation peut stocker toutes les demandes de modification de l'objet, et ne les transmettre à l'objet que lors de la validation de la transaction en cours.

Quoi qu'il en soit,le marshalling standard est simple implémenter. Du côté du serveur, il suffit d'implémenter l'interface IMarshal et le mécanisme de réception des demandes de la part du proxy. Par exemple, si le proxy et le serveur sont prévus pour fonctionner sur une même machine, il est possible d'utiliser des messages de fenêtres postés d'une application à une autre. Du côté client, il faut implémenter le proxy, ce qui est un peu plus compliqué. En effet, le proxy doit gérer toutes les interfaces du serveur, et effectuer la communication avec celui-ci dans l'implémentation de ces interfaces. Il doit également gérer l'interface IMarshal, pour établir cette communication.

Note: Lorsque l'on implémentera son propre marshalling, on devra veiller particulièrement à ce que les appels soient sérialisés dans les serveurs qui utilisent le modèle d'appartement monothreadé. Ceci signifie qu'en pratique, ces appels doivent effectivement être faits par le thread de l'appartement. La technique utilisée par le mécanisme de marshalling standard est la plus simple : l'emploi de la boucle des messages résout tous les problèmes de sérialisation de manière générale dans Windows (cette boucle est le paradigme de base de la programmation Windows). Pour les serveurs utilisant le modèle d'appartement multithreadé, il est inutile de prendre ces précautions, si bien que le marshalling est plus simple à réaliser avec ce type de serveurs.

50. Comment marshaller une interface d'un thread à un autre ?

DCOM utilise le processus de marshalling dès lors qu'un pointeur sur une interface doit être passé d'un appartement à un autre. En général, ce processus a lieu au sein des fonctions de DCOM qui renvoient une interface, ou lors du passage d'un pointeur d'interface en paramètre d'entrée ou de sortie d'une méthode d'une autre interface. Cependant, il se peut qu'ils soit nécessaire de marshaller soi-même une interface pour la passer d'un thread à un autre dans un même application, mais qui ne sont pas dans un même appartement. Cette opération peut être réalisée à l'aide des fonctions CoMarshalInterThreadInterfaceInStream et CoGetInterfaceAndReleaseStream. Ces fonctions ne font rien d'autre que d'appeler les fonctions de marshalling standard de DCOM avec les bons paramètres, à savoir les fonctions CoMarshalInterface et CoUnmarshalInterface. Elles permettent de transformer une interface en IStream et réciproquement, d'obtenir une interface à partir de ce IStream. Entre ces deux appels de fonctions, le mécanisme de marshalling complet a eu lieu, ce qui rend le deuxième pointeur sur l'interface utilisable dans l'appartement dans lequel il a été obtenu.

La fonction CoMarshalInterThreadInterfaceInStream utilise l'option TABLE_STRONG lors du marshalling de l'interface source. Ceci signifie qu'un AddRef est effectué sur l'objet dont on marshalle l'interface. Autrement dit, cet objet est maintenu en mémoire tant que l'interface n'est pas démarshallée et qu'un Release n'est pas appelé sur le pointeur renvoyé par CoGetInterfaceAndReleaseStream. Le résultat net de l'utilisation de ces deux fonctions est donc exactement équivalent à un appel à QueryInterface : une nouvelle interface est obtenue, et le compte de référence de l'objet auquel appartient cette interface est augmenté de un.

Il faut savoir que l'on ne peut pas conserver le IStream ad vitam eternam. Celui-ci doit en effet être utilisé rapidement, car DCOM libère automatiquement la référence sur l'objet dont il contient les données de marshalling au bout d'un certain temps s'il n'a pas été utilisé (environ 6 minutes). Dans ce cas, la fonction CoGetInterfaceAndReleaseStream renverra un code d'erreur, signalant que l'objet est déconnecté.

Par ailleurs, les données stockées dans l'objet IStream ne peuvent être utilisées qu'une seule fois. On ne doit pas tenter de démarshaller deux fois de suite ces données, même en effectuant un AddRef sur le IStream pour le conserver et en le réinitialisant.

51. Quelles sont les registres à rajouter pour enregistrer les proxies et les stubs ?

Les composants utilisés au travers du processus de marshalling nécessitent l'enregistrement des proxies et des stubs pour chacune de leurs interfaces pour fonctionner correctement en plus des informations permettant de localiser le serveur à partir du CLSID. Ces informations doivent être données interface par interface, car lorsque DCOM reçoit une demande d'interface pour la première fois, il doit localiser le serveur in-process capable de la marshaller. Comme, à ce moment, DCOM ne dispose que du GUID de l'interface en question, les informations de localisations du proxy et du stub sont stockées dans une clé dont le nom est exactement le GUID de cette interface. Le nom de cette sous-clé doit être le nom de l'interface décrite.

Toutes ces informations concernant les interfaces sont stockées dans une sous-clé de la clé HKEY_CLASSES_ROOT nommée Interface. Par exemple, pour l'interface IAdder du composant Adder, interface dont le GUID est e3261620-0ded-11d2-86cc-444553540000, l'entrée suivante doit être ajoutée dans la base de registre :

[HKEY_CLASSES_ROOT\Interface\{e3261620-0ded-11d2-86cc-444553540000}]
@="IAdder"

Cette sous-clé doit également contenir d'autres informations concernant l'interface. En particulier, il est nécessaire de définir une sous-clé nommée NumMethods et dont la valeur est le nombre de méthodes de l'interfaces, et une sous-clé nommée ProxyStubClsid32, dont la valeur est le CLSID du composant capable d'effectuer le marshalling de cette interface. Pour l'interface IAdder, on devra donc avoir les entrées suivantes :

[HKEY_CLASSES_ROOT\Interface\{e3261620-0ded-11d2-86cc-444553540000}\NumMethods]
@="5"
[HKEY_CLASSES_ROOT\Interface\
{e3261620-0ded-11d2-86cc-444553540000}\ProxyStubClsid32]
@="{e3261620-0ded-11d2-86cc-444553540000}"

Dans l'exemple donné ci-dessus, le CLSID du proxy et du stub est e3261620-0ded-11d2-86cc-444553540000. Bien entendu, le composant ayant ce CLSID (donc le composant implémentant le proxy et le stub, éventuellement généré par MIDL) doit être enregistré comme un serveur in-process dans la base de registres. Les entrées suivantes doivent donc être ajoutées :

[HKEY_CLASSES_ROOT\CLSID\{e3261620-0ded-11d2-86cc-444553540000}]
@="IAdder Standard Proxy/Stub Factory"
[HKEY_CLASSES_ROOT\CLSID\{e3261620-0ded-11d2-86cc-444553540000}\InprocServer32]
@="c:\\winnt\\system32\\AdderPrx.dll"

52. Comment les interblocages sont-ils gérés ?

DCOM dispose d'un mécanisme permettant de contrôler les interblocages pour les composants qui utilisent le modèle d'appartement monothreadé. Ce mécanisme prend place au sein des proxy et des stubs. Ceci implique qu'il n'est pas disponible pour les composants qui utilisent le modèle d'appartement multithreadé.

Avant de décrire ce mécanisme, il faut décrire la manière dont les appels de méthodes des composants se fait. Grosso modo, il existe trois types d'appels. Les appels les plus courants sont les appels synchrones, ainsi nommés parce que le client attend la réponse du serveur avant de poursuivre son exécution. Le deuxième type d'appel concerne les appels de méthodes pour les objets qui sont en charge d'un objet de l'interface graphique. Ces objets doivent impérativement exécuter complètement la requête qui leur est faite avant de rendre la main, afin que l'interface graphique se comporte correctement. Ceci implique qu'il est impossible de faire un appel synchrone (qui risquerait d'entrer dans une boucle de messages) pendant le traitement de ces appels. Ce type d'appel est nommé « appels synchronisés avec les entrées ». Enfin, le troisième type d'appel comprend tous les appels asynchrones. Ce sont essentiellement les appels de notification. Il est interdit de réaliser un appel synchrone pendant le traitement d'un appel asynchrone (ceci peut conduire à un interblocage dans certains cas).

Ce sont les appels synchrones qui posent le plus de problèmes, puisque lorsque le client est bloqué par un appel synchrone, il peut lui-même avoir à traiter des requêtes de la part de ses propres clients. Ceci peut conduire à des problèmes de réentrances, et au pire à un interblocage s'il n'est pas capable de traiter ces requêtes immédiatement et si un cycle apparaît dans l'arbre des dépendances des requêtes (il suffit pour cela que l'un de ses clients soit utilisés par le serveur qu'il appelle lui-même).

Les deux principaux problèmes de DCOM ici sont donc :

  • comment permettre les requêtes vers un objet bloqué par un appel synchrone (et, en particulier, comment continuer à gérer les messages destinés aux fenêtres du client, afin de ne pas bloquer son interface graphique) ?

  • comment détecter les interblocages lorsque ceux-ci apparaissent, et comment les éliminer ?

Le premier problème est résolu de la manière suivante. Lorsqu'un proxy reçoit une demande d'appel synchrone pour un composant situé dans un autre appartement, celui-ci entre dans une boucle des message modales et passe la requête à un autre thread. Pendant que le thread de l'appartement récupère les messages et gère les requêtes entrantes, le deuxième thread effectue la communication RPC avec le stub du serveur. Seul ce thread est bloqué en attente de la réponse du serveur.

Le deuxième problème est réglé de cette manière : lorsqu'un appel a lieu, DCOM affecte un identificateur de thread (qui lui est propre et n'a rien avoir avec les identificateurs du système sous-jacent). Cet identificateur est communiqué dans toute la chaîne des appels, d'appartements en appartements. Il permet ainsi d'identifier un thread d'exécution entre plusieurs appartements (et éventuellement entre plusieurs processus si des serveurs sont out-of-process). Lorsqu'un proxy/stub reçoit une demande d'appel dans la boucle des messages de son appartement, il peut déterminer qui est à l'origine de cet appel. Ceci lui permet de déterminer s'il est en situation d'interblocage (dans ce cas, c'est lui-même qui est à l'origine de l'appel).

Note: On constate ici que la gestion des interblocages et celle des appels synchrones ne fonctionnent ni pour les serveurs dont le modèle d'appartement est multithreadé, ni pour les appels qui ont lieu à l'intérieur d'un même appartement.

Dans Windows NT4, le mécanisme des proxies a été simplifié lorsque les deux appartements sont sur une même machine. Les communications ont lieu par l'intermédiaire de messages postés au lieu de passer par le mécanisme bloquant des RPC. Ceci permet de supprimer beaucoup de changements de contextes en passant d'un thread à un autre, parce qu'il n'y a plus qu'un thread dans le proxy (celui de l'appartement de du client). Cette simplification ne peut pas être faite au travers d'un réseau, puisque les handles de fenêtres ne sont alors plus uniques et que les plate-formes utilisées peuvent être hétéroclites.

L'identificateur de thread utilisé par DCOM lui permet d'identifier les tâches en cours dans le système. DCOM sait donc en permanence au nom de quel thread il exécute une requête, même si l'exécution a passé le contrôle à un autre thread ou un autre processus. Outre la détection des interblocages, cette technique permet à DCOM d'assurer que toutes les requêtes effectuées au nom d'un thread seront exécutées par les mêmes threads dans les différents appartements.

La gestion des interblocages détectés se fait grâce à un processus nommé le filtrage des messages (en fait, ce processus permet de faire beaucoup plus que gérer les interblocages). Lorsque le proxy est dans sa boucle des messages interne, il analyse toutes les demandes d'exécution de méthodes qu'il reçoit. Le résultat de cette analyse peut être communiquée à tout serveur, par l'intermédiaire de l'interface IMessageFilter. De même, lorsqu'un client est bloqué dans un appel synchrone, il peut connaître l'état de l'appel grâce à cette interface.

Pour pouvoir recevoir ces informations, il est nécessaire d'enregistrer un petit objet implémentant l'interface IMessageFilter au niveau de DCOM. Ceci est réalisé à l'aide de la fonction CoRegisterMessageFilter. Cette fonction est déclarée comme suit dans le fichier d'en-tête objbase.h :

HRESULT STDAPICALLTYPE CoRegisterMessageFilter ( LPMESSAGEFILTER lpFilter , LPMESSAGEFILTER **lppPrevious );

Cette fonction prend deux paramètres. Le premier est un pointeur sur l'interface IMessageFilter de l'objet filtre, et le deuxième est le pointeur sur l'interface IMessageFilter du précédent filtre. Il est possible de supprimer tous les filtres et d'utiliser le filtre par défaut en appelant cette fonction avec les deux paramètres fixés à la valeur nulle.

L'interface IMessageFilter est déclarée comme suit dans le fichier d'en-tête objidl.h :

interface IMessageFilter : IUnknown
{
DWORD HandleIncomingCall(
DWORD dwCallType, HTASK hThreadId,
DWORD dwTickCount, LPINTERFACEINFO lpInfo);
DWORD RetryRejectedCall(
HTASK hThreadId, DWORD dwTickCount,
DWORD dwRejectType);
DWORD MessagePending(
HTASK hThreadId, DWORD dwTickCount,
DWORD dwPendingType);
};

La méthode HandleIncomingCall de cette interface est appelée par DCOM lorsqu'une requête sur un des objets du serveur est effectuée. Le premier paramètre renseigne sur le contexte dans lequel cette requête arrive. Les constantes suivantes définissent les valeurs possibles :

typedef enum tagCALLTYPE
{
CALLTYPE_TOPLEVEL=1,
CALLTYPE_NESTED=2,
CALLTYPE_ASYNC=3,
CALLTYPE_TOPLEVEL_CALLPENDING=4,
CALLTYPE_ASYNC_CALLPENDING=5
} CALLTYPE;

La constante CALLTYPE_TOPLEVEL correspond à une requête normale, qui est effectuée par un client et pour laquelle aucun interblocage n'a eu lieu. Les requêtes de ce type devraient toujours être acceptées. La constante CALLTYPE_NESTED indique que la requête en cours a été générée suite à un appel du serveur, dans le même thread d'exécution. Elle permet donc d'identifier un interblocage. Normalement, le serveur devrait être capable de gérer ce cas et la requête ne devrait pas être refusée. La constante CALLTYPE_ASYNC indique que la méthode appelée est asynchrone. Il est impossible dans ce cas de refuser la requête entrante. La constante CALLTYPE_TOPLEVEL_CALLPENDING identifie que la requête provient d'un autre thread, alors que l'objet auquel elle s'adresse est bloqué dans l'attente de la fin d'un appel synchrone. Ce type de requête peut être accepté ou rejeté, si le serveur n'est pas capable de gérer plusieurs requêtes en même temps. Enfin, la constante CALLTYPE_ASYNC_CALLPENDING indique que la méthode appelée est asynchrone, et que l'objet est en attente de la fin d'un appel synchrone. Encore une fois, cette requête doit être satisfaite.

Le deuxième paramètre de HandleIncomingCall est l'identificateur du thread qui effectue la requête. Le troisième paramètre indique le temps qui s'est écoulé entre le moment où le serveur a effectué son appel bloquant et le moment où la requête est arrivée. Ce paramètre n'a pas de signification lorsque la requête est de type CALLTYPE_TOPLEVEL. Enfin, le dernier paramètre est un pointeur sur une structure INTERFACEINFO, qui permet d'identifier l'objet auquel la requête s'adresse, ainsi que l'interface et la méthode concernée.

La fonction HandleIncomingCall peut renvoyer l'une des trois valeurs suivantes, selon l'action à prendre :

  • SERVERCALL_ISHANDLED, si la requête est acceptée. La méthode de l'objet visé sera donc appelée normalement ;

  • SERVERCALL_REJECTED, si la requête ne peut pas et ne sera jamais acceptée. Le client recevra une erreur en retour RPC_E_CALL_REJECTED et ne devra pas faire une nouvelle tentative (elle serait vouée à l'échec) ;

  • SERVERCALL_RETRYLATER, qui indique que le serveur est momentanément occupé, mais que le client peut tenter à nouveau d'effectuer la requête plus tard.

La méthode RetryRejectedCall est appelée pour les applications clientes. Elle permet au client de définir l'action à prendre dans le cas où une requête a été refusée par un serveur. Les paramètres de cette fonction sont l'identificateur du thread dans lequel la requête a été faite, le temps écoulé depuis que la requête a été effectuée et la raison du rejet invoquée par le serveur (c'est la valeur retournée par HandleIncomingCall). Les valeurs qu'il est possible de retourner sont les suivantes :

  • -1 si l'appel doit être abandonné. Dans ce cas, le client recevra le code d'erreur RPC_E_CALL_REJECTED ;

  • une valeur comprise entre 0 et 99 inclus, ce qui signifie que la requête doit être refaite immédiatement ;

  • une valeur supérieure à 100, qui indique le temps en millisecondes avant que la requête de soit à nouveau tentée.

Enfin, la méthode MessagePending signale au clients qui sont en attente d'une réponse d'un serveur dans le cadre d'un appel bloquant qu'un message de l'interface graphique est arrivé. Ce message peut alors soit être traité immédiatement, soit être supprimé de la file d'attente des messages, soit laissé tel quel pour être traité après la fin de l'appel bloquant. Cette fonction prend en paramètre l'identificateur du thread bloqué, le temps écoulé depuis que l'appel bloquant a été réalisé, et l'une des deux constantes suivantes :

  • PENDINGTYPE_TOPLEVEL, qui indique que l'appel se faisait dans un cadre normal, sans interblocages ;

  • PENDINGTYPE_NESTED, qui indique que l'appel bloquant en cours prend part à un interblocage.

Les valeurs de retour possible indiquent quel action prendre :

  • PENDINGMSG_CANCELCALL, qui permet d'annuler l'appel bloquant en cours et de traiter le message suivant de la file des messages. Le code d'erreur RPC_E_CALL_CANCELED sera renvoyé au client ;

  • PENDINGMSG_WAITNOPROCESS, qui indique d'attendre sans traiter le message. Celui-ci n'est cependant pas détruit, il sera traité lorsque l'appel bloquant se terminera ;

  • PENDINGMSG_WAITDEFPROCESS, qui indique de traiter les messages de commutation de tâches et de changement de fenêtre, ainsi que les messages de rafraîchissement de l'affichage. Tous les autres messages sont détruits.

53. Peut-on rendre le système instable avec DCOM ?

Oui.

Il est impossible pour un client de détruire un composant qui est utilisé par quelqu'un d'autre. En effet, lorsqu'il appelle Release pour la dernière fois, les cas suivants peuvent survenir :

  • le serveur est dans le même appartement (il s'agit donc nécessairement d'un serveur in-process). Dans ce cas, la DLL du serveur est détachée du processus du client, mais elle peut rester en mémoire si un autre client l'utilise également. Comme les données des DLL sont privées à chaque application, les comptes de références sur les composants que la DLL implémente sont propres à chaque application. Les applications peuvent donc provoquer une erreur interne, mais en aucun cas rendre le système instable ;

  • le serveur est un exécutable. Dans ce cas, le client est connecté au serveur par l'intermédiaire d'un proxy et d'un stub, qui sont des DLL. On se ramène donc au cas précédent.

En fait, la seule chose qui peut se passer, c'est qu'un client ne se déconnecte pas correctement. Dans ce cas, le serveur va rester en mémoire un certain temps (de 5 à 8 minutes). Le stub va ensuite s'apercevoir que le client a disparu, et il va automatiquement libérer la référence sur l'objet qu'il représente et se détruire. Il n'est donc pas possible de générer des memory leak en créant des objets out-of process et en ne s'en déconnectant pas.

En revanche, il est possible pour un composant de consommer des ressources systèmes abusivement est d'enregistrer des objets actifs dans la table des objets actifs (la « ROT », abréviation de la « Running Object Table ») et de détruire ces composants sans les désenregistrer. Le système ne contrôle absolument pas que ces objets sont détruits, et ils resteront indéfiniment enregistrés car plus personne n'a de référence sur eux. Il est également possible de rendre le système instable en réalisant des interblocages avec les composants systèmes, dont fait partie le gestionnaire des tâches. Dans ce cas, le système ne sera pas bloqué, mais elle ne sera plus utilisable par l'utilisateur.

Mais le point de faiblesse majeur de DCOM est sans aucun doute la base de registres. Il va de soi que la destruction d'une clé ou l'émulation abusive d'un serveur peut provoquer des erreurs très difficiles, voire impossible, à résoudre. Ce n'est de toutes façons pas une nouveauté que la base de registres de Windows est son principal talon d'Achille…

54. Comment DCOM distribue-t-il les composants ?

Lorsqu'un client désire obtenir une interface sur un objet, il appelle CoGetClassObject (soit directement, soit indirectement par l'intermédiaire de CoCreateInstance). Cette fonction demande au Service Control Manager de Windows (« SCM » en abrégé), de localiser le composant soit sur la machine où s'exécute le client, soit sur un serveur. Le SCM utilise pour cela les options passées en paramètres à CoGetClassObject et les options par défaut de la base de registre. Par défaut, les serveurs in-process sont chargés en premier, puis viennent les serveurs out-of-process en local, puis, si le composant est distribué, la requête est passée au SCM de la machine qui doit exécuter le composant demandé. Sur cette machine, un serveur in-process peut être exécuté au sein d'un processus réceptacle créé pour l'occasion, ou un serveur exécutable peut être chargé.

Quoi qu'il en soit, dès que le serveur est chargé, deux cas sont possibles. Dans le cas des serveurs in-process, la fonction DllGetClassObject est appelée par CoGetClassObject directement et le pointeur sur la fabrique de classe est retourné. Dans le cas d'un serveur out-of-process, aucune fonction n'est exportée, et c'est au serveur d'enregistrer ses fabriques de classes auprès de DCOM en appelant CoRegisterClassObject. C'est à ce niveau que le mécanisme de marshalling intervient pour la première fois, car l'interface IClassFactory doit bien entendu être marshallée. Le SCM est capable de déterminer quelles sont les conditions d'exécution du composant, et demande au serveur de marshaller l'interface IClassFactory pour le composant que le client désire instancier.

Les informations de marshalling sont ensuite transmises au SCM de la machine d'où la requête est partie. L'interface sur la fabrique de classe est ensuite démarshallée, ce qui provoque le chargement du proxy du composant dans l'espace d'adressage du client. Ce proxy récupère les informations de connexion vers le stub du composant et établit une connexion distante. Dans le cas du processus de marshalling standard, les communications sont effectuées en utilisant les services RPC de Windows. Si un composant utilise son propre marshalling, il faut qu'il soit capable de travailler en réseau si l'on veut le distribuer. Cela n'empêche pas de pouvoir utiliser des mécanismes plus performants pour le cas où le client se trouverait sur la même machine, voire dans le même processus. Par exemple, des envois de messages ou des segments de mémoire partagée peuvent être utilisés plus efficacement que les RPC.

Notez que le mécanisme de marshalling est en fait le même pour toutes les interfaces. Le fait que l'interface IClassFactory soit marshallée en premier n'importe absolument pas. En fait, le marshalling a lieu également lorsqu'un pointeur sur une interface est retournée par une méthode d'une interface lors de son passage en valeur de retour de cette méthode. C'est en particulier le cas de la méthode CreateInstance de l'interface IClassFactory. Les pointeurs sur les interfaces des objets retournés à un client distant par la fabrique de classe sont donc systématiquement marshallés.

55. Comment peut-on distribuer un composant ?

Il est très facile de distribuer un composant dont le serveur est implémenté sous la forme d'un exécutable. En effet, aucune opération de programmation n'est nécessaire, il n'y a qu'un peu de configuration à réaliser.

La première étape est d'installer les serveurs des composants distribués sur les ordinateurs distants. Ces ordinateurs doivent être capables d'être serveurs, il faut donc que ce soient des postes fonctionnant sous Windows NT Workstation. À l'heure actuelle, Windows 95 ne peut pas être serveur, il est donc impossible de charger un composant sur un poste Windows 95 à distance. Cependant, Windows 95 peut supporter DCOM si on lui ajoute un patch nommé DCOM95.EXE, disponible chez Microsoft. Dès lors, Windows 95 peut être client DCOM, et un programme fonctionnant sur une machine Windows 95 peut utiliser des composants fonctionnant sur un serveur.

Une fois les composants installés, il suffit de placer des entrées dans la base de registres indiquant sur quelles machines les composants enregistrés devront être exécutés. Ces entrées seront utilisées par défaut si les programmes ne demandent pas explicitement une machine donnée pour l'exécution de leurs composants.

Enfin, la configuration des systèmes (droits d'accès aux composants, contrôle des ressources) constitue la dernière étape. Il s'agit alors d'indiquer quels utilisateurs ont le droit d'utiliser les composants.

56. Quelles sont les entrées à rajouter de la base de registres pour distribuer un composant ?

Grâce à DCOM, les composants peuvent être distribués sans être recompilés. Les applications clientes qui désirent spécifier la machine sur laquelle leurs composant doit être exécuté peuvent le faire en remplissant la structure COSERVERINFO avant d'appeler CoGetClassObject. Le problème des appels de méthodes d'interfaces au travers d'un réseau est automatiquement réglé par le processus de marshalling, et est complètement transparent pour les clients et les serveurs. Pour les serveurs in-process, DCOM fourni automatiquement un processus hôte dans lequel les objets peuvent fonctionner. Ce processus se nomme "dllhost". Il fournit un appartement multithreadé et éventuellement un ou plusieurs appartements monothreadés, pour les serveur in-process apartment threaded.

Les anciennes applications clientes ne sont pas toutes capables de spécifier le serveur sur lequel les composants qu'elles utilisent doivent être exécutés. De plus, toutes les applications récentes ne désirent pas non plus imposer un choix de serveur, et laisser ce problème au stade de la configuration. DCOM fournit donc une méthode pour spécifier le serveur à utiliser par défaut, si le composant ne peut pas être exécuté en local (soit in-process, soit out-of-process). Cette méthode se base sur les options d'applications, qui sont spécifiées composant par composant. Pour indiquer que des options ont été définies, il faut ajouter une valeur nommée AppId dans la clé du CLSID du composant, dont la valeur est un GUID. Par exemple, pour le composant Adder, on aurait :

[HKEY_CLASSES_ROOT\CLSID\{91e132a0-0df1-11d2-86cc-444553540000}]
"AppID"="{91e132a0-0df1-11d2-86cc-444553540000}"

Note: On prendra garde au fait que DCOM utilise ici une valeur au lieu d'une clé. Ce type de situation est très rare, et une erreur à ce niveau peut conduire à des erreurs incompréhensibles.

Le GUID utilisé pour la valeur AppId doit être unique, mais en fait on peut très bien utiliser le CLSID du composant lui-même. Ceci ne pose pas de problème car le GUID est utilisé dans deux cadres bien distincts et aucun conflit ne peut avoir lieu.

Les options pour le composant sont ensuite définies dans une sous-clé dont le nom est le GUID donné en valeur pour AppId. Cette sous-clé est placée dans la sous-clé AppId de HKEY_CLASSES_ROOT, et sa valeur est le nom du composant lui-même. Pour le composant Adder, les entrées de la base de registres sont donc les suivantes :

[HKEY_CLASSES_ROOT\AppID\{91e132a0-0df1-11d2-86cc-444553540000}]
@="Adder Component 1.0"

Dans cette clé, le nom du serveur (ou son adresse) peut être indiquée à l'aide, encore une fois d'une valeur (et non d'une clé), dont le nom est RemoteServerName. Par exemple :

[HKEY_CLASSES_ROOT\AppID\{91e132a0-0df1-11d2-86cc-444553540000}]
"RemoteServerName"="292.53.195.7"

Note: Ce mécanisme de définition des options d'applications dépasse le cadre de DCOM et d'OLE. En fait, tout exécutable peut utiliser des options qui lui sont propres et les stocker dans la sous-clé AppId de HKEY_CLASSES_ROOT. Pour cela, il faut donner au système le moyen de retrouver la sous-clé de la clé AppId à utiliser pour un exécutable donné. Ceci se fait en définissant une sous-clé de la clé AppId dont le nom est le nom de l'exécutable. Cette sous-clé doit contenir une valeur nommée AppId dont la valeur est, encore une fois, un GUID définissant une sous-clé de la clé AppId et contenant des options pour l'application. Par exemple, pour le programme serveur implémentant le composant Adder, on pourrait écrire les options suivantes :

[HKEY_CLASSES_ROOT\AppID\ExeAdder.exe]
"AppID"="{91e132a0-0df1-11d2-86cc-444553540000}"

Le mécanisme utilisé pour décrire les machines à utiliser fonctionne composant par composant. Ceci signifie que les composants d'un même serveur peuvent utiliser des options différentes, en spécifiant des AppID différents. En pratique cependant, il est courant d'utiliser un même AppID pour tous les composants d'un même serveur, en indiquant l'AppID de ce serveur. Si toutefois on ne désire pas procéder ainsi, on devra s'assurer que tous les composants du serveur sont absolument indépendants et ne communiquent que par des mécanismes de DCOM. Par exemple, le partage d'une donnée en mémoire ne permet pas de séparer les composants d'un même serveur sur des machines différentes.

Cette technique fonctionne parfaitement pour les serveurs in-process. Cependant, il faut préciser en plus le nom d'un processus au sein duquel le composant sera chargé. On appelle ce type de processus des surrogate processes. Pour indiquer le nom du processus à utiliser pour ce serveur sur la machine cible, il suffit de donner son chemin dans la valeur DllSurrogate (encore une fois, de type chaîne de caractères) de la clef AppID du composant. Si l'on ne désire pas utiliser de surrogate spécifique à ce composant, il est possible de laisser cette chaîne vide. Dans ce cas, DCOM utilisera le processus hôte standard dllhost.

Si, en revanche, on désire indiquer un processus particulier (par exemple parce que les composants du serveur in-process utilisent des données globales de ce processus), on devra écrire ce processus spécifiquement.

57. Que faut-il faire pour configurer le système pour les composants distribués ?

La configuration du système pour l'emploi des composants distribués consiste à réaliser les opérations suivantes :

  • passer toutes les machines en contrôle des ressources au niveau utilisateur ;

  • donner les droits d'utiliser les composants aux utilisateurs.

La première étape implique d'installer un serveur de domaine (par exemple, un poste avec Windows NT4 Server). Puis, il faut définir un compte pour chaque utilisateur, et configurer tous les postes pour contrôler les ressources au niveau utilisateur avec mot de passe sur le réseau. La liste des utilisateurs est récupérée auprès du serveur de domaine.

La deuxième étape est réalisée en utilisant l'utilitaire de configuration de DCOM : DCOMCNFG.EXE. Cet utilitaire est placé dans le répertoire SYSTEM pour Windows 95, et dans le répertoire SYSTEM32 pour Windows NT. Ce programme permet de spécifier les options de sécurité par défaut pour tous les composants, et, pour chaque composant, de personnaliser ces options.

La boîte de dialogue de DCOMCNFG présente trois onglets. Le premier onglet donne la liste des composants installés et permet de les configurer individuellement à l'aide du bouton « Properties… ». Le deuxième onglet, nommé « Default Properties », permet d'activer DCOM (case à cocher « Enable Distributed COM on this computer » et de préciser les valeurs de sécurité pour les communications. Les valeurs par défaut sont respectivement « Connect » et « Identity ». Enfin, le troisième onglet permet de fixer les droits d'accès par défaut aux composants de l'ordinateur. En cliquant sur le bouton « Edit Default… », on accède à une boîte de dialogue indiquant la liste des utilisateurs autorisés à utiliser les composants de cet ordinateur. Il est possible d'ajouter un utilisateur à l'aide du bouton « Add ». Le bouton « Remove » permet de supprimer les droits aux utilisateurs.

Lorsque l'on édite les propriétés d'un composant (grâce au bouton « Properties… » du premier onglet de DCOMCNFG), une deuxième boîte de dialogue apparaît. Cette boîte de dialogue est, elle aussi, composée de trois onglets. Le premier onglet donne des informations sur la configuration actuelle du composant. Le deuxième onglet, nommé « Location », permet de spécifier sur quel ordinateur le composant doit être exécuté. Lorsque l'on précise le nom de cet ordinateur, on peut utiliser l'un quelconque des noms sur le réseau : adresse UNC, adresse IP, etc… Enfin, le troisième onglet, nommé « Security », permet de préciser si les droits d'accès à utiliser pour ce composant sont les droits d'accès par défaut ou non. Si les droits d'accès sont personnalisés pour ce composant, il est possible de les modifier selon la même procédure que pour les droits d'accès par défaut à l'aide du bouton « Edit… ».

Sur les postes Windows NT, on devra prendre garde aux points suivants :

  • il faut impérativement donner les droits d'accès et d'exécution au système sur le composant, faute de quoi des erreurs étranges (E_OUTOFMEMORY) surviendront et le composant sera inutilisable ;

  • il faut prendre garde au compte dans lequel le composant sera exécuté. En effet, il prendra les droits du compte dans lequel il est utilisé. En pratique, il vaut mieux toujours exécuter le composant dans le compte de l'utilisateur interactif de la machine du poste client du composant. Ce compte porte le nom de compte de l'utilisateur exécutant. En revanche, si l'on cherche à faire en sorte que le composant affiche des fenêtres sur le poste, il est nécessaire d'exécuter le composant dans le contexte du compte de l'utilisateur interactif du poste sur lequel ce composant est lancé.

  • s'il faut avoir les droits suffisants pour exécuter les composants à distance, il faut également donner les droits nécessaires à ces composants pour qu'ils puissent émettre des événements vers leurs clients. Ceci signifie qu'il faut configurer également les postes clients pour que le compte dans lequel les composants répartis s'exécutent soit capable d'accéder aux machines clientes.

Note: Il est fortement déconseillé d'utiliser des événements synchrones en DCOM. En effet, les serveurs risquent de se bloquer dans le code de notification si un client est inaccessible ou est planté, ce qui peut perturber les autres clients. En particulier, les événements Automation ne seront pas utilisables, car on ne peut pas utilsier l'attribut async sur les méthodes de dispinterfaces.

Globalement, DCOM est sûr si l'on donne le droit d'accès au réseau et si les composants sont exécutés dans le compte de l'utilisateur exécutant. La sécurité est ainsi contrôlée complètement au niveau utilisateur, par le serveur de domaine. Cependant, il peut être utile de spécifier un utilisateur fixe pour différents composants, en particulier si l'on veut s'assurer de l'unicité des processus pour les serveurs de type EXE. En effet, DCOM lancera une instance de serveur EXE pour chaque compte utilisateur existant, ce qui peut poser des problèmes au niveau des ressources partagées pour certaines applications. Dans ce cas, on définira un utilisateur dédié à l'application au niveau du contrôleur de domaine, et on fera fonctionner tous les objets répartis au nom de cet utilisateur.

Note: On veillera bien entendu à ce que les postes contenant les composants distribués soient toujours allumés. Ceci semble évident, mais si les utilisateurs ont le droit d'éteindre les machines, cette condition n'est pas toujours vérifiée. Ceci limite donc sérieusement les champs d'application des technologies à objets distribués. Si l'on cherche à faire une application stable, les clients devront pouvoir détecter cette erreur et demander la connexion sur un serveur disposant également de ces composants, et dont on est sûr de l'activité (par exemple, le serveur de domaine).

58. Comment implémenter un surrogate process ?

Les surrogate processes doivent effectuer quelques opérations spécifiques pour pouvoir charger les serveurs in-process en dehors du contexte d'exécution du client. Ces opérations sont récapitulées ci-dessous :

  • le processus doit appeler CoInitializeSecurity pour indiquer les mécanismes de sécurité qu'il désire utiliser. Le processus par défaut fourni par DCOM utilise les informations placées dans la base de registres pour spécifier la politique de sécurité qu'il applique ;

  • créer un composant gérant l'interface ISurrogate. Cette interface est utilisée par DCOM pour demander au surrogate process de charger et de décharger dynamiquement les DLL à prendre en charge ;

  • enregistrer ce composant au niveau de DCOM à l'aide de la fonction CoRegisterSurrogate 

  • créer une boucle des messages dans laquelle la fonction CoFreeUnusedLibraries est appelée régulièrement. Cette étape est nécessaire pour que les serveurs DLL pris en charge puissent être déchargés lorsque plus personne ne les utilise, et pour que le processus puisse déterminer quand il peut se terminer.

La création des serveurs in-process en soi a lieu dans le traitement de la méthode ISurrogate&colon;&colon;LoadDllServer, que DCOM appelle dès qu'il a besoin de charger un de ces serveurs. Cette méthode doit simplement créer une fabrique de classes capable d'instancier le composant dont DCOM a besoin, et l'enregistrer au niveau du système à l'aide de CoRegisterClassObject. DCOM utilisera alors cette fabrique de classes pour créer les composants du serveur in-process. Bien entendu, cette fabrique n'est pas celle du composant à créer, par conséquent, l'implémentation de la méthode CreateInstance se contentera simplement de localiser la fabrique de classes réelle du composant du serveur in-process et de lui déléguer la requête de DCOM.

Lorsque DCOM n'a plus besoin du surrogate process (c'est à dire lorsqu'un des appels à CoFreeUnusedLibraries a libéré le dernier objet du serveur in-process), il appelle la méthode ISurrogate::FreeSurrogate. Cette méthode se contente de désenregistrer toutes les fabriques de classes en cours et de poster un message permettant de terminer le processus.

Note: La fabrique de classes enregistrée par ISurrogate::LoadDllServer doit bien sûr supporter l'interface IClassFactory, mais elle doit en plus gérer l'interface IMarshall. En effet, DCOM peut parfaitement demander d'autres interfaces que IClassFactory à la fabrique de classe fournie par le surrogate process. Ces interfaces ne pouvant pas être gérées a priori, il faut détourner les requêtes de DCOM vers le serveur réel. Comme DCOM appelle toujours les méthodes de IMarshall en premier dans le processus permettant d'obtenir une interface, ce détournement peut être effectué de manière transparente en implémentant IMarshall. Bien entendu, le code de marshalling devra être transféré vers la fabrique de classe du composant in-process réel. Ainsi, DCOM s'adressera directement à cette fabrique de classe pour la suite des opérations impliquées dans la création de l'objet.

Le surrogate process doit impérativement enregistrer ses fabriques de classes à l'aide de CoRegisterClassObject, car il s'agit bien ici d'un serveur EXE (même si les composants créés sont en réalité ceux du serveur in-process).

59. Que sont les objets connectables ?

Les objets connectables sont des objets qui sont capables de signaler des événements à leurs clients. Ces événements sont signalés par l'intermédiaire de notifications, une notification étant en réalité un appel de méthode d'une interface fournie par le client. Le mécanisme de connexion entre les objets connectables et leur client permettent de réaliser les opérations suivantes :

  • interrogation de l'objet sur les interfaces qu'il utilise pour signaler les événements ;

  • initialisation de la connexion par le client, ce dernier fournissant un pointeur sur l'interface désirée à l'objet connectable ;

  • terminaison de la connexion par le client, ce qui libère l'interface détenue par l'objet connectable.

On constate que l'interface utilisée pour les notifications est spécifiée par l'objet connectable, c'est donc une interface de cet objet. Cependant, l'objet qui gère cette interface et reçoit les notifications est implémenté par le client. Ce type d'interface est appelé interface sortante (« outgoing interfaces » en anglais) pour l'objet connectable (parce que pour l'objet client, ce sont des interfaces entrantes (« incoming interfaces ») qui permettent l'arrivée des notifications). Dans ce document, elles seront appelées interfaces externes, pour rappeler le fait qu'elles ne sont pas implémentées par l'objet connectable, mais par son client. Cette terminologie semble plus précise, parce que les interfaces classiques implémentées par les composants sont également des interfaces entrantes et n'ont rien à voir avec les objets connectables.

Les objets connectables utilisent l'architecture suivante pour leurs communications.

Les objets connectables implémentent l'interface IConnectionPointContainer qui, comme son nom l'indique, signifie que l'objet connectable contient des points de connexion. Un point de connexion est un petit objet contenu dans l'objet connectable qui est capable de maintenir la connexion avec le client. L'interface IConnectionPointContainer permet simplement d'énumérer les points de connexion que contient l'objet connectable et de trouver un point de connexion particulier. Ces deux fonctions permettent au client de déterminer l'ensemble des interfaces externe gérées par l'objet connectable, ainsi que d'obtenir un pointeur sur le point de connexion qui gère une interface externe donnée. L'interface IConnectionPointContainer est définie de la manière suivante dans le fichier d'en-tête ocidl.h :

interface IConnectionPointContainer : IUnknown
{
HRESULT EnumConnectionPoints(IEnumConnectionPoints **pEnum);
HRESULT FindConnectionPoint(REFIID iid,
IConnectionPoint **pConnection);
};

La première méthode renvoie un pointeur sur un énumérateur de points de connexion, et la deuxième méthode renvoie un pointeur sur un point de connexion identifié par l'interface qu'il doit gérer.

Les points de connexion sont capables de maintenir un nombre arbitraire de connexions sur une interface externe donnée. Cependant, chaque interface externe doit avoir son propre point de connexion. Les points de connexion implémentent l'interface IConnectionPoint. Cette interface est définie de la manière suivante dans le fichier d'en-tête ocidl.h :

interface IConnectionPoint : IUnknown
{
HRESULT GetConnectionInterface(IID *piid);
HRESULT GetConnectionPointContainer(
IConnectionPointContainer **ppContainer);
HRESULT Advise(IUnknown *pExtern, DWORD *pKey);
HRESULT Unadvise(DWORD dKey);
HRESULT EnumConnections(IEnumConnections **pEnum);
};

Les méthodes de cette interface permettent respectivement d'obtenir l'IID de l'interface externe gérée par ce point de connexion, d'obtenir le pointeur sur l'objet connectable implémentant l'interface IConnectionPointContainer afin de faire un chaînage arrière, d'établir une connexion en passant un pointeur sur l'interface externe désirée en échange d'une clé identifiant la connexion, de détruire cette connexion à l'aide de la clé obtenue, et d'énumérer toutes les connexions en cours gérées par ce point de connexion.

On constate donc ici qu'un point de connexion est capable de gérer plusieurs connexions. Chacune de ces connexions est identifiée par la clé que le point de connexion a donnée lors de l'établissement de cette connexion. Chaque connexion utilise donc un pointeur sur une interface externe fournie par un client.

Les points de connexions sont des objets indépendants les uns des autres. En effet, si un objet gère plus d'une interface externe, il ne peut pas implémenter lui-même l'interface IConnectionPoint, parce que cette interface ne peut gérer qu'une seule interface externe. Cependant, les points de connexions sont fortement liés à leurs conteneurs, en ce sens que la durée de vie du conteneur est comprise dans celle de ses points de connexion. Ceci signifie que tant qu'un point de connexion existe, l'objet connectable doit exister. En pratique, ceci revient à faire en sorte que les points de connexions appellent la méthode AddRef de leur conteneur à chaque établissement d'une nouvelle connexion, et la méthode Release lors de la déconnexion.

Note: Cette architecture est relativement symétrique. En effet, les interfaces externes sont, du point de vue logique, implémentées du côté client par un objet destinataire des notifications en provenance des objets connectables. Ces objets destinataires sont a priori distincts de l'objet client, mais ils peuvent sans problème être l'objet client lui-même (alors que les points de connexions ne peuvent pas être l'objet connectable). Dans tous les cas, les objets destinataires des notifications et les clients qui les gèrent sont très liés, car le client effectuera souvent une action en réponse à une notification. Les objets destinataires sont libres de demander un nombre arbitraire de connexions à plusieurs objets connectables gérant les mêmes interfaces externes. Comme on le voit, ce mécanisme permet de réaliser des graphes de connexions aussi complexes que l'on veut.

Les points de connexion doivent appeler eux-mêmes la méthode AddRef sur l'interface externe qu'ils reçoivent. En effet, ils obtiennent un pointeur sur une interface en utilisant un autre mécanisme que le mécanisme standard de DCOM, à savoir l'une des méthodes CoCreateInstance, IClassFactory&colon;&colon;CreateInstance ou QueryInterface.

Le marshalling standard fonctionne parfaitement pour les objets connectables. Si l'on implémente son propre marshalling, il faudra bien entendu gérer les points de connexions soi-même également.

Le mécanisme des objets connectables peut paraître compliqué. En fait, il l'est. Cependant, ce mécanisme assure l'indépendance entre les objets connectables et leurs points de connexion, ce qui est indispensable pour la pérennité de ces objets. À vrai dire, ce mécanisme n'a pas toujours été appliqué, même par OLE lui-même. C'est ainsi que les interfaces IAdviseSink ne l'utilise pas. Cette interface a en effet été définie avant que le mécanisme des objets connectables ne soit introduit par Microsoft, lorsque ce dernier a commencé à implémenter les contrôles OLE qui utilisent beaucoup les connexions. Par ailleurs, d'autres interfaces (IOleClientSite, IOleInplaceSite, IPropertyPageSite et IOleControlSite) n'utilisent pas non plus le mécanisme des objets connectables. La raison cette fois est différente : les objets gérant ces interfaces doivent obligatoirement se connecter avec leurs clients ou leurs serveurs. Cela fait partie du contrat, et les interfaces site ne permettent pas vraiment de réaliser des notifications. Elles permettent de réaliser une intégration, ce qui va au-delà du concept des objets connectables.

60. Qu'est-ce que OLE Automation ?

61. Qu'est-ce qu'une dispinterface ?

62. Qu'est-ce qu'un DISPID ?

63. À quoi sert l'interface IDispatch ?

Automation est la technique qui permet de « scripter » des composants. Scripter signifie ici que ces composants peuvent être pilotés à partir d'un script, écrit dans un langage de script généralement interprété. On dit donc souvent qu'un composant automation est aussi un composant programmable, puisqu'il peut être manipulé de l'extérieur, en dehors d'un programme compilé.

En général, un composant automation dispose d'un certain nombre de propriétés, qui peuvent être accédées soit en lecture seule, soit en lecture et en écriture, ainsi que de méthodes. En réalité, les accès aux propriétés sont implémentées comme des appels de méthodes, à raison d'une méthode pour la lecture et d'une méthode pour l'écriture. Cependant, ce mécanisme est transparent pour le script en cours d'exécution, seul l'interpréteur de script fera le travail nécessaire pour effectuer l'appel correctement.

La difficulté technique dans Automation est tout simplement que l'interpréteur de script qui sera utilisé ne connaît pas, a priori, toutes les interfaces de tous les composants. Par conséquent, il faut définir un mécanisme qui permette à cet interpréteur de construire facilement des appels sur les méthodes des composants à partir de ces informations, ou d'accéder aux propriétés de ces composants. Cette technique s'appelle le « late binding », parce que les liens entre le composant et le programme qui l'utiliser (à savoir, l'interpréteur de script), ne sont réalisés qu'à l'exécution, pas à la compilation. Automation est donc un mécanisme purement dynamique.

Cette difficulté est résolue simplement en donnant un nom à chaque méthode du composant, et en réalisant les appels non pas directement, mais en indiquant le nom de cette méthode ainsi que la liste des valeurs de ses paramètres. Tout ceci est pris en charge au travers de l'interface IDispatch, qui est réellement l'interface fondamentale d'Automation. En fait, pour qu'il composant soit Automation, il faut et il suffit qu'il implémente l'interface IDispatch. Cette interface est définie comme suit :

interface IDispatch : IUnknown
{
HRESULT GetTypeInfoCount(UINT *pctInfo);
HRESULT GetTypeInfo(UINT itInfo, LCID lcid, ITypeInfo **pptInfo);
HRESULT GetIDsOfNames(REFIID riid, OLECHAR **szNames, UINT cNames,
LCID lcid, DISPID *pDispID);
HRESULT Invoke(DISPID dispID, REFIID iid, LCID lcid, WORD wFlags,
DISPPARAMS *pDispParams, VARIANT *pVarResult,
EXCEPINFO *pExcepInfo, UINT *puiArgErr);
};

Les deux premières méthodes de cette interface permettent de récupérer les informations de typage de l'interface IDispatch. Toutes les interfaces IDispatch ne fournissent pas nécessairement ces informations, cependant, celles qui le fournissent pourront être distribuées sans type library.

Les deux méthodes suivantes sont en revanche indispensables. GetIDsOfNames permet à l'appelant de récupérer les identificateurs numériques associés aux noms des objets que l'on peut manipuler via une interface IDispatch. Ces identificateurs sont ceux qui doivent être utilisés pour effectuer un appel automation, grâce à la méthode Invoke. Typiquement, un interpréteur de script sait, en analysant le script qu'il exécute, le nom de la méthode à appeler ainsi que la liste des paramètres fournis. L'interpréteur commencera donc par récupérer l'identificateur associé à ce nom à l'aide de la méthode GetIDsOfNames, puis il construira la requête d'exécution et la passera à la méthode Invoke.

L'exécution des appels Automation se fait donc systématiquement par l'intermédiaire de la méthode Invoke. Le premier paramètre qu'elle recevra sera l'identificateur de la méthode ou de la propriété que le client désire manipuler. En général, cet identificateur est celui qui vient d'être renvoyé par la méthode GetIDsOfNames. Le deuxième paramètre est réservé et doit valoir NULL. Le troisième paramètre est l'identificateur de la langue dans laquelle les noms des arguments sont donnés, lorsque ces noms sont spécifiés pour un appel de méthode. Le paramètre wFlags est très important, c'est celui qui indique la nature de l'appel en cours. Ce paramètre peut prendre quatre valeurs différentes, respectivement pour les appels de méthodes, la lecture d'une propriété, l'affectation d'une valeur à une propriété, et l'affectation d'un objet par référence à une propriété. Le paramètre suivant est un pointeur sur une structure de type DISPPARAMS, structure qui est définie comme suit :

typedef struct tagDISPPARAMS
{
VARIANTARG FAR* rgvarg;
DISPID FAR* rgdispidNamedArgs;
unsigned int cArgs;
unsigned int cNamedArgs;
} DISPPARAMS;

Cette structure contient un tableau de valeurs pour chacun des arguments. Le deuxième champs contient un tableau de DISPID, à raison d'un DISPID pour chaque argument nommé. Automation permet en effet d'effectuer des appels en nommant chacun des arguments, ce qui permet de spécifier les valeurs des paramètres d'une méthode dans un ordre arbitraire. Si l'appel se fait sans nommer les arguments, ceux-ci devront impérativement être passés dans l'ordre attendu par la méthode. Les deux derniers champs de cette structure contiennent respectivement le nombre d'arguments total (nommés ou non), et le nombre d'arguments nommés.

Les deux paramètres suivants pointent sur la variable devant recevoir la valeur de retour de l'appel, ainsi qu'un pointeur sur une structure EXCEPINFO pour récupérer les informations sur les éventuelles exceptions qui peuvent apparaître pendant l'exécution de la requête. Enfin, le dernier paramètre n'est utilisé que lorsqu'une erreur est détectée par l'objet automation sur les paramètres qui ont été fournis. Il contient alors l'indice du paramètre fautif dans le tableau de la structure DISPPARAMS.

Comme vous pouvez le constater, la méthode Invoke permet réellement de construire dynamiquement un appel de méthode ou d'accéder à une propriété d'un objet dynamiquement. Bien entendu, vous pouvez consulter la documentation de Windows pour plus de renseignements sur ces paramètres.

L'interface IDispatch permet de gérer virtuellement n'importe quelle méthode Automation et n'importe quelle propriété. Au niveau binaire, elle est parfaitement identifiée par son IID, mais en fait, chaque composant offre sa propre interface Automation au travers de cette interface universelle. Pour distinguer les fonctionnalités exposées par un composant via son interface Automation de l'interface IDispatch, on qualifie l'ensemble de ces fonctionnalités de « dispinterface ». Une dispinterface n'est donc rien d'autre qu'une implémentation particulière de l'interface IDispatch.

La définition d'une dispinterface se fait dans le fichier IDL, à l'aide du mot-clé dispinterface. La syntaxe de ce mot-clé est quelque peu différente de celle du mot-clé interface. En particulier, il faut définir deux zones dans la définition de la dispinterface. La première zone est introduite par le mot-clé properties suivi de deux points (':'), et permet de définir toutes les propriétés de la dispinterface. La deuxième zone est introduite de la même manière, par le mot-clé methods. Elle permet bien entendu de définir les méthodes de la dispinterface. Il faut bien comprendre que la compilation du fichier IDL par MIDL ne génère absolument pas de code pour les dispinterface, car en réalité elles sont toutes simplement des interfaces IDispatch. En revanche, les informations qui permettent de décrire cette dispinterface sont stockées dans la type library du composant. En fait, le mot-clé dispinterface n'est pas un mot-clé du langage IDL, mais plutôt un mot-clé du langage ODL de description des type libraries.

Il va de soi que l'implémentation de l'interface IDispatch n'est pas de tout repos. En effet, il faut interpréter le DISPID de la requête pour déterminer quelle méthode doit être exécutée, et interpréter les paramètres dans la structure DISPPARAMS en fonction de ce DISPID. Heureusement, il existe des techniques permettant de réaliser des dispinterfaces quasiment automatiquement. Ces techniques se basent en particulier sur les informations stockées dans les type libraries. En particulier, OLE fournit des services de haut niveau pour gérer les dispinterfaces. Les type librairies sont donc un complément indispensable à l'interface IDispatch.

Note: En général, les environnements de développement comme Visual Basic lisent également les informations sur les interfaces des composants dans les types librairies, ce qui leur permet de proposer la liste des méthodes appelables et de vérifier la validité d'un script sans l'exécuter. Ces informations peuvent même être utilisées pour compiler les scripts, ce qui revient à construire les tables de fonctions virtuelles et appeler ainsi les méthodes sur des interfaces des composants.

64. Qu'est-ce qu'un VARIANT ?

65. Qu'est-ce qu'un BSTR ?

66. Qu'est-ce qu'un SAFEARRAY ?

Comme vous avez pu le constater, tous les paramètres sont fournis dans un tableau, dont les éléments sont de type VARIANTARG. De même, la valeur renvoyée par la méthode Invoke est de type VARIANT. Que sont ces types ?

A priori, les paramètres d'une méthode sont de types arbitraires, et souvent de types différent. De même, on ne peut pas savoir a priori que est la nature du résultat renvoyée par une propriété ou par une méthode. Par conséquent, il faut utiliser des types génériques, capables de stocker des valeurs de n'importe quel type de variable, aussi bien pour le passage des paramètres que pour le passage de la valeur de retour. C'est le rôle du type VARIANT (VARIANTARG est un synonyme de VARIANT).

Le type VARIANT n'est en fait rien d'autre qu'une structure contenant une union d'autres types et un discriminant permettant de savoir comment interpréter les données stockées dans cette union. Le discriminant peut prendre un certain nombre de valeurs, et à chaque valeur correspond un des membres de l'union de la structure VARIANT. Le type de ce discriminant est VARTYPE. Les VARIANT sont capables de stocker les valeurs de tous les principaux types de données. Ils peuvent également contenir des pointeurs sur des interfaces IUnknown et IDispatch, ce qui permet de passer des objets en paramètres dans un appel Automation. Le type VARIANT est donc le type fondamental d'OLE Automation.

Si vous regardez les types qu'il est possible de stocker dans un VARIANT, vous constaterez qu'il permet de stocker des données de type BSTR et de type SAFEARRAY. Ces deux types de données sont particulièrement utilisés dans le cadre d'OLE Automation. Ils permettent respectivement de stocker des chaînes de caractères et des tableaux disposant de mécanismes de contrôle de débortement.

Les BSTR sont en fait des chaînes de caractères Unicode 16 bits (chaque caractère est codé sur 16 bits), terminées par le caractère Unicode nul, et préfixées par un entier 32 bits stockant la taille totale de la chaîne. Ces chaînes peuvent donc être utilisées aussi bien par le langage C que par les langages comme le Pascal ou Visual Basic. Les BSTR ne doivent pas être construits manuellement, en fait, ils doivent être créés et détruits à l'aide des fonctions systèmes SysAllocString et SysFreeString.

De même, les SAFEARRAY doivent être créés et détruits par l'intermédiaire des fonctions systèmes SafeArrayCreate et SafeArrayDestroy. De plus, ils ne doivent être manipulés que par l'intermédiaire des fonctions système dont le nom commence par « SafeArray ». Vous pouvez consulter la documentation de Windows pour plus de détails à leur sujet.

67. Qu'est-ce qu'une interface duale ?

L'inconvénient majeur des dispinterfaces est qu'elles sont très peu performantes. En effet, les appels à Invoke exigent beaucoup de travail du côté client, et les paramètres doivent être interprétés du côté serveur. L'écart de performances par rapport à un appel direct de méthode d'une interface normal est énorme. Cependant, ces interfaces sont nécessaires si l'on veut accéder aux composants à partir d'un langage de script.

L'idéal est de prendre le meilleur des deux mondes, et de faire des interfaces que l'on peut utiliser soit comme des dispinterfaces, soit comme des interfaces classiques (on dit aussi des interface « custom »). Ainsi, les clients qui savent utiliser l'interface custom bénéficieront des performances maximales, et les interpréteurs de scripts pourront toujours utiliser l'interface IDispatch.

De telles interfaces sont des interfaces dites « duales ». Leur définition ne pose pas de réels problèmes, cependant, elles doivent se plier à quelques règles de base :

  • premièrement, elles doivent hériter de l'interface IDispatch. C'est ce qui permet de les utiliser polymorphiquement comme des dispinterfaces ;

  • deuxièmement, elles ne doivent utiliser que des types compatibles avec OLE Automation. Ceci revient à dire que les méthodes de ces interfaces ne peuvent utiliser que les types admis par le type VARIANT ;

  • troisièmement, elles doivent être déclarées avec l'attribut dual pour indiquer à MIDL qu'il s'agit d'interface duales.

Par ailleurs, si l'on veut que les informations stockées dans la type library soient correctes pour cette interface, il faut que les méthodes de ces interfaces soient qualifiées d'attributs complémentaires. Ces attributs comprennent l'attribut dispid, qui indiquer le DISPID de la méthode ou de la propriété automation, propput, propputref ou propget pour indiquer si cette méthode permet d'accéder à une propriété et ce qu'elle permet de faire. Les méthodes qui ne sont pas qualifiées de l'un de ces trois attributs sont considérées comme des méthodes de la dispinterface.

On prendra toutefois bien garde à ne pas définir des dispinterface en tant qu'interface duale, car l'accès direct aux méthodes de l'interface duale provoquera un plantage immédiat du client. En effet, les dispinterfaces ne gèrent en réalité que l'interface IDispatch.

68. Quelles sont les limitations des dispinterfaces et des interfaces duales ?

69. Peut-on créer un composant disposant de plusieurs dispinterfaces ?

70. Quel type d'interface dois-je utiliser pour obtenir le maximum de performances ?

71. Je n'arrive pas à m'abonner à une interface événementielle duale à partir de Visual Basic…

La limitation majeure des dispinterface est qu'un même composant ne peut pas en implémenter plusieurs. En effet, une dispinterface n'est, au niveau binaire, qu'une simple interface IDispatch, et chaque composant ne peut implémenter qu'une seule fois une interface donnée. Par conséquent, il est impossible de disposer d'un composant gérant deux dispinterfaces distinctes, car ces interfaces seraient accédées au travers de la même interface binaire IDispatch. D'autre part, il serait impossible de contrôler facilement que les DISPIDs des propriétés et des méthodes de plusieurs interfaces IDispatch ne sont pas en conflit.

Il est possible, en revanche, de gérer plusieurs interfaces duales pour un composant. L'accès à ces interfaces par l'intermédiaire de la table des fonctions virtuelles ne posera pas de problème, puisque chaque interface est clairement identifiée. Cependant, chacune de ces interfaces se comporte aussi comme une dispinterface, puisqu'elles héritent toutes de l'interface IDispatch. Ceci signifie que ces interfaces ne seront pas toutes accessibles au travers des environnements de scripts tel que Visual Basic, puisqu'ils n'utilisent en général que les dispinterface.

La question est donc de savoir quelle interface sera utilisée par ces environnements. En fait, ce sera soit la première interface déclarée dans le coclass du composant, soit l'interface déclarée avec l'attribut default. Il va de soi que l'on ne peut utiliser l'attribut default qu'une seule fois dans le coclass définissant un composant.

Le deuxième inconvénient des dispinterface est leur lenteur. En effet, tous les appels se font par l'intermédiaire de la méthode Invoke, ce qui nécessite des traitements additionnels tant au niveau du client qu'au niveau du serveur. Si l'on recherche des performances, on s'orientera donc plus particulièrement vers des interfaces custom. Ces interfaces ont cependant le gros défaut de ne pas pouvoir être utilisées facilement par les langages de scripts. Par ailleurs, elles nécessitent de fournir également les DLL contenant les facelets et les stublets avec chaque composant qui utilise ce type d'interface, afin de permettre leur marshalling.

La solution idéale est bien entendue l'utilisation systématique des interfaces duales, afin de permettre aussi bien aux clients C++ d'appeler directement les méthodes de l'interface qu'aux interpréteurs de scripts d'utiliser l'interface IDispatch. En fait, il est fortement recommandé d'utiliser, autant que faire se peut, les interfaces duales lors de la création de composants DCOM. On notera par ailleurs que les interfaces duales sont marshallées automatiquement par OLE, qui fournit un proxy spécial dans la DLL oleaut32.dll. Ceci permet donc d'éviter d'avoir à fournir les DLLs des stublets et des facelets avec ses composants. Ceci n'empêche absolument pas un composant de gérer lui-même son propre marshalling (ceci dit, le code de marshalling aura à gérer l'interface IDispatch, ce qui n'est pas une sinécure). Notez que le proxy fourni en standard par OLE utilise les informations des type librairies pour construire les facelets et les stublets des interfaces duales. Ceci signifie qu'il n'effectue pas le marshalling en passant par la méthode Invoke, et que les appels directs de méthodes des interfaces duales sont donc effectivement plus efficaces que les appels via IDispach.

Malheureusement, même les interfaces duales ont quelques petits inconvénients. Elles ne peuvent pas être utilisées pour définir des interfaces événementielles des composants accédés par l'intermédiaire de Visual Basic. Ceci est une limitation de Visual Basic, qui n'est capable de recevoir des événements qu'au travers de dispinterfaces pures. Les interfaces événementielles des composants DCOM devront donc, en général, être déclarées comme des dispinterfaces.

72. Running Object Table, Monikers, Principales interfaces de OLE (ITypeLib, IClassFactory2, IStream, IPropertyNotifySink, etc…)

TODO: À compléter.

73. Dieu que c'est compliqué !

74. Mais c'est inutilisable !

75. Moi je fais un additionneur en 2 lignes en C…

76. Il n'y a pas plus simple ?

77. Que sont les ATL ?

Comme vous avez pu le constater, DCOM est une technologie extrêmement complexe à utiliser, et encore plus à mettre en oeuvre. En réalité, c'est tout simplement inutilisable tel quel, brut de fonderie. C'est pour cela que Microsoft a dû développer des outils complémentaires pour permettre l'utilisation et la réalisation facile de composants DCOM.

L'un des principaux outils est bien entendu Visual Basic, qui permet non seulement d'utiliser facilement les composants DCOM pourvu qu'ils disposent d'une type library, mais aussi de créer d'autres composants avec une facilité déconcertante au regard de ce qui se passe en interne. C'est réellement dans ce genre d'environnement que l'on se rend compte de la puissance de cette technologie, car la simplicité d'utilisation de Visual Basic provient réellement de la richesse de fonctionnalités fournies par DCOM.

En C++ en revanche, tout n'est pas si facile. La réalisation d'un composant est un véritable cauchemar, même si on est absolument maître de tout. Microsoft a développé une librairie de classes template spécifiquement pour réaliser des composants DCOM : j'ai nommé les « Active Template Library » (« ATL » en abrégé).

Ces librairies de classes soulagent réellement le programmeur, en réalisant la plupart du code de gestion de la durée de vie des composants. Les frais de développement sont donc d'autant plus réduits. Cependant, les ATL sont insuffisantes en soi, il faut leur ajouter un certain nombre de « Wizards », fournis par l'environnement de développement Visual C++, qui permettent de générer le code C++ pour la plupart des opérations des composants. En particulier, les points de connexions ne sont pas totalement pris en charge par les ATL, du code spécifique doit être généré par l'intermédiaire d'un Wizard.

La suite de ce document décrit comment créer et utiliser des composants DCOM avec les ATL et Visual C++ 6.0, ainsi qu'avec Visual Basic 6.0. Vous pourrez constater que Visual Basic exploite pleinement les technologies mises en place par DCOM, et constitue l'environnement de développement idéal pour les petits controles ActiveX.

78. Qu'est-ce qu'une classe template ?

Il est préférable, pour pouvoir utiliser les ATL, de connaître la notion de template du C++. Ce paragraphe a pour but de vous en faire une présentation minimale pour permettre une utilisation sereine des ATL.

En C++, une classe est un type de donnée complexe, qui est défini à la fois par sa structure de données et par les opérations, encore appelées méthodes, qu'on peut lui appliquer. Les instances de ces classes, c'est à dire les variables de ce type, sont ce qu'on appelle des objets. Il est possible d'appliquer à tous les objets d'une même classe l'ensemble des opérations définies dans cette classe.

Il est courant en conception objet d'utiliser la notion de classe pour regrouper toutes les entités qui ont un comportement commun. Il en va de même pour les classes : il est souvent possible de définir un comportement commun pour différentes classes, et donc de les regrouper dans une « classe de classe ». On appelle communément ce type de classes des « métaclasses ». Une classe C++ n'est donc rien d'autre qu'une instance de métaclasse. Le langage C++ ne gère pas en général la notion de métaclasse, et en fait, la manipulation des entités au niveau « méta » relève plus du modèle objet et de l'outil de développement lui-même que du programme en soi. Ceci signifie que les métaclasses sont généralement manipulées au niveau des outils de conception ou des environnements de développement.

Si vous ne comprenez pas ce qu'est une métaclasse, imaginez que vous disposiez de plusieurs classes gérant des listes chaînées. Quelle que soit la nature des objets que ces listes manipulent, elles ont toutes le même comportement et peuvent être définies de manière générique. Toutes ces classes de gestion des listes appartiennent donc à la métaclasse « liste ». S'il est possible de définir le comportement général d'une liste chaînée au niveau méta (donc par exemple au niveau de l'environnement de développement), il est inutile de coder chaque classe de liste pour chaque type de donnée stockable. Il suffit d'instancier la métaclasse liste en spécifiant le type de donnée utilisé. Ce travail d'instanciation peut être réalisé par exemple par un générateur de code, ou par l'environnement.

Les classes template du C++ sont un cas particulier de métaclasses. Elles permettent de définir des classes génériques au niveau méta, qui sont paramétrées par les types de données qu'elles manipulent ou par des valeurs constantes. Les classes template permettent donc de définir des classes qui travaillent sur des types génériques. Leur instanciation est réalisée par le compilateur, lorsque l'on précise les types réels à utiliser en lieu et place des types génériques. Pour reprendre l'exemple des listes chaînées, il est possible de définir une classe liste template, qui stocke des objets de type T indéterminé. Les classes de gestion des listes d'entiers, de réels, de chaînes de caractères ou de n'importe quoi peuvent alors être générées automatiquement par le compilateur à partir de la classe template.

Notez que la définition d'une classe template ne génère aucun code, car la métaclasse ainsi définie se trouve au niveau méta. Ce n'est que lorsque l'on instancie cette métaclasse, donc lorsque l'on précise les types réels à utiliser, que le code est généré pour ces types. D'autre part, cette instanciation se fait à la compilation, et non à l'exécution. Il s'agit donc bien d'une instanciation dans le cadre de l'outil de développement (en l'occurrence, le compilateur). Le C++ est incapable de manipuler des métaclasses ou des classes template dynamiquement (c'est à dire à l'exécution).

Pratiquement, une classe template se déclare de la manière suivante :

template <typename T>
class C
{
// Définition de C en utilisant T comme s'il s'agissait
// d'un type normal.
};

La première ligne « template » permet d'introduire la liste des types génériques que la classe template va utiliser. Dans l'exemple donné ci-dessus, cette liste ne comprend qu'un seul type générique, T. Il est parfaitement faisable d'utiliser plusieurs paramètres template. Il est également possible d'utiliser des valeurs pour les paramètres template à la place de types. Par exemple, on peut définir une classe template paramétrée par la valeur d'un entier.

Note: Il est courant d'utiliser également le mot-clé class à la place du mot-clé typename dans la déclaration des types template.

L'instanciation d'une classe template se fait dès que l'on précise la valeur des paramètres template, que ceux-ci soient des types ou des valeurs. Ceci se fait en donnant la liste de ces valeurs entre crochets. Par exemple, pour définir un type Ci équivalent à l'instance de la classe template C pour les entiers, on écrira :

typedef C<int> Ci;

Cette écriture revient à remplacer partout dans la définition de la classe template C le type générique T par le type réel int. C'est à ce moment que le code est réellement créé par le compilateur.

La librairie des ATL utilise les template C++ pour de multiples usages. En fait, la raison fondamentale de cette utilisation est que les classes ATL ont besoin de manipuler les interfaces que les composants exposent, ainsi que les classes C++ de ces composants eux-même. Les classes ATL sont donc souvent paramétrées par le type de l'interface à implémenter et par la classe C++ du composant qui l'implémente.

79. Comment faire un composant simple avec les ATL ?

Pour créer un composant ATL, il faut choisir le type de projet « ATL COM AppWizard » dans la boîte de dialogue ouverte par le menu « File | New… » de Visual C++. Après avoir donné un nom au projet, par exemple « FAQSAMPLE1 » et validé, Visual C++ demande le type de serveur que l'on désire créer (DLL, exécutable ou service Windows). Parmi les options les plus importantes, on notera celle qui permet d'utiliser les MFC (« Support MFC »).

Le Wizard grise les options lorsque l'on choisit de faire un serveur de type exécutable ou service. Par conséquent, les projets de ce type ne pourront pas, par défaut, utiliser les MFC. Si l'on veut malgré tout utiliser les MFC, il faudra l'indiquer dans les options de projets, et modifier substantiellement le code d'initialisation du serveur pour initialiser les MFC. Vous pourrez obtenir la liste des opérations à accomplir dans l'article intitulé « HOWTO: Add MFC Support to an ATL Project » de la Knowledge Base de Microsoft. Cet article se trouve dans le MSDN et concerne plus Visual C++ que DCOM, par conséquent, les modifications à apporter au projet ne seront pas traitées ici. Nous utiliserons donc systématiquement des serveurs in-process dans la suite de ce document, afin de pouvoir bénéficier des MFC.

La validation des options de projets provoque la création d'un projet vide, contenant :

  • un fichier d'en-tête précompilé « stdafx.h », ainsi que le fichier C++ associé « stdafx.cpp » ;

  • un fichier de ressources .rc et son fichier d'en-tête associé « ressource.h » ;

  • un fichier IDL portant le nom du projet, et dans lequel toutes les définitions d'interfaces et de composant seront placées ;

  • un fichier de définition d'options de linking .DEF ;

  • un fichier C++ principal, qui contient tout le code d'initialisation des ATL et de gestion des fabriques de classes des composants qui seront créés par la suite.

À ce stade, ce serveur ne contient aucun composant. Pour en ajouter un, il faut cliquer sur l'onglet « ClassView » de la fenêtre du Workspace afin de faire apparaître le class browser de Visual C++, puis cliquer sur le bouton droit de la souris sur le projet et choisir le menu « New ATL Object… ». Ce menu fait apparaître une boîte de dialogue permettant de choisir le type d'objet à créer. Nous allons créer un objet ATL simple, et donc sélectionner la rubrique « Objects » puis le type d'objet « Simple Object ». Le bouton « Next » fait apparaître les options de notre objet. Nous pouvons donner son nom, par exemple « Adder ». Automatiquement, Visual C++ propose des noms dérivés de ce nom simple pour les noms de fichiers sources, du composant, de sa première interface, de son type et de son ProgID. Vous pouvez bien entendu modifier ces options.

L'onglet « Attributes » permet de spécifier certaines options pour le futur composant que le Wizard va créer. En particulier, il est possible de spécifier son modèle de threading, si ses interfaces seront custom ou duales et s'il supporte l'agrégation. Les options situées en bas de la boîte de dialogue permettent d'activer le support de l'interface ISupportErrorInfo, qui permet fournir aux programmes comme Visual Basic des informations détaillées sur la nature des erreurs que le composant peut renvoyer, de gérer les points de connexion et d'optimiser le code de marshalling au sein d'un même processus. Laissons les options par défaut inchangées et validons.

Automatiquement, le Wizard définit le composant dans l'IDL, crée le code C++ de ce composant, l'ajoute dans la liste des composants dans le fichier principal du projet, et crée un fichier .rgs qui contiendra toutes les informations que le composant enregistrera dans la base de registre lorsqu'il sera enregistré.

Le composant Adder existe à présent, et il gère l'interface IAdder. Il faut maintenant définir cette interface. Vous pouvez pour cela éditer directement le fichier IDL, ou cliquer avec le bouton droit de la souris et choisir l'une des options « Add Method… » ou « Add Property… ». Nous allons créer une méthode. Dans la boîte de dialogue qui s'ouvre, tapons « Add » dans le champ « Method Name: », et « [in] long x, [in] long y, [out, retval] long *pResult » dans la liste des paramètres. Visual C++ présente au fur et à mesure la déclaration IDL qui sera ajoutée dans la définition de l'interface IAdder. Validons pour créer cette méthode.

Maintenant, il ne reste plus qu'à écrire le code de cette méthode. Pour cela, allons dans le fichier d'implémentation du composant Adder.cpp et écrivons le corps de la méthode dans le squelette que le Wizard a créé pour nous. Votre méthode doit ressembler à ceci :

STDMETHODIMP CAdder&colon;&colon;Add(long x, long y, long *pResult)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
// TODO: Add your implementation code here
if (pResult == NULL) return E_POINTER;
*pResult = x + y;
return S_OK;
}

Il ne reste plus qu'à compiler. Et voilà ! Nous avons fait un composant ATL. Comparé à la quantité de code qu'il faut écrire lorsque l'on veut le faire complètement à la main, il n'y a quasiment plus rien à faire  En fait, comme vous pourrez le constater en parcourant les fichiers écrits par le Wizard, beaucoup de code a été écrit automatiquement. Voyons de quoi il en retourne.

Pour commencer, la définition de l'interface IAdder a été ajoutée dans le fichier IDL. Nous y retrouvons notre méthode. De même, le composant Adder a été défini dans la section library de ce même fichier.

Ensuite, toutes les informations d'enregistrement du composant ont été écrites dans le fichier Adder.rgs. Ces informations seront compilées par le compilateur de ressources de Visual et placées dans la ressource REGISTRY du fichier exécutable contenant le composant.

Troisièmement, une entrée OBJECT_ENTRY(CLSID_Adder, CAdder) a été ajoutée dans la liste des composants du fichier principal du projet (cette liste commence à la macro BEGIN_OBJECT_MAP et se termine à la macro END_OBJECT_MAP). Ceci n'a l'air de rien, mais ces deux lignes définissent un tableau de données que les ATL utilisent pour implémenter automatiquement les class factories pour les composant qui y sont enregistrés. C'est déjà une bonne partie du travail qui est fait là…

Enfin, la classe C++ CAdder est déclarée dans le fichier Adder.h. Cette classe se présente comme suit :

class ATL_NO_VTABLE CAdder :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CAdder, &CLSID_Adder>,
public IDispatchImpl<IAdder, &IID_IAdder, &LIBID_FAQSAMPLE1Lib>
{
public&colon;
CAdder()
{
}
DECLARE_REGISTRY_RESOURCEID(IDR_ADDER)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CAdder)
COM_INTERFACE_ENTRY(IAdder)
COM_INTERFACE_ENTRY(IDispatch)
END_COM_MAP()
// IAdder
public&colon;
STDMETHOD(Add)(/*[in]*/ long x, /*[in]*/ long y,
/*[out, retval]*/ long *pResult);
};

Parmi tout ce code, nous retrouvons la déclaration de notre méthode Add. Mais il se passe, encore une fois, beaucoup de choses. Premièrement, la classe CAdder hérite de la classe template ATL CComObjectRootEx. Celle-ci contient le code de gestion du compte de références sur l'objet.

CAdder hérite également de la classe CComCoClass. Cette classe est fondamentale : elle contient la définition des fonctions permettant de gérer la création d'une nouvelle instance, la gestion des erreurs et la gestion de l'agrégation. Les méthodes de cette classe de base sont référencées dans la liste des composants du fichier principal pour fournir la fabrique de classe pour le composant Adder.

Dernière classe de base, IDispatchImpl. Cette classe ATL prend en charge la gestion de l'interface IDispatch, ainsi que celle de l'interface IAdder, qu'elle reçoit en paramètre. C'est cette classe qui gère donc complètement notre interface…

Il va de soi que les ATL ont besoin de connaître les interfaces que gèrent le composant, afin d'implémenter correctement la méthode QueryInterface. Ce sont exactement ces informations qui leurs sont fournies par les macros COM_INTERFACE_ENTRY de la liste des interfaces du composant (cette liste est comprise entre les macros BEGIN_COM_MAP et END_COM_MAP).

Enfin, pour terminer ce tour d'horizon, la macro DECLARE_REGISTRY_RESOURCEID contient la définition des fonctions qui permettent à ce composant de s'enregistrer et de se désenregistrer dans la base de registre de Windows. Ces fonctions sont référencées dans la liste des composants déclarées dans le fichier principal du projet, et sont appelées par les ATL lorsque l'on demande l'enregistrement du serveur. Les informations d'enregistrement étant stockées dans les ressources du programme, la macro DECLARE_REGISTRY_RESOURCEID prend en paramètre l'identificateur de la ressource contenant le fichier .RGS du composant.

80. Comment utiliser un composant simple avec Visual Basic ?

L'utilisation d'un composant DCOM dans VB est très simple. VB est capable d'utiliser quasiment n'importe quel composant, du moment qu'il dispose d'une type library. Dans ce cas, il se comportera comme un client C++, et ne passera pas par l'interface IDispatch. En revanche, si le composant ne dispose pas de type library, VB utilisera systématiquement l'interface IDispatch, ce qui rendra les performances nettement moins bonnes. D'autre part, il ne pourra pas vérifier les fautes de frappes à la volée, comme il le fait pour les composants dont il peut obtenir une description.

Les serveurs ATL contiennent tous leur propre type library dans leur ressources, ce qui fait qu'il est possible de les utiliser directement dans VB dès qu'ils sont enregistrés dans le système. Cependant, il faut spécifiquement indiquer à VB qu'il doit utiliser cette type library. Ceci se fait en la sélectionnant dans la boîte de dialogue ouverte par le menu « Projet | Références… ».

Pour essayer notre nouveau composant additionneur, créons un projet de type « EXE standard » dans Visual Basic. Ajoutons la référence sur la type library « FAQSAMPLE1 1.0 » comme indiqué ci-dessus. Dessinons ensuite un bouton sur la boîte de dialogue, puis double-cliquons dessus. Ceci ouvre la fenêtre de code pour cette action. Tapons le code suivant :

Private Sub Command1_Click()
Dim A As New Adder
Dim Result As Integer
Result = A.Add(2, 3)
MsgBox Result
End Sub

Lançons ensuite le programme et cliquons sur le bouton. Ô miracle, le résultat de l'addition, calculé par notre composant, s'affiche !

81. Comment faire un composant simple avec Visual Basic ?

Visual Basic est certainement l'environnement le plus adapté pour créer des composants DCOM de petite taille. Nous allons faire le composant de multiplication à présent.

Créons un nouveau projet de type « EXE ActiveX » dans VB. Celui-ci crée automatiquement un nouveau module de classe nommé « Class1 ». Renommons le en « Calculator » et saisissons le code suivant dans la fenêtre de code :

Public Function mul(x As Long, y As Long) As Long
mul = x * y
End Function

C'est tout ce qu'il y a à écrire pour créer notre composant Calculator. Il reste cependant quelques points de détails à régler dans les options de projets pour pouvoir créer un composant réellement utilisable. Ces options se fixent dans la boîte de dialogue des propriétés du projet, qui peut être affichée grâce au menu « Projet | Propriétés de XXX  », où « XXX » est le nom courant du projet. Le premier onglet permet de spécifier le nom du projet dans la zone d'édition « Nom du projet ». Ce nom sera aussi celui de la type library du serveur de notre composant. Dans notre cas, nous avons choisi le nom « FAQSAMPLE2 ».

Nous pouvons à présent compiler l'exécutable avec le menu « Fichier | Créer … ». Ce menu ouvre une boîte de dialogue permettant de choisir l'emplacement et le nom de l'exécutable à créer. Nous avons utilisé FAQSAMPLE2.EXE comme nom d'exécutable. Visual Basic compilera alors le programme et créera notre serveur. Comme pour Visual C++, l'enregistrement est fait automatiquement à chaque compilation.

La suite des opérations se passe dans l'onglet « Composant » de la boîte de dialogue des propriétés du projet. Il faut impérativement y activer l'option « Compatibilité binaire » de la zone « Compatibilité des versions ». Cette option permet de demander à VB de conserver toujours les mêmes CLSID et IID pour les composants et les interfaces du projet. Par défaut, VB modifie ces valeurs à chaque compilation, ce qui fait que les composants ne sont pas utilisables directement au travers de leur CLSID. Ce n'est pas très pratique en C++, où l'on préfère en général utiliser les CLSID plutôt que les ProgID, et les interfaces custom plutôt que les dispinterfaces. Notez que cette option nécessite d'indiquer le nom du fichier binaire dans lequel se trouve la définition des CLSID et des IID à utiliser, et donc d'avoir compilé au moins une fois le serveur.

82. Comment utiliser un composant simple avec les ATL ?

Les ATL ne fournissent pas d'aide particulière pour l'utilisation des composants DCOM. Ceux-ci doivent être créés manuellement avec la fonction CoCreateInstance, et détruit avec la méthode Release. Les méthodes des interfaces des composants peuvent être appelées directement, ce qui n'est pas très difficile, sauf si l'interface est une dispinterface.

Notez qu'heureusement pour nous, Visual Basic génère des interfaces duales pour tous ses composants (sauf pour les interfaces sortantes des points de connexions). Ceci signifie qu'il est très facile d'utiliser les composants, même ceux générés par Visual Basic, en C++. Cependant, il y a un petit problème : il faut disposer du fichier d'en-tête de déclaration des composants à utiliser. Pour les composants Visual Basic (ainsi que la plupart des composants qui ne sont distribués que sous forme binaire), nous ne disposons pas de ce fichier, ni de fichier IDL.

Pour résoudre ce problème épineux, Visual C++ fournit une directive de préprocesseur spéciale afin de récupérer toutes les déclarations C++ à partir d'une type library. Il s'agit de la directive #import, qui prend en paramètre le nom du fichier contenant la type library à importer. En fait, cette directive force la création d'un fichier .TLH (« Type Library Header »), qui sera inclus en lieu et place de la directive #import. Si l'on regarde le contenu de ce fichier, on constatera qu'il contient non seulement la déclaration des interfaces, mais aussi la définition des CLSID et des IID.

Cette directive est très pratique, mais son emploi pose quelques problèmes de portabilité évidents. En effet, elle n'est absolument pas standard. De plus, le code généré par cette directive utilise lui-aussi des extensions non standard au langage C++, en particulier pour résoudre les problèmes de définitions multiples des CLSID et des IID à l'édition de lien, en cas d'import multiples dans différents fichiers du projet. En outre, il faut généralement spécifier les options no_namespace, named_guids et raw_interfaces_only pour avoir du code qui ressemble à celui que MIDL aurait généré à partir d'un fichier IDL. Il faut également tenir compte du fait que ces fichiers peuvent parfois être la source de problèmes assez étranges lorsque l'on utilise le mécanisme des en-têtes précompilés de Visual C++. Mais le défaut majeur de cette technique est qu'elle nécessite que le composant binaire que l'on désire utiliser soit installé sur la machine de compilation, ce qui n'est généralement pas le cas.

C'est pour pour ces raisons qu'on aura intérêt à utiliser une autre technique, plus classique et plus « dans les règles de l'art ». Celle-ci est très simple : il suffit de récupérer les informations de la type library du composant à utiliser et de les inclure dans un fichier IDL. Ainsi, on se ramène au cas « normal », qui est celui où l'on dispose du fichier IDL de chaque composant. Pour cela, on utilisera l'outil « OLE/COM Object Viewer », accessible dans le menu « Tools » de Visual C++. Ce programme permet de visualiser les interfaces des composants du système, aussi bien dynamiques que celles définies dans les type libraries. Ce sont ces dernières qui nous intéressent.

Ouvrons par exemple la type library du composant FAQSAMPLE2.EXE (menu « File | View Typelib… »). Voici le fichier IDL correspondant :

// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: FAQSAMPLE2.exe
[
uuid(E2D64385-4FFD-11D4-B3BC-9FEBDB931851),
version(1.0)
]
library FAQSAMPLE2
{
// TLib :
// TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("StdOle2.tlb");
// Forward declare all types defined in this typelib
interface _Calculator;
[
odl,
uuid(E2D64386-4FFD-11D4-B3BC-9FEBDB931851),
version(1.0),
hidden,
dual,
nonextensible,
oleautomation
]
interface _Calculator : IDispatch {
[id(0x60030000)]
HRESULT mul(
[in, out] long* x,
[in, out] long* y,
[out, retval] long* );
};
[
uuid(E2D64387-4FFD-11D4-B3BC-9FEBDB931851),
version(1.0)
]
coclass Calculator {
[default] interface _Calculator;
};
};

C'est simple : il suffit de récupérer la définition de l'interface _Calculator et du composant Calculator et de les inclure dans le fichier IDL du projet où l'on désire les utiliser. Ce fichier IDL contiendra alors toutes les définitions nécessaires à l'utilisation de ce composant, sans avoir recours à des artifices douteux ni avoir besoin d'installer ce composant.

83. Comment utiliser simplement un composant du même projet avec les ATL ?

L'utilisation de CoCreateInstance peut être assez contraignante lorsque l'on doit utiliser un composant du même projet. En particulier, il peut être intéressant de créer ces composants comme des objets C++ classiques (après tout, un composant ATL est aussi une classe C++), afin de pouvoir les manipuler à l'aide de méthode C++ standard en interne.

C'est exactement ce que permet de faire la classe template ATL CComObject. Cette classe permet de manipuler les composants ATL comme des objets C++, en implémentant les fonctions de IUnknown et en assurant une gestion cohérente de la durée de vie de l'objet C++ par rapport à son compte de références. L'utilisation de CComObject est très simple. Il suffit de lui fournir en paramètre le nom du composant ATL à utiliser comme une classe C++ :

CComObject<CAdder>

On aura intérêt à définir un typedef dans le fichier d'en-tête de la définition de la classe du composant :

typedef CComObject<CAdder> CAdderObj;

Dès lors, il sera possible de créer un additionneur directement à l'aide d'une expression du type :

CAdderObj *pAdder = new CAdderObj;

Notez que la création d'un composant ne demande pas d'interface. Le compte de référence de ce composant vaut donc 0, et le pointeur pAdder ne doit pas être renvoyé à un client en tant que pointeur d'interface. Si l'on désire une interface sur notre additionneur, on devra d'abord appeler la méthode QueryInterface. Notez également que le dernier Release sur l'objet provoquera sa destruction automatiquement. Le pointeur pAdder ne sera donc plus valide. Pour empêcher ce genre de problème, on pourra demander une interface à titre privé. Enfin, n'utilisez par directement l'opérateur delete sur un CComObject. En effet, la destruction des composants doit se faire via la méthode Release (bien entendu, vous pouvez toujours utiliser delete si vous ne faites pas de QueryInterface sur cet objet).

84. Comment réalise-t-on un composant disposant de propriétés avec les ATL ?

85. Comment faire un composant gérant des événements avec les ATL ?

86. Comment ajouter la gestion des interfaces événementielles à un composant existant ?

Réaliser un composant contenant des points de connexions n'est pas plus difficile que de réaliser une composant normal avec les Wizards de Visual C++ et les ATL. Cependant, il est conseillé de prévoir dès la création du composant la gestion des points de connexions, faute de quoi il faut réaliser une partie du travail du Wizard manuellement.

Nous allons profiter de cet exemple pour compliquer un peu les choses, et voir comment on peut ajouter des propriétés à un composant. Soit un composant permettant de faire des soustractions. Ce composant fonctionne de la manière suivante : il dispose de deux propriétés, dans lesquelles les valeurs des deux paramètres à soustraire doivent être placées, d'une méthode Sub, qui permet de demander le calcul, et d'une propriété Result, accessible en lecture seule, qui contiendra le résultat. Ce composant génère un événement Done lorsqu'il a fini son calcul.

Créons avant tout un nouveau projet ATL de type DLL nommé FAQSAMPLE3, comme nous l'avons fait pour le projet FAQSAMPLE1. Ajoutons un nouveau composant Substract à ce projet, à l'aide du menu contextuel « New ATL Object… » du class browser de Visual C++. Choisissons un objet simple, auquel nous donnerons le nom « Substract ». Dans l'onglet « Attributes », cochons la case « Support Connection Points » et créons le composant.

Qu'est-ce que l'option « Support Connection Points » a ajouté au composant ? Si l'on regarde le fichier IDL, nous verrons que le Wizard a créé une dispinterface _ISubstractEvents, et que le composant Substract utilise cette interface comme interface sortante par défaut. Ceci signifie que les outils comme Visual Basic ne pourront utiliser que les événements définis dans cette interface.

Il est parfaitement légal d'utiliser des interfaces duales pour les points de connexion. Cependant, ces points de connexion ne seront pas utilisables dans Visual Basic, parce que celui-ci ne sait gérer que des dispinterfaces sur les points de connexions. Il est donc conseillé de laisser le code généré par le Wizard tel quel. Si toutefois on désire utiliser des interfaces duales, il faut transformer la dispinterface en interface duale. Ceci nécessite de procéder comme suit :

  • déclarer l'interface en tant qu'interface normale plutôt qu'en tant que dispinterface ;

  • faire hériter cette interface de l'interface IDispatch ;

  • supprimer les champs properties et methods de la définition de l'interface ;

  • rajouter les attributs object, dual pour cette interface en plus de son uuid. Le mot-clé dual est nécessaire : sans lui, le code d'enregistrement des ATL n'enregistre pas l'IID de l'interface dans la clé Interfaces, et elle ne peut donc pas être marshallée automatiquement. Notez que c'est aussi ce mot-clé qui empêchera Visual Basic de recevoir les événements de cette interface.

Par ailleurs, s'il existe déjà des points de connexions utilisant cette interface dans le projet, il va falloir modifier le code de certains composants. En pratique, il est plus simple de refaire tous les points de connexions, et de supprimer les références à l'ancienne dispinterface.

Notez que le Wizard définit les interfaces événementielles dans la section library de l'IDL. Il fait ceci afin de forcer une référence sur cette interface, ce qui permet de définir cette interface dans le .h généré par MIDL. Ceci n'est pas nécessaire si l'interface sortante n'est pas implémentée en tant qu'interface normale par un composant du projet.

La deuxième modification apportée par le Wizard se trouve dans la déclaration de la classe du composant. Cette classe dérive à présent de la classe template ATL IConnectionPointContainerImpl, qui permet de gérer la liste des points de connexion pour ce composant. D'autre part, cette liste a été ajoutée dans la classe. Elle est définie par les macros BEGIN_CONNECTION_POINT_MAP et END_CONNECTION_POINT_MAP.

Si on désire ajouter la gestion des points de connexions à un composant existant pour lequel ceci n'avait pas été prévu initialement, il faut refaire ces opérations manuelles. Autrement dit, il faut définir l'interface dans l'IDL et l'ajouter en tant qu'interface « source » dans le coclass du composant, faire dériver le composant de IConnectionPointContainerImpl et lui ajouter la liste des points de connexions.

Nous pouvons maintenant ajouter les propriétés à notre interface ISubstract. Pour cela, il suffit d'utiliser le menu contextuel « Add Property… » sur cette interface dans le class Browser de Visual C++. Nous pouvons ensuite donner un type pour cette propriété, choisissons ici le type long. La propriété sera nommée Operand1. Vous pouvez indiquer les modes d'accès de la propriété dans la section « Function Type » de la boîte de dialogue d'ajout des propriétés. Pour la propriété résultat, nous ne cocherons que la case « Get Function », ce qui permettra de la rendre accessible en lecture seule.

Les propriétés sont implémentées dans Visual C++ comme des fonctions. Il existe une fonction pour la lecture de la propriété, et une fonction pour l'écriture. Comme on peut le constater dans le code de la classe CSubstract, ces deux accesseurs portent le nom de la propriété, préfixé par « get_ » et « put_ » respectivement pour la lecture et l'écriture.

Nous pouvons créer de même les propriétés Operand2, Result et la méthode Sub. Il ne reste plus qu'à écrire le code dans le fichier Substract.cpp :

STDMETHODIMP CSubstract&colon;&colon;get_Operand1(long *pVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
if (pVal == NULL) return E_POINTER;
*pVal = m_lOperand1;
return S_OK;
}
STDMETHODIMP CSubstract&colon;&colon;put_Operand1(long newVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
m_lOperand1 = newVal;
return S_OK;
}
STDMETHODIMP CSubstract&colon;&colon;get_Operand2(long *pVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
if (pVal == NULL) return E_POINTER;
*pVal = m_lOperand2;
return S_OK;
}
STDMETHODIMP CSubstract&colon;&colon;put_Operand2(long newVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
m_lOperand2 = newVal;
return S_OK;
}
STDMETHODIMP CSubstract&colon;&colon;get_Result(long *pVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
if (pVal == NULL) return E_POINTER;
*pVal = m_lResult;
return S_OK;
}
STDMETHODIMP CSubstract&colon;&colon;Sub()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
m_lResult = m_lOperand1 - m_lOperand2;
return S_OK;
}

Notez que ce code utilise les données membres m_lOperand1, m_lOperand2 et m_lResult de la classe CSubstract. Ces données devront donc bien entendu être ajoutée dans la déclaration de cette classe.

Il faut ensuite ajouter notre événement dans l'interface événementielle. Pour cela, il suffit d'ajouter la méthode Done, qui ne prend aucun paramètre, dans cette interface. Il est nécessaire de recompiler le fichier IDL à ce stade.

Note: Si des paramètres doivent être passés à des méthodes d'interfaces événementielles, il est vivement recommandé de les passer par pointeurs. C'est ce que fait Visual Basic par défaut. En fait, c'est une contrainte imposée par un bug au sein des librairies de OLE, qui fait que certains clients ne pourront pas capter les événements correctement si on ne la respecte pas.

Il faut maintenant implémenter le point de connexion pour le composant Substract. Ceci se fait en utilisant le menu « Implement Connection Point… » sur la classe CSubstract dans le class browser de Visual C++. Visual C++ nous donne la liste des points de connexions que cette classe est susceptible de gérer. Le Wizard détermine cette liste sur la base de ce qui est écrit dans le fichier IDL, dans la définition du composant Substract. Si l'on veut implémenter une autre interface événementielle, il faut donc ajouter cette interface en tant qu'interface « source » dans le coclass de Substract.

Le Wizard effectue encore plusieurs modifications lorsqu'il implémente un point de connexion. Premièrement, il crée une classe template CProxy_ISubstractEvents, qui gère le point de connexion pour l'interface _ISubstractEvents. Ensuite, il fait dériver la classe du composant de cette classe, parce que le composant contient ce point de connexion. Finalement, il ajoute une entrée dans la liste des points de connexions de la classe à l'aide de la macro CONNECTION_POINT_ENTRY. Prenez bien garde à ne pas confondre la notion de proxy du marshalling avec les classes CProxyXXX générées par le Wizard. Ce sont deux notions qui n'ont absolument rien à voir.

Notez que la génération de la classe CProxy_ISubstractEvents utilise la type library du projet. C'est pour cette raison qu'il faut impérativement compiler le fichier IDL avant de générer un point de connexion. De plus, toute modification d'une interface événementielle nécessite de regénérer ce point de connexion. Ceci ne modifie absolument rien à votre code.

Il ne reste plus qu'à signaler les événements lorsque c'est nécessaire. Dans notre cas, ceci doit être fait lorsque le calcul du résultat est terminé, dans la méthode Sub de l'interface ISubstract. L'envoi des événements se fait simplement en appelant la méthode Fire_XXX, où « XXX » est le nom de l'événement à générer. Cette méthode est définie dans la classe d'implémentation du point de connexion, et est héritée par la classe du composant. Dans notre cas, l'utilisation de l'événement Done se fait comme suit :

STDMETHODIMP CSubstract&colon;&colon;Sub()
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
m_lResult = m_lOperand1 - m_lOperand2;
Fire_Done();
return S_OK;
}

Voilà, nous avons créé un composant un peu plus complexe avec les ATL…

87. Comment capter les événements d'un composant dans Visual Basic ?

Visual Basic peut récupérer les événements des objets déclarés en portée globale. Pour cela, il suffit de déclarer ces objets avec le mot-clé WithEvents :

Dim WithEvents S as Substract

S est alors déclaré comme une référence sur un composant Substract, et est capable de récupérer les événements de ce type de composant. Ces événements seront reçus dans des fonctions portant le nom de l'événement, préfixé par le nom de l'objet pour lequel cet événement a été généré. Par exemple, pour l'objet S, l'événement Done sera traité par la fonction S_Done.

Notez que Visual Basic n'accepte pas les interfaces duales comme source d'événements. Il faut impérativement des dispinterfaces, faute de quoi il indique une erreur « Mécanisme de bibliothèque d'objets non géré ».

88. Comment faire un composant contenant des propriétés en Visual Basic ?

89. Comment faire un composant gérant des événements en Visual Basic ?

Créer un composant gérant des propriétés est immédiat en Visual Basic. Il suffit de définir des méthodes avec les mots-clés Property Let et Property Get. De même, il est beaucoup plus facile de générer des événements en Visual Basic qu'en C++. Il suffit simplement de les déclarer dans les fichiers de classe, avec la syntaxe suivante :

Public Event XXX()
XXX est le nom de l'événement à gérer. Cet événement pourra ensuite être généré à l'aide du mot-clé RaiseEvent.

Par exemple, si l'on désire créer un composant Division, on écrira le code suivant :

Public Event Done()
Private x As Long
Private y As Long
Private r As Long
Public Property Let Operand1(Value As Long)
x = Value
End Property
Public Property Get Operand1() As Long
Operand1 = x
End Property
Public Property Let Operand2(Value As Long)
y = Value
End Property
Public Property Get Operand2() As Long
Operand1 = y
End Property
Public Property Get Result() As Long
Result = r
End Property
Public Sub Divide()
r = x / y
RaiseEvent Done
End Sub

90. Comment capter les événements automation d'un composant avec les ATL ?

Du fait que Visual Basic génère des dispinterfaces pour les points de connexion de ses composants, la gestion des événements avec le C++ est relativement pénible. En effet, cela nécessite d'implémenter l'interface IDispatch et d'analyser les DISPIDs des événements reçus. Heureusement, les ATL fournissent une classe template IDispEventImpl, qui permet de récupérer les événements automation des dispinterfaces. Cependant, aucun Wizard n'existe pour générer le code nécessaire à la gestion des événements automation, il faut donc l'écrire soi-même.

La première étape pour capter les événements automation est avant tout de créer un composant qui servira de cible pour les événements. En général, le composant intéressé par les événements d'un point de connexion n'est pas lui-même la cible de ces événements. Le fait de distinguer ces deux composants permet de s'assurer qu'il n'y aura pas de cycle dans le compte des références. Supposons par exemple que le composant intéressé par les notifications détienne une interface sur l'objet source, ce qui est souvent le cas. S'il était également l'objet cible de ces notifications, la source détiendrait une référence sur une interface de l'objet cible, et les deux objets se bloqueraient mutuellement en mémoire.

En général, le composant cible s'appelle un « sink », du terme « puits » en anglais, par opposition à l'objet « source » des événements (les événements vont de la source vers le puits). Il nous faut donc simplement créer un composant ATL simple, nommé par exemple DivisionSink si l'on veut recevoir les événements du composant Division de l'exemple FAQSAMPLE4.

Le Wizard va automatiquement créer une interface IDivisionSink pour ce composant. En fait, nous n'avons pas besoin de cette interface, puisque notre composant ne gérera que la dispinterface événementielle de Division, à savoir la dispinterface __Division. Nous devons donc modifier le code généré pour supprimer l'interface IDivisionSink dans le fichier IDL, et indiquer que le composant DivisionSink gère en fait la dispinterface événementielle __Division. La définition de notre composant dans le fichier IDL doit donc ressembler à ceci :

[
uuid(1910BF90-502F-11D4-B3BC-9FEBDB931851),
helpstring("DivisionSink Class")
]
coclass DivisionSink
{
[default] dispinterface __Division;
};

Notez que ceci suppose que nous ayons récupéré les informations de la type library du serveur FAQSAMPLE4.EXE. Ceci ajoute donc une dépendance des projets clients envers les projets serveurs.

Il faut ensuite faire le ménage dans la définition de la classe CDivisionSink et remplacer les références à l'interface IDivisionSink par des références équivalentes à la dispinterface __Division.

La deuxième étape est de faire dériver la classe CDivisionSink de la classe de base IDispEventImpl. Il faut renseigner les paramètres template suivant à cette classe :

  • un identificateur unique dans le projet, qui caractérise la classe CDivisionSink (en fait, cet identificateur est utilisé pour identifier les contrôles, mais ici nous ne cherchons pas à faire un contrôle ATL) ;

  • le nom de la classe qui dérive de IDispEventImpl ;

  • l'adresse sur le GUID de la dispinterface à implémenter ;

  • l'adresse sur le GUID de la type library contenant la définition de cette interface ;

  • le numéro de version majeur de cette type library ;

  • son numéro de version mineur.

Dans notre cas, cela donne :

IDispEventImpl<0, CDivisionSink, &DIID___Division,
&LIBID_FAQSAMPLE4Lib, 1, 0>

Notez bien qu'on référence ici la type library de la source, pas du projet en cours.

La troisième étape est de créer une méthode C++ standard pour chaque événement automation susceptible de se produire. Ces méthodes doivent utiliser la convention d'appel __stdcall, car elles seront appelées en callback par les ATL. Dans notre cas, nous utiliserons la méthode C++ OnDone, déclarée comme suit dans la classe CDivisionSink :

void __stdcall OnDone();

Enfin, la dernière étape est de créer une map d'association entre les DISPIDs des événements automation susceptibles de se produire et les fonctions callback à appeler. Cette map est créée à l'aide des macros BEGIN_SINK_MAP, END_SINK_MAP et SINK_ENTRY_EX. Cette dernière macro prend en paramètre le même numéro qui a été utilisé pour identifier la classe CDivisionSink dans IDispEventImpl, le GUID de la dispinterface à gérer, le DISPID de l'événement automation et l'adresse de la fonction callback à appeler pour chaque occurrence de cet événement.

Pour récapituler, la déclaration complète de notre classe CDivisionSink est la suivante :

/////////////////////////////////////////////////////////////////////
// CDivisionSink
class ATL_NO_VTABLE CDivisionSink :
public CComObjectRootEx<CComSingleThreadModel>,
public CComCoClass<CDivisionSink, &CLSID_DivisionSink>,
public IDispEventImpl<0, CDivisionSink, &DIID___Division,
&LIBID_FAQSAMPLE4Lib, 1, 0>,
public IDispatchImpl<__Division, &DIID___Division,
&LIBID_FAQSAMPLE4Lib>
{
public&colon;
CDivisionSink();
virtual ~CDivisionSink();
void __stdcall OnDone();
DECLARE_REGISTRY_RESOURCEID(IDR_DIVISIONSINK)
DECLARE_PROTECT_FINAL_CONSTRUCT()
BEGIN_COM_MAP(CDivisionSink)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(__Division)
END_COM_MAP()
BEGIN_SINK_MAP(CDivisionSink)
SINK_ENTRY_EX(0, DIID___Division, 0x00000001, &OnDone)
END_SINK_MAP()
// IDivisionSink
public&colon;
};
typedef CComObject<CDivisionSink> CDivisionSinkObj;

La méthode OnDone des objets de cette classe sera appelée à chaque fois qu'un événement Done sera généré par les objet auxquels ils seront connectés. Il reste à savoir comment on peut connecter une instance de CDivisionSink à un objet gérant la dispinterface événementielle __Division. Eh bien, ceci se fait simplement à l'aide de la méthode DispEventAdvise de la classe cible. Par exemple, une séquence d'appel typique serait la suivante :

// Crée un objet Division :
_Division *pDivider;
CoCreateInstance(CLSID_Division, NULL, CLSCTX_SERVER,
IID__Division, (void **) &pDivider);
// Crée un objet cible :
CDivisionSinkObj *pSink = new CDivisionSinkObj;
// Demande l'interface IUnknown de l'objet Division :
IUnknown *pDivUnk;
pDivider->QueryInterface(IID_IUnknown, (void **) &pDivUnk);
// Abonne l'objet cible aux événements de l'objet division :
pSink->DispEventAdvise(pDivUnk);
// Effectue une simple opération :
long lTemp = 18;
pDivider->put_Operand1(&lTemp);
lTemp = 3;
pDivider->put_Operand2(&lTemp);
pDivider->Divide();
// Désabonne l'objet cible :
pSink->DispEventUnadvise(pDivUnk);
// Libère les ressources :
pDivUnk->Release();
pDivider->Release();

La méthode OnDone de la classe CDivisionSink sera appelée à la fin du traitement de la méthode Divide du composant Division. Notez que l'objet cible est automatiquement détruit lorsqu'on le désabonne de sa source d'événements.

91. Comment capter les événements sur une interface duale ?

Il est beaucoup plus simple de capter les événements provenant d'une interface custom ou une interface duale. En effet, le composant cible doit simplement implémenter l'interface duale en question, et s'abonner à l'objet source des événements.

La seule difficulté réside donc dans la manière de réaliser cette connexion. Ceci se fait à l'aide de la fonction globale AtlAdvise des ATL. Cette fonction prend en paramètre :

  • l'interface IUnknown du point de connexion auquel on désire s'abonner ;

  • l'interface IUnknown de l'objet qui doit recevoir les événements ;

  • l'IID de l'interface événementielle à utiliser ;

  • un pointeur sur un DWORD qui recevra l'identificateur de cette connexion.

Ce dernier paramètre devra être utilisé pour détruire cette connexion. Ceci se fait avec la fonction AtlUnadvise. Cette dernière fonction prend en paramètre l'interface IUnknown du point de connexion, l'IID de l'interface utilisée pour cette connexion et l'identificateur de la connexion renvoyée par AtlAdvise.

92. Peut-on recevoir des événements automation sur une interface duale ?

Même pour la réception des événements automation, il peut être plus pratique de définir une interface duale plutôt qu'une dispinterface en tant que client d'un point de connexion. Les appels fonctionneront dans tous les cas, qu'ils soient effectués par l'intermédiaire de l'interface IDispatch ou par l'interface custom. Pour que cette technique fonctionne, il faut toutefois que l'interface duale soit déclarée exactement avec le même IID que l'interface IDispatch, et faire l'AtlAdvise sur cet IID. Dans ce cas de configuration, le serveur appellera l'interface duale sur sont interface automation, et les ATL redirigeront automatiquement ces appels sur les méthodes de l'interface duale.

Cette technique est substantiellement plus simple que l'utilisation de la classe IDispEventImpl, mais elle implique malheureusement que la définition des interfaces des points de connexions ne peut pas être la même entre les clients et les serveurs. Ceci signifie également qu'on ne peut pas, dans un même projet, être à la fois son propre client et serveur. En effet, cela imposerait d'avoir une interface duale et une dispinterface définies avec le même IID dans le fichier IDL du projet.

93. Comment implémenter l'interface ISupportErrorInfo ?

L'interface ISupportErrorInfo permet à un composant de détailler la nature de l'erreur qui s'est produite lorsqu'il s'en produit une. Lorsqu'un appel à une méthode d'une interface échoue, le client peut récupérer ces informations complémentaires afin de préciser l'erreur à l'utilisateur. Ces informations sont plus pratiques que les codes de résultats HRESULT classiques, et les outils comme Visual Basic en font un usage intensif.

La gestion de cette interface est quasiment complètement automatisée par les ATL. Pour gérer cette interface, il suffit de cocher l'option « Support ISupportErrorInfo » dans les attributs du composant lors de sa création. Dès lors, il est possible d'appeler la fonction AtlReportError pour donner un message d'erreur explicite à chaque fois qu'un code d'erreur doit être retourné. Cette méthode prend en paramètre l'adresse sur la structure contenant le CLSID du composant générant l'erreur, le texte de l'erreur en Unicode, l'adresse sur la structure contenant l'IID de l'interface contenant la méthode dans laquelle l'erreur s'est produite, et enfin le code d'erreur qui sera retourné. Il existe différentes surcharges de cette fonction pour gérer différents aspects relatifs aux ressources de l'application et à l'aide en ligne.

Malheureusement, il est nécessaire de prévoir la gestion de l'interface ISupportErrorInfo dès la création du composant. Si l'on veut ajouter le support de cette interface a posteriori, il faut écrire manuellement le code que le Wizard génère pour gérer cette interface. Pour cela, il faut procéder comme suit :

  • faire dériver la classe du composant de l'interface ISupportErrorInfo ;

  • ajouter une entrée dans la liste des interfaces du composant pour cette interface à l'aide de la macro COM_INTERFACE_ENTRY ;

  • déclarer la méthode InterfaceSupportsErrorInfo dans la classe comme suit :

    STDMETHOD(InterfaceSupportsErrorInfo)(REFIID riid);

  • définir cette interface comme suit dans le fichier .cpp du composant :

    STDMETHODIMP CError&colon;&colon;InterfaceSupportsErrorInfo(REFIID riid)
    {
    static const IID* arr[] =
    {
    &IID_IError
    };
    for (int i=0; i < sizeof(arr) / sizeof(arr[0]); i++)
    {
    if (InlineIsEqualGUID(*arr[i],riid))
    return S_OK;
    }
    return S_FALSE;
    }

Ce code suppose que le composant modifié ici se nomme Error et est implémenté par la classe CError. Vous remarquerez que cette méthode contient la définition d'un tableau statique de pointeurs sur les IID des interfaces susceptibles d'être spécifiées dans les appel à AtlReportError. Il faut donc compléter ce tableau avec la liste des adresse des IID des interfaces du composant.

94. Comment implémenter un point de connexion pour l'interface IPropertyNotifySink ?

Les composants automation peuvent gérer, s'ils le désirent, un point de connexion pour l'interface IPropertyNotifySink. Cette interface permet à un composant serveur de demander à l'ensemble de ses clients l'autorisation de modifier la valeur d'une de ses propriétés, et de leur notifier les changements de valeurs si ceux-ci se produisent.

L'interface IPropertyNotifySink est déclarée comme suit :

interface IPropertyNotifySink : IUnknown
{
HRESULT OnChanged(DISPID DispID);
HRESULT OnRequestEdit(DISPID DispID);
};

Cette interface est très simple. À chaque fois qu'il désire modifier la valeur d'une propriété, le serveur appelle la méthode OnRequestEdit sur les interfaces IPropertyNotifySink de chacun de ses clients qui s'est connecté à son point de connexion, en fournissant le DISPID de cette propriété. Les clients peuvent répondre S_OK pour signaler qu'ils sont d'accord pour que le serveur modifie cette propriété, ou S_FALSE s'ils veulent rendre cette propriété en lecture seule. Remarquez que cette interface ne peut pas être utilisée pour effectuer la validation de la nouvelle valeur de la propriété, puisque celle-ci n'est pas communiquée par le serveur. Le but de cette méthode est ici de donner la possibilité à un client de contrôler les accès en écriture sur les propriétés d'un objet automation.

La méthode OnChanged, quant à elle, permet au serveur de notifier ses clients qu'une de ses propriétés a changé de valeur. Le DISPID de la propriété est passé en paramètre. Les clients peuvent récupérer cette nouvelle valeur s'il le désirent, mais le serveur ne la fournit pas non plus lors de la notification.

Remarquez qu'un serveur automation n'est pas obligé d'appeler ces deux méthodes pour toutes ses propriétés, même s'il gère un point de connexion pour l'interface IPropertyNotifySink. En fait, il ne doit demander l'autorisation de modification que pour les propriétés marquées de l'attribut requestedit dans le fichier IDL. De même, il ne doit notifier les changements de valeur que pour les propriétés marquées de l'attribut bindable dans le fichier IDL. Il est bien entendu possible d'utiliser les deux attributs pour une même propriété.

L'implémentation de l'interface IPropertyNotifySink ne pose pas de problème particulier pour les clients, car c'est une interface custom standard comme toutes les autres. De même, l'implémentation du point de connexion sur un composant serveur n'est pas difficile et peut être réalisée comme tous les autres points de connexions. Cependant, les ATL fournissent une implémentation standard pour ce point de connexion, et il est recommandé de l'utiliser. C'est en effet cette implémentation qui est utilisée par les contrôles ActiveX, et si vous désirez gérer ce point de connexion, autant se conformer aux mécanismes des contrôles ActiveX.

L'implémentation du point de connexion pour l'interface IPropertyNotifySink n'est pas réalisée automatiquement par les Wizards. Il faut donc modifier soi-même le code du composant ATL qui doit le gérer. Heureusement, ce n'est pas compliqué. Les opérations à effectuées sont les suivantes :

  • il faut ajouter le support pour les points de connexions au composant si ce n'est pas déjà fait ;

  • il faut déclarer l'interface IPropertyNotifySink comme interface source dans le fichier IDL pour ce composant ;

  • il faut ajouter le support du point de connexion standard des ATL pour l'interface IPropertyNotifySink ;

  • il faut déclarer ce point de connexion dans la liste des points de connexion du composant ;

  • il faut faire dériver le composant de la classe ATL CComControl, pour qu'il puisse utiliser les mécanismes standard des contrôles ATL ;

  • il faut définir une liste des propriétés du composant dont les notifications par l'interface IPropertyNotifySink doivent être gérées.

La première opération est effectuée automatiquement par le Wizard lors de la création du composant si l'on a indiqué qu'il gérait les points de connexions. Dans le cas contraire, il suffit de faire dériver la classe du composant de la classe template publique IConnectionPointContainerImpl, qui prend en paramètre le nom de la classe qui en dérive. Il faut également ajouter une entrée pour l'interface IConnectionPointContainer dans la liste des interfaces du composant.

L'ajout du support de l'interface IPropertyNotifySink dans le fichier IDL ne pose pas de problème. Pour implémenter le point de connexion pour cette interface, il faut faire dériver la classe du composant de la classe ATL IPropertyNotifySinkCP, qui prend en paramètre le nom de la classe qui en dérive. L'ajout de l'interface IPropertyNotifySink dans la liste des points de connexion se fait en ajoutant une ligne « CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink) » entre les macros BEGIN_CONNECTION_POINT_MAP et END_CONNECTION_POINT_MAP.

La partie importante est de faire dériver la classe du composant de la classe template ATL CComControl. Cette classe prend en paramètre le nom de la classe qui en dérive, et est définie dans le fichier d'en-tête atlctl.h. Ce fichier devra donc être ajouté, soit dans le fichier d'en-tête du composant, soit dans le fichier d'en-tête précompilé. Cette classe contient les fonctionnalités nécessaires aux contrôles ActiveX pour ATL. Elle suppose que le composant contient une liste des propriétés que le contrôle doit gérer. Il faut donc impérativement définir cette liste, ce qui se fait à l'aide des macros BEGIN_PROP_MAP, END_PROP_MAP et PROP_ENTRY. La première macro marque le début de cette liste, et prend en paramètre le nom de la classe qui la contient. La deuxième macro marque la fin de cette liste, et la troisième macro doit être utilisée pour définir chaque propriété entre le début et la fin de cette liste. Elle prend en paramètre une chaîne de caractère qui décrit la propriété, le DISPID de la propriété et le CLSID d'un composant gérant la page de propriété associée à ce contrôle, s'il en dispose d'une. Pour les composant ne disposant pas de pages de propriétés, on peut utiliser le CLSID nul, identifié par la constante CLSID_NULL.

L'en-tête typique d'une classe gérant un point de connexion pour l'interface IPropertyNotifySink est donc le suivant :

class ATL_NO_VTABLE CPropertyContainer &colon;
public CComObjectRootEx<CComSingleThreadModel>,
public CComControl<CPropertyContainer>,
public CComCoClass<CPropertyContainer, &CLSID_PropertyContainer>,
public IConnectionPointContainerImpl<CPropertyContainer>,
public IPropertyNotifySinkCP<CPropertyContainer>,
public IDispatchImpl<IPropertyContainer,
&IID_IPropertyContainer, &LIBID_PROPERTYLibv,
…

Les différentes listes contenues dans la classe se présenteront comme suit :

BEGIN_COM_MAP(CPropertyContainer)
COM_INTERFACE_ENTRY(IPropertyContainer)
COM_INTERFACE_ENTRY(IDispatch)
COM_INTERFACE_ENTRY(IConnectionPointContainer)
END_COM_MAP()
BEGIN_CONNECTION_POINT_MAP(CPropertyContainer)
CONNECTION_POINT_ENTRY(IID_IPropertyNotifySink)
END_CONNECTION_POINT_MAP()
BEGIN_PROP_MAP(CPropertyContainer)
PROP_ENTRY("Ma propriété", 1, CLSID_NULL)
END_PROP_MAP()

Dans l'exemple donné ci-dessus, le composant est supposé gérer une interface duale IPropertyContainer, qui contient les méthodes get_Prop et put_Prop permettant d'accéder à une propriété Prop. La partie intéressante est maintenant l'implémentation de l'accesseur put_Prop, parce que c'est lui qui va effectuer les notifications via l'interface IPropertyNotifySink.

Si la propriété est marquée de l'attribut requestedit dans le fichier IDL contenant la définition de l'interface, il doit appeler la méthode FireOnRequestEdit avec le DISPID de la propriété avant toute tentative de modification de la valeur de cette propriété. Si la réponse renvoyée est S_OK, il peut effectuer la modification. Dans le cas contraire, il doit ignorer la nouvelle valeur. De la même manière, il doit appeler la méthode FireOnChanged avec le DISPID de la propriété en paramètre dès que la valeur de cette propriété est modifiée, si celle-ci est marquée de l'attribut bindable dans le fichier IDL. Le code d'exemple ci-dessus conviendrait pour la propriété Prop de notre composant CPropertyContainer, si elle est marquée des deux attributs requestedit et bindable dans le fichier IDL :

STDMETHODIMP CPropertyContainer&colon;&colon;put_Prop(long newVal)
{
AFX_MANAGE_STATE(AfxGetStaticModuleState())
if (FireOnRequestEdit(1) == S_OK)
{
m_lValue = newVal;
FireOnChanged(1);
}
return S_OK;
}

Remarquez que les deux méthodes FireOnRequestEdit et FireOnChanged ne sont pas implémentées par la classe d'implémentation du point de connexion IPropertyNotifySinkCP, mais par la classe de base CComControl. En fait, la classe CComControl utilise les informations stockées dans la property map pour effectuer les notifications, property map qui elle-même utilise les fonctions fournies par le point de connexion IPropertyNotifySink. L'utilisation de la classe CComControl impose donc la création de la property map. Afin de s'assurer que celle-ci existe, la classe CComControl utilise un type défini par un typedef dans la macro BEGIN_PROPERTY_MAP. Celle-ci utilise également un type défini dans la classe d'implémentation du point de connexion IPropertyNotifySinkCP. En fin de compte, les ATL imposent d'être un contrôle pour gérer ce point de connexion (bien qu'en théorie, ce ne soit pas strictement nécessaire).

95. Quels sont les problèmes que l'on peut rencontrer avec les ATL ?

96. J'ai trouvé une bogue dans les ATL !

Les ATL sont une aide précieuse pour programmer des composants DCOM en C++. Ce travail de titan, sans être aussi facile qu'avec un outil comme Visual Basic, redevient faisable. Cependant, tout n'est pas si rose. En effet, il faut toujours avoir une bonne compréhension des mécanismes de DCOM sous-jacents, et parfois on a à résoudre des problèmes curieux voire incompréhensible avec les ATL.

Il faut d'abord savoir que les Wizards de Visual C++ ne sont pas exempt de bogues, loin de là. Ils peuvent parfaitement générer du code faux, voire du code qui ne compile franchement pas. La correction des erreurs de compilation et d'édition de lien est assez facile à faire, mais lorsqu'on croise ces erreurs pour la première fois, on peut s'arracher les cheveux facilement.

Ensuite, des erreurs encore plus techniques et incompréhensibles peuvent parfaitement se produire à l'exécution. Cette fois, ces erreurs proviennent souvent de fautes de manipulation de la part du programmeur, ou tout simplement d'une utilisation erronée des fonctionnalités des ATL.

La fin de ce document essaie de recenser, de manière non exhaustive, les principaux problèmes que l'on peut rencontrer lorsqu'on programme des composants avec les ATL.

97. Il me manque des définitions sur des constantes DIID_XXX ou IID_XXX !

98. Le compilateur n'arrive pas à récupérer l'adresse d'une constante DIID_XXX ou IID_XXX !

99. Le compilateur MIDL n'a pas généré les définitions des constantes des identificateurs d'interfaces !

Il n'est pas rare que le compilateur C++ se plaigne sur des constantes IID_XXX ou DIID_XXX, qui normalement doivent représenter la valeur du GUID des interfaces et des dispinterfaces. Ce genre de situations peut avoir trois causes :

  • le programmeur a modifié une dispinterface événementielle en interface duale pour implémenter un point de connexion avec une interface duale. Il faut dans ce cas recompiler le fichier IDL et réimplémenter le point de connexion. En effet, le nom des constantes contenant le GUID pour les interfaces duales commence par IID, alors que celui utilisé pour les dispinterface commence par DIID. Si un point de connexion a déjà été implémenté, et que l'interface change de nature, il faut refaire complètement le point de connexion et retirer toutes les référencent à l'ancien point de connexion ;

  • le programmeur utilise une interface définie avant le mot-clé « library » dans le fichier IDL, mais non référencé dans la définition de la librairie. Il faut ajouter une référence sur cette interface ou cette dispinterface dans le corps de la définition de la type library, avec l'une des syntaxes suivantes :

    interface nom;

    ou :

    dispinterface nom;

    En effet, le compilateur MIDL ne génère les définitions pour les interfaces dans les fichiers sources que lorsqu'elles sont référencées dans le corps de la type library ;
  • l'inclusion sur les fichier « xxx.h » et « xxx_i.c » générés par MIDL dans le fichier source principal du projet est mal faite. Il faut impérativement inclure les fichiers xxx_i.c après le fichier xxx.h correspondant. En effet, les constantes définie en portée globale dans un fichier source sont déclarées statiques par défaut si elles n'ont pas été déclarées extern avant leur définition. C'est une règle du langage C++, et elle impose que les fichiers d'en-tête, contenant les déclarations, soient toujours inclus avant la définition des constantes dans les fichiers sources.

100. Le code généré par le Wizard ne compile pas !

101. Je n'arrive pas à compiler le fichier d'un point de connexion !

Les Wizards ont parfois quelques difficultés à implémenter les points de connexions pour les interfaces événementielles dont les méthodes utilisent des passages de paramètres de type BOOL ou des passages de paramètres par référence à l'aide de pointeur. Il faut corriger manuellement le code généré par Visual C++ pour ces points de connexions. Cette correction devra être refaite à chaque fois que le point de connexion sera réimplémenté pour une interface.

102. Je n'arrive pas à instancier une classe parce qu'elle a des méthodes virtuelles pures, et pourtant toutes mes méthodes d'interfaces sont définies !

103. L'édition de liens de mon programme échoue avec des unresolved symbols, et pourtant toutes mes méthodes d'interfaces sont définies !

Les Wizards ont parfois quelques difficultés à implémenter des méthodes utilisant le type BOOL ou des types déclarés « constants » avec le mot-clé const. Par conséquent, ils définissent des fonctions dont les signatures sont incorrectes pour ces interfaces. Le compilateur accepte ces définitions, et les considère comme des surcharges des méthodes virtuelles pures des interfaces. Ces dernières ne sont donc réellement pas implémentées. La correction est ici très simple, il suffit de corriger la liste des paramètres des méthodes définies par le Wizard.

104. Mon programme plante dès que j'effectue une notification d'événement !

Le point de connexion utilise une interface duale et effectue donc l'appel des clients sur l'une des méthodes custom. Le client est un client Visual Basic, ou un client qui suppose que l'interface événementielle est une dispinterface. Par conséquent, son objet cible ne dispose pas des méthodes custom de l'interface duale, et l'appel de ces méthodes plante le programme.

La solution est d'utiliser d'utiliser une dispinterface pour cette interface événementielle. Il faut modifier le fichier IDL et réimplémenter le point de connexion.

105. Les paramètres que je reçois dans mes méthodes d'interfaces événementielles sont complètement faux !

106. Certains des paramètres de mes méthodes d'interfaces événementielles sont corrompus !

Le code des ATL utilise, au sein de la classe IDispEventImpl, une fonction d'OLE non documentée et boguée. Cette fonction a pour but de générer un appel de fonction __stdcall à partir de la description de la fonction dans une type library et d'un tableau de paramètres. C'est cette fonction qui effectue l'appel des méthodes des entrées SINK_ENTRY_EX.

La nature du bogue est la suivante : un décalage peut parfois apparaître dans la structure de la pile de la fonction événementielle appelée, lorsque l'on utilise des paramètres dont la taille n'est pas de 32 bits. Le résultat est que les paramètres reçus par la fonction événementielle sont faux, et bien entendu inutilisables. Notez que ce genre de phénomène peut également se produire si la fonction événementielle n'est pas déclarée avec les conventions d'appel __stdcall.

Pour éviter ce problème, il faut se restreindre à des paramètres 32 bits exclusivement pour les méthodes des interfaces événementielles. En pratique, Visual Basic montre l'exemple en passant tous les paramètres par pointeurs. C'est la seule solution viable, qui assure en plus la meilleure intégration avec Visual Basic.

107. AtlAdvise échoue, et pourtant je ne vois pas d'erreur !

AtlAdvise peut échouer pour de multiples raisons. La plus courante est sans doute que le composant cible ne gère pas l'interface événementielle demandée. Si les apparences sont trompeuses, c'est que l'IID ou le DIID demandé n'est pas celui de l'interface à laquelle on essaie de s'abonner.

Une autre raison, beaucoup plus obscure, est la suivante. L'abonnement à une interface événementielle d'un objet situé dans un autre appartement nécessite le marshalling de cette interface. Si aucun proxy / stub n'est enregistré pour cette interface dans la clé Interface de la base de registres, l'abonnement échouera sans renvoyer de message très clair. Cette situation peut se produire pour les interfaces custom quand la DLL contenant les facelets et les stublets n'a pas été enregistrée, ou quand l'interface est une interface dérivant de l'interface IDispatch, mais n'a pas été déclarée duale dans le fichier IDL. Cet oubli arrive souvent lorsqu'on a transformé une dispinterface événementielle en interface duale. La solution est donc soit d'enregistrer la DLL de marshalling, soit de rajouter le mot-clé dual dans les attributs de l'interface événementielle. Notez que le code de marshalling est automatiquement fourni par OLE pour les interfaces duales et les dispinterfaces.

108. Tout est correct, et pourtant je ne reçoit toujours aucune notification d'événement automation sur mon interface événementielle !

109. Les méthodes de mon interface duale ne sont pas appelées lorsqu'un client m'envoie une requête automation sur cette interface !

Les implémentations des interfaces duales et des sink maps pour la réception des événements provenant d'une dispinterface utilisent la définition de cette interface dans une type library. C'est la raison pour laquelle il faut toujours communiquer le nom de la constante contenant le GUID de la type library aux classes IDispatchImpl et IDispEventImpl. Par conséquent, si la description de la dispinterface n'est pas donnée dans la type library communiquée à ces deux classes, le code des ATL ne peut pas interpréter les DISPID et les paramètres qu'il reçoit dans son implémentation de la méthode IDispatch. Ces requêtes automation sont alors tout simplement ignorées, ce qui a pour conséquence que les méthodes des interfaces duales et les fonctions de traitement des événements automation ne sont pas appelées.

Si ce genre de problème vous arrive, il se peut simplement que le GUID de la type library communiqué aux classes ATL ne soit pas le bon. En pratique, ce type d'erreur survient rarement, parce qu'on ne peut mettre qu'un seul fichier IDL dans un projet ATL dans Visual C++, et il n'y a donc qu'une seule type library dans ce projet. Cependant, ce peut être le cas si l'on cherche à implémenter une interface duale ou des événements automation d'une dispinterface définies dans une type library externe. Il faut donc bien vérifier que les GUID des type libraries communiqués aux classes ATL sont corrects.

Un autre cas possible est tout simplement que vous référencez bien la type library du projet courant, mais que cette type library ne contient pas la définition de l'interface duale ou de la dispinterface qui pose problème. Ceci peut arriver si la section de library du fichier IDL ne contient aucune référence sur cette interface ou cette dispinterface. La solution dans ce cas est en général de rajouter cette référence manuellement dans la section library, ou de déplacer toutes les définitions de ces interfaces dans cette section. Une référence d'interface peut être ajoutée dans la type library du projet simplement avec une ligne comme celle-ci :

interface nom;

ou :

dispinterface nom;

dans la section library du fichier IDL.

110. Je n'arrive pas à créer une page de propriété pour un de mes composants !

Les composants gérant les pages de propriétés sont aggrégés par les property frames dans OLE. Ceci implique qu'il faut que les property pages supportent l'aggrégation d'une part, et qu'elles soient dans le même appartement que la property page qui les contient d'autre part. Ce dernier point est plus difficile à contrôler dans un programme multithreadé. Une façon simple de s'en assurer est d'utiliser le modèles de threading Both pour les pages de propriétés.

111. J'ai une erreur lors de l'enregistrement de mon composant !

Les composants ATL ont besoin, pour s'enregistrer, des données d'enregistrement contenus dans leurs ressources. Ces données sont placées dans les fichiers .RGS de votre projet. L'enregistrement peut échouer si vous avez modifié le CLSID d'un composant dans le fichier IDL et que vous n'avez pas reporté cette modification dans le fichier .RGS de ce composant.

La solution consiste ici à contrôler la cohérence entre les CLSID du fichier IDL et les CLSID des fichiers .RGS du projet, et à recompiler le tout.

112. L'enregistrement de mon composant échoue avec une erreur sur un ordinal introuvable dans la DLL ATL.DLL !

113. J'ai un conflit de version avec la librairie ATL.DLL !

Il existe plusieurs versions de la DLL ATL.DLL. Malheureusement, elles portent toutes le même nom, et sont incompatibles parce qu'elles enregistrent toutes différentes versions des mêmes composants. C'est une erreur grave dans la gestion des versions de la libraire ATL (on peut également rencontrer ce type d'erreur avec les librairies des MFC).

La seule solution est de compiler les composants avec le code des ATL en statique. Contrairement au MFC, où cette solution est peu satisfaisante en raison de la taille des exécutables générés, elle convient parfaitement pour les ATL.

Pour cela, il faut compiler les serveurs en utilisant la configuration « MinDependancy ». Cette configuration utilise les macros « _ATL_STATIC_REGISTRY » et « _ATL_MIN_CRT », et n'utilise pas la macro « _ATL_DLL » contrairement aux autres configurations. La simple correction de ces macros de compilation conditionnelle suffit à supprimer la dépendance des fichiers binaires produits envers la DLL ATL.DLL.

Notez toutefois que si vous avez ajouté le support des MFC dans votre projet, vous devrez également supprimer l'inclusion du fichier atlimpl.cpp dans le fichier StdAfx.cpp, faute de quoi vous aurez des symboles définis plusieurs fois à l'édition de liens dans les configuration DEBUG.

114. Je n'arrive pas à créer un composant distribué en DCOM !

Les erreurs possibles sont les suivantes :

  • la DLL de proxy/stub du composant distant n'est pas enregistrée sur le poste client ;

  • les clés enregistrées dans la base de registres sont erronées ;

  • le serveur n'est pas enregistré sur le poste distant ;

  • le compte utilisateur du poste client n'a pas les droits nécessaires pour exécuter le composant sur la machine distante.

115. Je n'arrive pas à créer un composant distribué en Visual Basic, mais j'y arrive en C++ !

Le composant est déclaré avec le mot-clé WithEvents et le poste client ne donne pas assez de droits au compte utilisateur dans lequel le serveur distant fonctionne. Lorsqu'il crée un objet marqué avec le mot-clé WithEvents, Visual Basic crée l'objet et effectue l'abonnement aux notifications dans la foulée. Il peut donc obtenir une erreur lors de l'abonnement, et signaler une erreur bien que le composant ait effectivement été créé.

Le code C++ parvient à créer l'objet, mais l'abonnement se fait dans une deuxième étape, il est donc possible de ne pas s'apercevoir qu'il n'a pas pu être réalisé.

116. Je n'arrive pas à m'abonner aux événements d'un composant distribué 

C'est un problème de sécurité de Windows NT. Il faut revoir les droits d'accès sur le poste client.

Une autre possibilité est que la DLL de proxy/stub de l'interface événementielle n'est pas installée sur le poste client.

Bibliographie

Documents

Inside OLE2, Kraig Brokschnmidt, Microsoft Press.

Professionnal ATL COM Programming, Dr. Richard Grimes, Wrox.

MSJ, 1996#5 (Mai), Librairie MSDN.

Win32 SDK, OLE, Librairie MSDN.

Win32 SDK, RPC, Librairie MSDN.

Adresses Internet utiles

Spécifications COM, http://www.microsoft.com/oledev/olecom/title.htm .

Business overview, http://www.microsoft.com/msdn/sdk/techinfo/dcombo.doc .

Technical overview, http://www.microsoft.com/msdn/sdk/techinfo/dcomto.doc .

ActiveX group, http://www.ActiveX.org .

Newsgroup

msnews.microsoft.com.

microsoft.public.win32.programmer.ole.