IdentifiantMot de passe
Loading...
Mot de passe oublié ?Je m'inscris ! (gratuit)
logo
Sommaire > Multithreading, apartment et marshalling
        Comment le multithreading est-il géré ?
        Qu'est-ce qu'un appartement (apartment) ?
        Qu'est-ce qu'un composant free-threaded ?
        Comment DCOM effectue-t-il les appels de méthodes entre deux appartements ?
        Qu'est-ce que le marshalling ?
        Que sont les stublets et les facelets ?
        Comment les interfaces sont-elles marshallées ?
        Comment est gérée la comptabilisation des références sur les interfaces en cours de marshalling ?
        Comment utiliser son propre marshalling ?
        Comment marshaller une interface d'un thread à un autre ?
        Quelles sont les registres à rajouter pour enregistrer les proxies et les stubs ?



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

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

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

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


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

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

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


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

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

info Le modèle d'appartement monothreadé est souvent dénommé apartment threaded. C'est un abus de langage, car en réalité, tous les composants sont dans un appartement. La différence porte uniquement sur le fait qu'il puisse y avoir plusieurs threads dans le même appartement, ou non.
En réalité, les appartements monothreadés ne sont qu'un cas particulier d'appartements multithreadé, pour lesquels il n'y a qu'un seul thread...

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

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

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

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

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

Tableau 3. Modèles de threading des composants

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

info D'après ce qui vient d'être dit, les composants qui ne gèrent que le modèle Free ne peuvent pas être utilisés directement à partir d'un appartement monothreadé. En théorie, les composants qui gèrent le modèle multithreadé pourraient être utilisés directement par les threads des appartements monothreadés, car qui peut le plus peut le moins. Cependant, certains composants utilisent un marshalling personnalisé, et le proxy de ces serveurs peut effectuer des opérations particulières qui imposent de l'utiliser. Ce cas est très rare mais impose d'utiliser le marshalling même pour l'accès aux objets d'un appartement multithreadé à partir d'un appartement monothreadé. De plus, le composant multithreadé n'est pas tenu de rappeler les interfaces de rappel des clients sur le même thread. Les clients monothreadés ne peuvent tolérer ce comportement, ce qui impose encore une fois l'utilisation du marshalling entre les composants serveurs multithreadés et les clients monothreadés. Les composants multithreadés qui garantissent que les appels sur les interfaces de rappel des clients monothreadés ne se font que dans le contexte du client n'ont pas besoin d'utiliser le mécanisme du marshalling. Dans ce cas, il peuvent être déclaré Both.
Les serveurs in-process qui utilisent le modèle de threading Both sont les plus performants, puisque les mécanismes de marshalling ne sont pas nécessaires entre leurs composants et leurs clients. Tous les appels de méthodes sont donc réalisés directement. Cependant, leur écriture est nettement plus technique, car il faut s'assurer qu'aucun thread ne fait des notifications sur des composants monothreadés qui n'utilisent pas ce thread. Les composants qui ne créent pas de threads additionnels sont à ranger dans cette catégorie, puisque le seul thread qu'ils peuvent utiliser est celui de leur client monothreadé.

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

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

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

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

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

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

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

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

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

  • appeler CoInitialize ou CoInitializeEx au début de chaque thread qui utilise DCOM
  • pour chaque thread s'exécutant dans un appartement monothreadé, il faut écrire une boucle des messages si l'on veut que les objets créés dans ce thread puisse recevoir des appels provenant d'un autre appartement
  • aucun pointeur sur une interface ne doit être communiquée directement à un thread d'un autre appartement, quel qu'il soit. Si un pointeur sur une interface d'un appartement monothreadé doit être utilisé en dehors de cet appartement, il faut soit l'obtenir à l'aide des mécanismes standard de DCOM, soit le marshaller manuellement
  • pour les objets des appartement multithreadés, il faut s'assurer de la gestion correcte des accès concurrent. Ceci se fait grâce événements, aux sections critiques, aux mutex ou aux sémaphores du système
