IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)

FAQ DCOM/OLE/ATL

FAQ DCOM/OLE/ATLConsultez toutes les FAQ

Nombre d'auteurs : 2, nombre de questions : 91, dernière mise à jour : 16 juin 2021 

 
OuvrirSommaireProgrammation OLE

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.

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

Créé le 9 juillet 2000  par Christian Casteyde

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 :

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

 
Sélectionnez
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 bibliothèque 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 bibliothèque 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 :

 
Sélectionnez
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 bibliothèque 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 :

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

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

 
Sélectionnez
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

 
Sélectionnez
#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;
}
Créé le 9 juillet 2000  par Christian Casteyde

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).
Créé le 9 juillet 2000  par Christian Casteyde

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 bibliothèque statique uuid.lib.

La spécification de IUnknown est la suivante :

 
Sélectionnez
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).

Créé le 9 juillet 2000  par Christian Casteyde

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.

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 :

 
Sélectionnez
struct IMalloc : public IUnknown
{
    virtual void * STDMETHODCALLTYPE Alloc(ULONG cb)=0;
    virtual void * STDMETHODCALLTYPE Realloc(void *pv, ULONG cb)=0;
    virtual void STDMETHODCALLTYPE Free(void *pv)=0;
    virtual ULONG STDMETHODCALLTYPE GetSize(void *pv)=0;
    virtual int STDMETHODCALLTYPE DidAlloc(void *pv)=0;
    virtual void STDMETHODCALLTYPE HeapMinimize(void)=0;
};

Les méthodes de cette interface sont classiques. Alloc permet d'allouer un bloc de mémoire, Free de le libérer et Realloc de changer sa taille. GetSize permet de déterminer la taille d'un bloc de mémoire, et DidAlloc permet d'indiquer si un bloc mémoire a été alloué par l'allocateur dont on appelle cette méthode. La méthode HeapMinimize permet de compacter le tas des blocs mémoire et de rendre l'espace inutilisé au système d'exploitation.

La fonction de l'API OLE qui permet d'obtenir un pointeur sur l'allocateur mémoire est déclarée ci-dessous :

 
Sélectionnez
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.

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 bibliothèque standard du C, et ne font qu'encapsuler les appels à CoGetMalloc/méthode IMalloc::Release :

 
Sélectionnez
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.

Créé le 9 juillet 2000  par Christian Casteyde

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

 
Sélectionnez
class CAdder : public IAdder
{
    // Compte de références sur l'objet :
    unsigned long m_ulRefCount;
public:
    // Méthodes de l'interface IUnknown :
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(
        REFIID iid, void **ppvObject);
    virtual ULONG STDMETHODCALLTYPE AddRef(void);
    virtual ULONG STDMETHODCALLTYPE Release(void);
    // Méthodes de l'interface IAdder :
    virtual HRESULT STDMETHODCALLTYPE Add(long i, long j, long *iResult);
    virtual HRESULT STDMETHODCALLTYPE Sub(long i, long j, long *iResult);
    // Constructeur et autres fonctions de gestion de la classe :
    CAdder(void);
    HRESULT Init(void);
};

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 :

 
Sélectionnez
CAdder::CAdder(void) : m_ulRefCount(0)
{
}

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

 
Sélectionnez
ULONG STDMETHODCALLTYPE CAdder::AddRef(void)
{
    m_ulRefCount++;
    return m_ulRefCount;
}

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 :

 
Sélectionnez
ULONG STDMETHODCALLTYPE CAdder::Release(void)
{
    m_ulRefCount--;
    if (m_ulRefCount!=0) return m_ulRefCount;
    delete this;     // Destruction de l'objet.
    return 0;        // Ne pas renvoyer m_ulRefCount (il n'existe plus).
}

Dans ce code, on voit que le compteur est décrémenté. Si ce compteur est nul, l'objet est détruit. Dans tous les cas, le nombre de références est renvoyé.

