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 

 
OuvrirSommaireFonctionnement de DCOM

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

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

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

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

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

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

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

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

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

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

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

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

 
Sélectionnez
HRESULT STDAPICALLTYPE CoRegisterMessageFilter ( LPMESSAGEFILTER lpFilter , LPMESSAGEFILTER **lppPrevious );

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

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

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

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

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

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

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

La fonction HandleIncomingCall peut renvoyer l'une des trois valeurs suivantes, selon l'action à prendre : SERVERCALL_ISHANDLED, si la requête est acceptée. La méthode de l'objet visé sera donc appelée normalement ; SERVERCALL_REJECTED, si la requête ne peut pas et ne sera jamais acceptée. Le client recevra une erreur en retour RPC_E_CALL_REJECTED et ne devra pas faire une nouvelle tentative (elle serait vouée à l'échec) ; SERVERCALL_RETRYLATER, qui indique que le serveur est momentanément occupé, mais que le client peut tenter à nouveau d'effectuer la requête plus tard.

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

  • -1 si l'appel doit être abandonné. Dans ce cas, le client recevra le code d'erreur RPC_E_CALL_REJECTED
  • une valeur comprise entre 0 et 99 inclus, ce qui signifie que la requête doit être refaite immédiatement
  • une valeur supérieure à 100, qui indique le temps en millisecondes avant que la requête de soit à nouveau tentée

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

  • PENDINGTYPE_TOPLEVEL, qui indique que l'appel se faisait dans un cadre normal, sans interblocages ;
  • PENDINGTYPE_NESTED, qui indique que l'appel bloquant en cours prend part à un interblocage.

Les valeurs de retour possible indiquent quel action prendre :

  • PENDINGMSG_CANCELCALL, qui permet d'annuler l'appel bloquant en cours et de traiter le message suivant de la file des messages. Le code d'erreur RPC_E_CALL_CANCELED sera renvoyé au client ;
  • PENDINGMSG_WAITNOPROCESS, qui indique d'attendre sans traiter le message. Celui-ci n'est cependant pas détruit, il sera traité lorsque l'appel bloquant se terminera ;
  • PENDINGMSG_WAITDEFPROCESS, qui indique de traiter les messages de commutation de tâches et de changement de fenêtre, ainsi que les messages de rafraîchissement de l'affichage. Tous les autres messages sont détruits.
Créé le 9 juillet 2000  par Christian Casteyde

Oui.

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

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

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

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

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

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

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

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

  • passer toutes les machines en contrôle des ressources au niveau utilisateur
  • donner les droits d'utiliser les composants aux utilisateurs

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

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

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

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

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

  • il faut impérativement donner les droits d'accès et d'exécution au système sur le composant, faute de quoi des erreurs étranges (E_OUTOFMEMORY) surviendront et le composant sera inutilisable ;
  • il faut prendre garde au compte dans lequel le composant sera exécuté. En effet, il prendra les droits du compte dans lequel il est utilisé. En pratique, il vaut mieux toujours exécuter le composant dans le compte de l'utilisateur interactif de la machine du poste client du composant. Ce compte porte le nom de compte de l'utilisateur exécutant. En revanche, si l'on cherche à faire en sorte que le composant affiche des fenêtres sur le poste, il est nécessaire d'exécuter le composant dans le contexte du compte de l'utilisateur interactif du poste sur lequel ce composant est lancé.
  • s'il faut avoir les droits suffisants pour exécuter les composants à distance, il faut également donner les droits nécessaires à ces composants pour qu'ils puissent émettre des événements vers leurs clients. Ceci signifie qu'il faut configurer également les postes clients pour que le compte dans lequel les composants répartis s'exécutent soit capable d'accéder aux machines clientes.

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

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

  • le processus doit appeler CoInitializeSecurity pour indiquer les mécanismes de sécurité qu'il désire utiliser. Le processus par défaut fourni par DCOM utilise les informations placées dans la base de registres pour spécifier la politique de sécurité qu'il applique
  • créer un composant gérant l'interface ISurrogate. Cette interface est utilisée par DCOM pour demander au surrogate process de charger et de décharger dynamiquement les DLL à prendre en charge
  • enregistrer ce composant au niveau de DCOM à l'aide de la fonction CoRegisterSurrogate
  • créer une boucle des messages dans laquelle la fonction CoFreeUnusedLibraries est appelée régulièrement. Cette étape est nécessaire pour que les serveurs DLL pris en charge puissent être déchargés lorsque plus personne ne les utilise, et pour que le processus puisse déterminer quand il peut se terminer

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

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

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

  • interrogation de l'objet sur les interfaces qu'il utilise pour signaler les événements
  • initialisation de la connexion par le client, ce dernier fournissant un pointeur sur l'interface désirée à l'objet connectable
  • terminaison de la connexion par le client, ce qui libère l'interface détenue par l'objet connectable

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

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

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

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

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

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

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

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

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

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

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

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

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

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

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

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

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

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

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

  • premièrement, elles doivent hériter de l'interface IDispatch. C'est ce qui permet de les utiliser polymorphiquement comme des dispinterfaces
  • deuxièmement, elles ne doivent utiliser que des types compatibles avec OLE Automation. Ceci revient à dire que les méthodes de ces interfaces ne peuvent utiliser que les types admis par le type VARIANT
  • troisièmement, elles doivent être déclarées avec l'attribut dual pour indiquer à MIDL qu'il s'agit d'interface duales

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

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

Créé le 9 juillet 2000  par Christian Casteyde

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

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

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

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

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

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

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.