info DCOM ne protège contre les accès concurrents que les composants d'un appartement monothreadé, pas les serveurs eux-mêmes. Ceci signifie que même les serveurs utilisant le modèle d'appartement monothreadé doivent malgré tout être capables de gérer les concurrences d'accès. En effet, ils peuvent être appelés par plusieurs threads de plusieurs appartements, et permettre ainsi la création d'objets dans plusieurs appartements. Les points sur lesquels il faudra faire attention sont les variables globales (décomptes de références et de blocage du serveur, accessibles par plusieurs threads du client), la gestion de création des objets, et l'implémentation de DllCanUnloadNow (le serveur ne doit pas être détruit tant qu'un objet existe dans un quelconque appartement).

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

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

Les mécanismes du marshalling sont détaillés dans la question Que sont les stublets et les facelets ?


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

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

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

info Le langage IDL de Microsoft est un dérivé du langage IDL des RPC DCE. Bien qu'ayant également pour vocation de décrire des interfaces et des fonctions distantes, ce n'est pas le même langage que le langage IDL de CORBA.
Par conséquent, à moins qu'on n'implémente soi-même le marshalling pour chaque composant, ainsi que les proxies capables de communiquer avec ces composants, il est impératif de définir complètement les interfaces de ses composants dans le langage IDL. Ce langage sera décrit plus loin.


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

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

  • premièrement, DCOM cherche à déterminer si le composant gère lui-même son propre marshalling. Pour cela, il demande s'il gère l'interface IMarshall. Si ce n'est pas le cas, le marshalling standard de DCOM sera utilisé. Il charge alors un stub générique dans l'espace d'adressage du serveur, qui gère l'interface IMarshall
  • ensuite, DCOM essaie de demander au composant le CLSID du proxy qu'il devra charger dans l'espace d'adressage du client (remarquez au passage le nombre de failles dans la sécurité du système qu'un tel mécanisme peut générer...). Si le composant est livré avec un proxy qui lui est spécifique, il peut permettre son utilisation à ce niveau. Dans le cas contraire, DCOM utilisera un proxy générique fourni par le mécanisme de marshalling standard. Quel que soit le proxy utilisé, celui-ci doit impérativement gérer l'interface IMarshall
  • DCOM demande au stub ou au composant lui-même s'il gère son propre marshalling les informations qui seront nécessaires au proxy pour se connecter à lui. Ces informations sont récupérées par l'intermédiaire de l'interface IMarshall
  • les informations de connexions sont ensuite transmises au code du client qui a demandé une interface sur le composant (ce code peut très bien être CoGetClassObject, dont le client attend la terminaison)
  • ce code récupère le CLSID du proxy à utiliser, et le charge dans l'espace d'adressage du client. Il passe ensuite les informations de connexions à ce proxy. Celui-ci établit la connexion avec le stub ou le composant à l'aide de ces informations
  • l'interface demandée par le client est ensuite récupérée sur le proxy, et est renvoyée. Le résultat net de l'opération est que le client peut utiliser directement l'interface qui lui est renvoyée : tous les appels seront redirigés vers le proxy. Celui-ci les transmettra au stub, qui réalisera l'appel sur le composant, ou directement au composant, si celui-ci gère son propre marshalling
Comme on le voit, l'interface IMarshall est au centre du processus de marshalling. Elle doit être implémentée aussi bien du côté client par le proxy, que du côté serveur, que ce soit par le stub générique de DCOM ou par le composant lui-même. IMarshall est déclarée comme suit :

interface IMarshal : IUnknown
{
    HRESULT GetUnmarshalClass(
        REFIID riid, void *pvInterface,
        DWORD dwDestContext, void *pvDestContext,
        DWORD mshlflags, CLSID *pclsid);
    HRESULT GetMarshalSizeMax(
        REFIID riid, void *pvInterface,
        DWORD dwDestContext, void *pvDestContext,
        DWORD mshlflags, DWORD *pcb);
    HRESULT MarshalInterface(
        IStream *pS, REFIID riid,
        void *pvInterface, DWORD dwDestContext,
        void *pvDestContext, DWORD mshlflags);
    HRESULT UnmarshalInterface(
        IStream *pS, REFIID riid, void **ppvInterface);
    HRESULT DisconnectObject(DWORD dwReserved);
    HRESULT ReleaseMarshalData(IStream *pS);
};
Seules les trois premières méthodes de cette interface sont utilisées par DCOM pour marshaller une interface. Les dernières méthodes ne sont destinées qu'à l'usage du proxy qui communiquera avec le stub. Le détail des arguments des méthodes de IMarshal n'est pas décrit ici, pour plus de renseignements consulter le SDK d'OLE. Il n'est en général par nécessaire d'utiliser directement l'interface IMarshall pour marshaller une interface. Les opérations de marshalling et de démarshalling sont en effet complètement prises en charge par les fonctions CoMarshallInterface et CoUnmarshallInterface.

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

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

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

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

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

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

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

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

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

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

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

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

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