Enfin, la méthode QueryInterface se charge de renvoyer les pointeurs sur les interfaces gérées. Si l'interface demandée n'est pas gérée, le pointeur nul doit être retourné. En revanche, si l'interface est géré, QueryInterface augmente le compte de références de une unité :

 
Sélectionnez
HRESULT STDMETHODCALLTYPE CAdder::QueryInterface(REFIID iid, void **ppvObject)
{
    *ppvObject=0;   // Toujours initialiser le pointeur renvoyé.
    if (iid==IID_IUnknown)
        *reinterpret_cast<IUnknown **>(ppvObject)=
        static_cast<IUnknown *>(this);
    else if (iid==IID_IAdder)
        *reinterpret_cast<IAdder **>(ppvObject)=
        static_cast<IAdder *>(this);
    if (*ppvObject==0) return E_NOINTERFACE;
    AddRef();           // On incrémente le compteur de références.
    return NOERROR;
}

Une fois le composant créé, il faut fournir le code de création pour que les clients puissent l'utiliser. Typiquement, ce code de création demande l'IID de l'interface demandée pour le composant et renvoie un pointeur cette l'interface :

 
Sélectionnez
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.

Créé le 9 juillet 2000  par Christian Casteyde

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.

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

 
Sélectionnez
// Définition de l'interface IOpposite :
struct IOpposite : public IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE Opposite(long i, long *pResult)=0;
};
// Implémentation d'un objet gérant les deux interfaces
// par héritage multiple :
class CAdder : public IAdder, public IOpposite
{
    unsigned long m_ulRefCount;      // Le compteur de références.
public:
    // Les méthodes de IUnknown sont communes à toutes les interfaces :
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(
        REFIID iid, void **ppvObject);
    virtual ULONG STDMETHODCALLTYPE AddRef(void);
    virtual ULONG STDMETHODCALLTYPE Release(void);
    // Méthodes de l'interface IAdder :
    virtual HRESULT STDMETHODCALLTYPE Add(long i, long j, long *pResult);
    virtual HRESULT STDMETHODCALLTYPE Sub(long i, long j, long *pResult);
    // Méthodes de l'interface IOpposite :
    virtual HRESULT STDMETHODCALLTYPE Opposite(long i, long *pResult);
    // Constructeurs et fonctions d'initialisation :
    CAdder(void);
    HRESULT Init(void);
};
// Implémentation :
CAdder::CAdder(void) : m_ulRefCount(0)
{
    return ;
}
HRESULT CAdder::Init(void)
{
    return NOERROR;
}
// IUnknown :
HRESULT STDMETHODCALLTYPE CAdder::QueryInterface(REFIID iid,
                                                 void **ppvObject)
{
    *ppvObject=0;
    if (iid==IID_IUnknown)
        *reinterpret_cast<IUnknown **>(ppvObject)=
        static_cast<IAdder *>(this);
    else if (iid==IID_IAdder)
        *reinterpret_cast<IAdder **>(ppvObject)=
        static_cast<IAdder *>(this);
    else if (iid==IID_IOpposite)
        *reinterpret_cast<IOpposite **>(ppvObject)=
        static_cast<IOpposite *>(this);
    if (*ppvObject==0) return E_NOINTERFACE;
    AddRef();
    return NOERROR;
}
ULONG STDMETHODCALLTYPE CAdder::AddRef(void)
{
    m_ulRefCount++;
    return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdder::Release(void)
{
    m_ulRefCount--;
    if (m_ulRefCount!=0) return m_ulRefCount;
    delete this;
    return 0;
}
// IAdder :
HRESULT STDMETHODCALLTYPE CAdder::Add(long i, long j, long *pResult)
{
    *pResult=i+j;
    return ;
}
HRESULT STDMETHODCALLTYPE CAdder::Sub(long i, long j, long *pResult)
{
    *pResult=i-j;
    return ;
}
// IOpposite :
HRESULT STDMETHODCALLTYPE CAdder::Opposite(long i, long *pResult)
{
    *pResult=-i;
    return ;
}

La seule différence par rapport à l'implémentation par héritage simple réside dans la fonction QueryInterface. Dans le cas où l'interface demandée est l'interface IUnknown, le pointeur this est transtypé en un pointeur sur une des interfaces gérées par le composant. On est obligé de renvoyer un pointeur sur une autre interface que l'interface IUnknown parce qu'il existe plusieurs interfaces IUnknown dans ce composant (une pour chaque interface gérée), ce qui provoque une ambiguïté. En fait, le choix de l'interface que l'on renvoie n'est absolument pas déterminant, puisque du point de vue du client, seules les trois premières entrées dans la table des fonctions de l'interface comptent (or ces trois premières entrées sont toujours prises par les fonctions de l'interface IUnknown). On remarquera au passage que l'on a eu besoin d'implémenter qu'une seule interface IUnknown. Donc l'impossibilité d'implémenter plusieurs fonctions membres de même nom et de même signature dans des interfaces différentes peut également être considéré ici comme un avantage.