info On notera que le marshalling se fait maintenant interface par interface, et non plus objet par objet (c'est à dire globalement au niveau du proxy et du composant lui-même). Ceci est nécessaire parce que DCOM ne peut pas déterminer a priori toutes les interfaces que l'objet serveur est capable de gérer. Les stublets et facelets sont donc chargés à la demande, lorsqu'une nouvelle interface doit être marshallée.
Il est essentiel que du point de vue du client le proxy se comporte comme un seul objet, à savoir le serveur qu'il représente. Ceci ne pose pas de problèmes dans le cas où le serveur gère son propre marshalling, car le proxy est alors écrit pour ce serveur. En revanche, pour le marshalling standard, il est nécessaire que les facelets soient toutes agrégées dans le proxy. Le proxy standard est donc constitué d'un nombre variable de ces composants (le terme facelet a été choisi par opposition au terme stublet du stub).

De même, DCOM stipule que deux pointeurs sur la même interface d'un objet doivent être égaux. Il est donc nécessaire que le proxy standard de DCOM réutilise la même facelet pour chaque pointeur sur une interface donnée. Il réalise ce contrôle au sein de sa fonction QueryInterface. Dans le mécanisme du marshalling, c'est au proxy d'établir la communication avec le serveur, à l'aide des données de marshalling obtenues via l'interface IMarshall du serveur. Toutefois, pour le marshalling standard est géré, cette connexion est prise en charge par la facelet générée par MIDL. Ceci ne change toutefois rien ni pour le client, ni pour le serveur, ni pour le reste de DCOM.

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


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

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

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

  • MSHLFLAGS_NORMAL, qui indique que le marshalling a lieu dans le cadre classique du passage d'un pointeur d'interface d'un appartement à un autre. Ce flag signifie que le proxy destination ne pourra utiliser qu'une seule fois ces données, et que la fonction CoReleaseMarshalData (qui elle-même appelle la méthode ReleaseMarshalData de l'interface IMarshal) sera appelée automatiquement. Il est donc inutile de l'appeler soi-même
  • MSHLFLAGS_TABLESTRONG, qui indique que les données de marshalling ne seront pas utilisées immédiatement a priori. Elles vont être stockées dans une table globale pour utilisation ultérieure. Ces données peuvent être utilisées plusieurs fois, afin d'obtenir plusieurs pointeurs sur l'interface ainsi marshallée. Ceci implique que l'appel à CoReleaseMarshalData n'est pas fait automatiquement après le démarshalling, c'est donc à l'utilisateur de faire cet appel lorsque les données de marshalling sont supprimées de la table globale. Par ailleurs, l'objet dont l'interface est marshallée est maintenu en mémoire par un appel à sa méthode AddRef, et sa présence est donc garantie tant que CoReleaseMarshalData n'est pas appelée
  • MSHLFLAGS_TABLEWEAK, qui permet exactement la même utilisation que MSHLFLAGS_TABLE_STRONG, à ceci près que l'objet source n'est pas bloqué en mémoire. Il est donc possible que les données de marshalling référencent un objet qui n'existe plus lorsqu'elles seront utilisées. Dans ce cas, le démarshalling échouera évidemment, et le client devra prendre les mesures appropriées. Notez que l'appel à CoReleaseMarshalData est toujours nécessaire

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

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

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

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

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

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

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


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

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

[HKEY_CLASSES_ROOT\Interface\{e3261620-0ded-11d2-86cc-444553540000}]
@="IAdder"
Cette sous-clé doit également contenir d'autres informations concernant l'interface. En particulier, il est nécessaire de définir une sous-clé nommée NumMethods dont la valeur est le nombre de méthodes de l'interfaces, et une sous-clé nommée ProxyStubClsid32, dont la valeur est le CLSID du composant capable d'effectuer le marshalling de cette interface. Pour l'interface IAdder, on devra donc avoir les entrées suivantes :

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

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


Consultez les autres F.A.Q's


Valid XHTML 1.0 TransitionalValid CSS!

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