Créé le 9 juillet 2000  par Christian Casteyde

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.

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

Créé le 9 juillet 2000  par Christian Casteyde

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

 
Sélectionnez
// Définition de l'interface IAdder :
struct IAdder : public IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE Add(long i, long j,
        long *pResult)=0;
    virtual HRESULT STDMETHODCALLTYPE Sub(long i, long j,
        long *pResult)=0;
};
// Définition de l'interface IOpposite :
struct IOpposite : public IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE Opposite(long i, long *pResult)=0;
};
// Définition de l'interface IInnerUnknown :
struct IInnerUnknown
{
    virtual HRESULT STDMETHODCALLTYPE InnerQueryInterface(REFIID iid,
        void **ppvObject)=0;
    virtual ULONG STDMETHODCALLTYPE InnerAddRef(void)=0;
    virtual ULONG STDMETHODCALLTYPE InnerRelease(void)=0;
};
// Implémentation d'un objet gérant l'agrégation
// et plusieurs interfaces par héritage multiple :
class CAdder : public IInnerUnknown, public IAdder, public IOpposite
{
    IUnknown *m_pUnknown;// Pointeur sur l'interface IUnknown à utiliser.
    unsigned long m_ulRefCount;
public:
    // Les méthodes de IInnerUnknown :
    virtual HRESULT STDMETHODCALLTYPE InnerQueryInterface(REFIID iid,
        void **ppvObject);
    virtual ULONG STDMETHODCALLTYPE InnerAddRef(void);
    virtual ULONG STDMETHODCALLTYPE InnerRelease(void);
    // Les méthodes de IUnknown :
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid,
        void **ppvObject);
    virtual ULONG STDMETHODCALLTYPE AddRef(void);
    virtual ULONG STDMETHODCALLTYPE Release(void);
    // Méthodes de l'interface IAdder :
    virtual HRESULT STDMETHODCALLTYPE Add(long i, long j,
        long *pResult);
    virtual HRESULT STDMETHODCALLTYPE Sub(long i, long j,
        long *pResult);
    // Méthodes de l'interface IOpposite :
    virtual HRESULT STDMETHODCALLTYPE Opposite(long i, long *pResult);
    // Constructeurs et fonctions d'initialisation :
    CAdder(IUnknown *pUnknown);
    HRESULT Init(void);
};
// Implémentation :
CAdder::CAdder(IUnknown *pUnknown) : m_ulRefCount(0)
{
    // On détermine l'interface IUnknown à utiliser :
    if (pUnknown!=0)
        // Interface externe de l'agrégat
        // (passé en paramètre lors de la construction) :
        m_pUnknown=pUnknown;
    else
        // Interface IUnknown interne du composant :
        m_pUnknown=reinterpret_cast<IUnknown *>(
        static_cast<IInnerUnknown *>(this));
    return ;
}
HRESULT CAdder::Init(void)
{
    return NOERROR;
}
// Les méthodes de IUnknown appellent les fonctions de l'interface interne
// ou celles de l'interface externe de l'agrégat :
HRESULT STDMETHODCALLTYPE CAdder::QueryInterface(REFIID iid, void **ppvObject)
{
    return m_pUnknown->QueryInterface(iid, ppvObject);
}
ULONG STDMETHODCALLTYPE CAdder::AddRef(void)
{
    return m_pUnknown->AddRef();
}
ULONG STDMETHODCALLTYPE CAdder::Release(void)
{
    return m_pUnknown->Release();
}
// Les méthodes de IInnerUnknown gèrent la durée de vie du composant :
HRESULT STDMETHODCALLTYPE CAdder::InnerQueryInterface(REFIID iid,
                                                      void **ppvObject)
{
    // Initialisation du pointeur :
    *ppvObject=0;
    // Obtient le pointeur sur l'interface demandée :
    // Le test suivant ne peut être vérifié que dans deux cas :
    // - soit l'objet n'est pas aggrégé ;
    // - soit il est aggrégé et la fonction de création demande
    // l'interface IUnknown interne pour l'aggrégat.
    // Dans les deux cas on doit renvoyer un pointeur
    // sur l'interface IInnerUnknown :
    if (iid==IID_IUnknown)
        *reinterpret_cast<IInnerUnknown **>(ppvObject)=
        static_cast<IInnerUnknown *>(this);
    // Les tests suivants ne sont exécutés que lorsque l'objet
    // n'est pas agrégé ou lorsque la fonction QueryInterface
    // de l'agrégat délègue son travail :
    else if (iid==IID_IAdder)
        *reinterpret_cast<IAdder **>(ppvObject)=
        static_cast<IAdder *>(this);
    else if (iid==IID_IOpposite)
        *reinterpret_cast<IOpposite **>(ppvObject)=
        static_cast<IOpposite *>(this);
    if (*ppvObject==0) return E_NOINTERFACE;
    // Si l'interface est gérée, fixe le compte de références. Dans le
    // cas des objets agrégés, on doit appeler AddRef pour l'agrégat.
    // Dans les autres cas, ainsi que dans le cas de la création de
    // l'objet agrégé dans un agrégat, on doit appeler AddRef de l'objet
    // agrégé. Dans tous les cas, on peut appeler directement AddRef sur
    // l'interface qui a été renvoyée :
    reinterpret_cast<IUnknown *>(*ppvObject)->AddRef();
    return NOERROR;
}
ULONG STDMETHODCALLTYPE CAdder::InnerAddRef(void)
{
    m_ulRefCount++;
    return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdder::InnerRelease(void)
{
    m_ulRefCount--;
    if (m_ulRefCount!=0) return m_ulRefCount;
    delete this;
    return 0;
}
// Les méthodes des autres interfaces restent inchangées :
HRESULT STDMETHODCALLTYPE CAdder::Add(long i, long j, long *pResult)
{
    *pResult=i+j;
    return ;
}
HRESULT STDMETHODCALLTYPE CAdder::Sub(long i, long j, long *pResult)
{
    *pResult=i-j;
    return ;
}
HRESULT STDMETHODCALLTYPE CAdder::Opposite(long i, long *pResult)
{
    *pResult=-i;
    return ;
}
// Le code de création peut être implémenté de la manière suivante :
HRESULT CreateAdder(REFIID iid, IUnknown *pOuterUnknown, void **ppvObject)
{
    // Initialisation du  pointeur retourné :
    *ppvObject=0;
    // Vérification des paramètres pour l'agrégation :
    if (pOuterUnknown!=0 && iid!=IID_IUnknown)
        return CLASS_E_NOAGGREGATION;
    // Création de l'objet :
    CAdder *pAdder=new CAdder(pOuterUnknown);
    pAdder->Init();
    // Demande de l'interface désirée. On ne peut pas appeler
    // QueryInterface directement parce que cette fonction appellerait
    // la fonction QueryInterface de l'agrégat dans le cas de
    // l'agrégation. On ne renverrait donc pas l'interface IUnknown
    // interne de l'objet en cours de création :
    return pAdder->InnerQueryInterface(iid, ppvObject);
}
Créé le 9 juillet 2000  par Christian Casteyde

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

 
Sélectionnez
// Implémentation de l'agrégat :
// Définition de l'interface IMultiplier de l'agrégat :
struct IMultiplier : public IUnknown
{
    virtual HRESULT STDMETHODCALLTYPE Mul(long i, long j,
        long *pResult)=0;
};
// Classe implémentant l'agrégat :
class CCalculator : public IInnerUnknown, public IMultiplier
{
    IUnknown *m_pUnknown;
    unsigned long m_ulRefCount;
    // Pointeurs sur l'objet agrégé :
    IUnknown *m_pAdderInnerUnknown;
public:
    // Les méthodes de IInnerUnknown :
    virtual HRESULT STDMETHODCALLTYPE InnerQueryInterface(REFIID iid,
        void **ppvObject);
    virtual ULONG STDMETHODCALLTYPE InnerAddRef(void);
    virtual ULONG STDMETHODCALLTYPE InnerRelease(void);
    // Les méthodes de IUnknown :
    virtual HRESULT STDMETHODCALLTYPE QueryInterface(REFIID iid,
        void **ppvObject);
    virtual ULONG STDMETHODCALLTYPE AddRef(void);
    virtual ULONG STDMETHODCALLTYPE Release(void);
    // La méthode de IMultiplier :
    virtual HRESULT STDMETHODCALLTYPE Mul(long i, long j, long *pResult);
    // Constructeur, destructeur et autres fonctions d'initialisation :
    CCalculator(IUnknown *pUnknown);
    ~CCalculator(void);
    HRESULT Init(void);
};
CCalculator::CCalculator(IUnknown *pUnknown) : m_ulRefCount(0)
{
    if (pUnknown!=0) m_pUnknown=pUnknown;
    else m_pUnknown=reinterpret_cast<IUnknown *>(
        static_cast<IInnerUnknown *>(this));
    return ;
}
HRESULT CCalculator::Init(void)
{
    // Protection contre les destructions prématurées
    m_pIUnknown->AddRef();
    // La fonction d'initialisation construit l'objet Adder
    // que le Multiplier va utiliser :
    return CreateAdder(IID_IUnknown, m_pUnknown,
        reinterpret_cast<void **>(&m_pAdderInnerUnknown));
}
CCalculator::~CCalculator(void)
{
    // Destruction des pointeurs stockés à usage interne.
    // Ceci se fait en appelant la méthode AddRef de sa propre
    // interface IUnknown externe pour fixer le compte de références
    // à sa valeur correcte avant d'appeler Release sur le pointeur
    // à libérer. Par exemple, si m_pAdder était un pointeur
    // à usage interne, on écrirait :
    // m_pUnknown->AddRef();
    // m_pAdder->Release();
    // Puis, on détruit le pointeur sur l'interface IUnknown interne
    // de l'objet agrégé :
    m_pAdderInnerUnknown->Release();
    return ;
}
HRESULT STDMETHODCALLTYPE CCalculator::QueryInterface(REFIID iid,
                                                      void **ppvObject)
{
    return m_pUnknown->QueryInterface(iid, ppvObject);
}
ULONG STDMETHODCALLTYPE CCalculator::AddRef(void)
{
    return m_pUnknown->AddRef();
}
ULONG STDMETHODCALLTYPE CCalculator::Release(void)
{
    return m_pUnknown->Release();
}
HRESULT STDMETHODCALLTYPE CCalculator::InnerQueryInterface(REFIID iid,
                                                           void **ppvObject)
{
    *ppvObject=0;
    if (iid==IID_IUnknown)
        *reinterpret_cast<IInnerUnknown **>(ppvObject)=
        static_cast<IInnerUnknown *>(this);
    else if (iid==IID_IMultiplier)
        *reinterpret_cast<IMultiplier **>(ppvObject)=
        static_cast<IMultiplier *>(this);
    // Si l'interface est gérée, on appelle AddRef et on retourne*
    // NOERROR :
    if (*ppvObject!=0)
    {
        reinterpret_cast<IUnknown *>(*ppvObject)->AddRef();
        return NOERROR;
    }
    // Si l'interface n'est pas gérée directement, déléguer l'appel
    // à QueryInterface des objets dont on est éventuellement constitué
    // en tant qu'agrégat.
    // Les tests sur les interfaces doivent être réalisés malgré tout
    // afin d'éviter l'évolution incontrôlée des spécifications
    // de l'agrégat avec l'évolution de ses constituants.
    if (iid==IID_IAdder || iid==IID_IOpposite)
        return m_pAdderInnerUnknown->QueryInterface(IID_IAdder,
        ppvObject);
    // Toutes les autres interfaces sont non reconnues :
    return E_NOINTERFACE;
}
ULONG STDMETHODCALLTYPE CCalculator::InnerAddRef(void)
{
    m_ulRefCount++;
    return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CCalculator::InnerRelease(void)
{
    m_ulRefCount--;
    if (m_ulRefCount!=0) return m_ulRefCount;
    // On doit prévenir les réentrances éventuelles avant le code de
    // destruction. Ceci se fait classiquement en incrémentant
    // artificiellement le compteur de références :
    m_pUnknown->AddRef();
    delete this;
    return 0;
}
// Implémentation de IMultiplier :
HRESULT STDMETHODCALLTYPE CCalculator::Mul(long i, long j,
                                           long *pResult)
{
    *pResult=i*j;
    return ;
}
// Fonction de création de l'agrégat :
HRESULT CreateCalculator(REFIID iid, IUnknown *pOuterUnknown,
                         void **ppvObject)
{
    // Initialisation du  pointeur retourné :
    *ppvObject=0;
    // Vérification des paramètres pour l'agrégation :
    if (pOuterUnknown!=0 && iid!=IID_IUnknown)
        return CLASS_E_NOAGGREGATION;
    // Création de l'objet :
    CCalculator *pCalculator=new CCalculator(pOuterUnknown);
    pCalculator->Init();
    // Demande de l'interface désirée :
    HRESULT hResult=pCalculator->InnerQueryInterface(iid, ppvObject);
    // Fixe le compteur de référence (supprime le AddRef de la fonction
    // Init() :
    pCalculator->Release();
    return hResult;
}

Dans l'exemple précédent, la fonction membre Init de CCalculator passe le pointeur sur son interface externe IUnknown à la fonction de création de l'objet agrégé :

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

 
Sélectionnez
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.

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 :

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

 
Sélectionnez
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.

Créé le 9 juillet 2000  par Christian Casteyde

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.

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.

Créé le 9 juillet 2000  par Christian Casteyde

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

 
Sélectionnez
struct IClassFactory : 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 Comment gère-t-on la durée de vie des serveurs ?. 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 :

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

 
Sélectionnez
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

 
Sélectionnez
#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;
}
Créé le 9 juillet 2000  par Christian Casteyde

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

 
Sélectionnez
// Fabrique de classe pour le composant Adder :
static unsigned long ulObjectCount=0;
static unsigned long ulLockCount=0;
class CAdderFactory : public IClassFactory
{
    unsigned long m_ulRefCount;
public:
    // 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::CAdderFactory(void) : m_ulRefCount(0)
{
    return ;
}
HRESULT STDMETHODCALLTYPE CAdderFactory::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::AddRef(void)
{
    m_ulRefCount++;
    return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdderFactory::Release(void)
{
    m_ulRefCount--;
    if (m_ulRefCount!=0) return m_ulRefCount;
    delete this;
    return 0;
}
// Les méthodes de IClassFactory :
HRESULT STDMETHODCALLTYPE CAdderFactory::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::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.

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

Créé le 9 juillet 2000  par Christian Casteyde

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 :

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

 
Sélectionnez
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.

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 :

 
Sélectionnez
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.

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.

Créé le 9 juillet 2000  par Christian Casteyde

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

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

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

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

 
Sélectionnez
ULONG STDMETHODCALLTYPE CCalculator::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 :

 
Sélectionnez
ulObjectCount++;
ObjectDestroyed();

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

Créé le 9 juillet 2000  par Christian Casteyde

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

 
Sélectionnez
#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 : 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:
    // 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 : public IClassFactory
{
    unsigned long m_ulRefCount;
public:
    // 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

 
Sélectionnez
#include <windows.h>
#include <objbase.h>
#include "AdderPrx/Adder.h"
#include "adder.h"
//////////////////////////////////////////////////////////
//      Implémentation du composant Adder               //
//////////////////////////////////////////////////////////
// Constructeur :
CAdder::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::Init(void)
{
    return NOERROR;
}
// Les méthodes de IUnknown appellent les fonctions
// de l'interface interne ou celles de l'interface externe
// de l'agrégat :
HRESULT STDMETHODCALLTYPE CAdder::QueryInterface(REFIID iid,
                                                 void **ppvObject)
{
    return m_pUnknown->QueryInterface(iid, ppvObject);
}
ULONG STDMETHODCALLTYPE CAdder::AddRef(void)
{
    return m_pUnknown->AddRef();
}
ULONG STDMETHODCALLTYPE CAdder::Release(void)
{
    return m_pUnknown->Release();
}
// Les méthodes de IInnerUnknown gèrent la durée de vie du composant :
HRESULT STDMETHODCALLTYPE CAdder::InnerQueryInterface(
    REFIID iid, void **ppvObject)
{
    // Initialisation du pointeur :
    *ppvObject=0;
    // Obtient le pointeur sur l'interface demandée :
    // Le test suivant ne peut être vérifié que dans deux cas :
    // - soit l'objet n'est pas aggrégé ;
    // - soit il est aggrégé et la fonction de création demande
    // l'interface IUnknown interne pour l'aggrégat.
    // Dans les deux cas on doit renvoyer un pointeur
    // sur l'interface IInnerUnknown :
    if (iid==IID_IUnknown)
        *reinterpret_cast<IInnerUnknown **>(ppvObject)=
        static_cast<IInnerUnknown *>(this);
    // Les tests suivants ne sont exécutés que lorsque l'objet
    // n'est pas agrégé ou lorsque la fonction QueryInterface
    // de l'agrégat délègue son travail :
    else if (iid==IID_IAdder)
        *reinterpret_cast<IAdder **>(ppvObject)=
        static_cast<IAdder *>(this);
    else if (iid==IID_IOpposite)
        *reinterpret_cast<IOpposite **>(ppvObject)=
        static_cast<IOpposite *>(this);
    if (*ppvObject==0) return E_NOINTERFACE;
    // Si l'interface est gérée, fixe le compte de références. Dans le
    // cas des objets agrégés, on doit appeler AddRef pour l'agrégat.
    // Dans les autres cas, ainsi que dans le cas de la création de
    // l'objet agrégé dans un agrégat, on doit appeler AddRef de l'objet
    // agrégé. Dans tous les cas, on peut appeler directement AddRef sur
    // l'interface qui a été renvoyée :
    reinterpret_cast<IUnknown *>(*ppvObject)->AddRef();
    return NOERROR;
}
ULONG STDMETHODCALLTYPE CAdder::InnerAddRef(void)
{
    m_ulRefCount++;
    return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdder::InnerRelease(void)
{
    m_ulRefCount--;
    if (m_ulRefCount!=0) return m_ulRefCount;
    // 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::Add(long i, long j, long *pResult)
{
    *pResult=i+j;
    return NOERROR;
}
HRESULT STDMETHODCALLTYPE CAdder::Sub(long i, long j, long *pResult)
{
    *pResult=i-j;
    return NOERROR;
}
HRESULT STDMETHODCALLTYPE CAdder::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::CAdderFactory(void) : m_ulRefCount(0)
{
    return ;
}
HRESULT STDMETHODCALLTYPE CAdderFactory::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::AddRef(void)
{
    m_ulRefCount++;
    return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CAdderFactory::Release(void)
{
    m_ulRefCount--;
    if (m_ulRefCount!=0) return m_ulRefCount;
    delete this;
    return 0;
}
// Les méthodes de IClassFactory :
HRESULT STDMETHODCALLTYPE CAdderFactory::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::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

 
Sélectionnez
// 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 : 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:
    // 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 : public IClassFactory
{
    unsigned long m_ulRefCount;
public:
    // 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

 
Sélectionnez
#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::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::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::~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::QueryInterface(REFIID iid,
                                                      void **ppvObject)
{
    return m_pUnknown->QueryInterface(iid, ppvObject);
}
ULONG STDMETHODCALLTYPE CCalculator::AddRef(void)
{
    return m_pUnknown->AddRef();
}
ULONG STDMETHODCALLTYPE CCalculator::Release(void)
{
    return m_pUnknown->Release();
}
HRESULT STDMETHODCALLTYPE CCalculator::InnerQueryInterface(REFIID iid,
                                                           void **ppvObject)
{
    *ppvObject=0;
    if (iid==IID_IUnknown)
        *reinterpret_cast<IInnerUnknown **>(ppvObject)=
        static_cast<IInnerUnknown *>(this);
    else if (iid==IID_IMultiplier)
        *reinterpret_cast<IMultiplier **>(ppvObject)=
        static_cast<IMultiplier *>(this);
    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::InnerAddRef(void)
{
    m_ulRefCount++;
    return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CCalculator::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::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::CCalculatorFactory(void) : m_ulRefCount(0)
{
    return ;
}
HRESULT STDMETHODCALLTYPE CCalculatorFactory::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::AddRef(void)
{
    m_ulRefCount++;
    return m_ulRefCount;
}
ULONG STDMETHODCALLTYPE CCalculatorFactory::Release(void)
{
    m_ulRefCount--;
    if (m_ulRefCount!=0) return m_ulRefCount;
    delete this;
    return 0;
}
// Les méthodes de IClassFactory :
HRESULT STDMETHODCALLTYPE CCalculatorFactory::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::Init() :
        pCalculator->Release();
    }
    if (FAILED(hResult))
    {
        delete pCalculator;
        ObjectDestroyed();
    }
    return hResult;
}
HRESULT STDMETHODCALLTYPE CCalculatorFactory::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

 
Sélectionnez
#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;
}

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.

Créé le 9 juillet 2000  par Christian Casteyde

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.

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.

  • 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.
Créé le 9 juillet 2000  par Christian Casteyde

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 bibliothèque 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.

Créé le 9 juillet 2000  par Christian Casteyde

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

 
Sélectionnez
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.

Créé le 9 juillet 2000  par Christian Casteyde

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 :

 
Sélectionnez
[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 :

 
Sélectionnez
[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 :

 
Sélectionnez
[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 :

 
Sélectionnez
[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:

 
Sélectionnez
[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 :

 
Sélectionnez
[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 :

 
Sélectionnez
[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}"
Créé le 9 juillet 2000  par Christian Casteyde

Les sources présentées sur cette page sont libres de droits et vous pouvez les utiliser à votre convenance. Par contre, la page de présentation constitue une œuvre intellectuelle protégée par les droits d'auteur. Copyright © 2013 Developpez Developpez LLC. Tous droits réservés Developpez LLC. Aucune reproduction, même partielle, ne peut être faite de ce site ni de l'ensemble de son contenu : textes, documents et images sans l'autorisation expresse de Developpez LLC. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.