Les domaines d'application

Un article de Mangue.org, l'encyclopéde libre.

Dans la plate-forme .NET, les domaines d'application représententun environnement dans lequel les applications s'exécutent. Chaque application s'exécutant dans son domaine, cela permet de les isoler les unes des autres. On parle aussi d'AppDomains.


Sommaire

Isolation des programmes

En général, les différents programmes tournant sur une même machine sont isolés les uns des autres par le fait qu'ils s'exécutent chacun dans un processus différent. L'isolation est dûe au fait que les adresses mémoire sont relatives au processus. En d'autre termes, un pointeur sur un espace mémoire qu'on passerait d'un processus à un autre ne servirait à rien, puisqu'il ne pointerait alors plus sur l'espace mémoire voulu. De la même façon, il n'est pas possible de faire des appels de fonctions directement d'un processus à un autre, le pointeur sur fonction étant invalidé par le changement de processus.
Pour les programmes de la plate-forme .NET, cette isolation est fournie par les domaines d'application.

Image:Cours1-00.gif
L'isolation des applications est en général réalisée grâce aux processus

Image:Cours1-01.gif
Avec .NET, cette isolation est réalisée grâce aux domaines d'application

Fonctionnement et avantages

Avant de pouvoir être exécuté, le code managé doit être vérifié. (à moins que l'administrateur n'aie donné la permission de passer outre cette procédure) Cette vérification sert à déterminer si le code peut tenter d'accéder à des adresses mémoire invalides ou effectuer des opérations de nature à empêcher son processus hôte de fonctionner correctement. Le code qui passe ce test avec succès est dit sécurisé. (type-safe) Le fait de vérifier que le code est sécurisé permet au CLR de fournir un niveau d'isolation équivalent à celui qu'offrent les processus, tout en mobilisant beaucoup moins de ressources.
On peut donc dire que les domaines d'application sont en fait les "unités d'isolation" utilisées par le CLR pour organiser la séparation des programmes. Chacun d'eux s'exécute dans un domaine. Plusieurs domaines peuvent coexister dans le même processus, ce qui permet, tout en conservant le même niveau d'isolation qu'avec les processus, de simplifier les appels inter-processus (qui sont maintenant des appels inter-domaines) et les changements de processus. (qui sont maintenant des changements de domaine)
Isoler les applications est également important en termes de sécurité. Par exemple, avec ce système, il est possible d'exécuter des contrôles appartenant à plusieurs applications web dans un seul processus navigateur de façon à ce qu'aucun des contrôles ne puisse accéder aux ressources et aux données de autres, tout en les faisant cohabiter dans la même application. Autrement dit, cela permet de faire de l'isolation à l'intérieur même des processus.
Pour résumer, voici les avantages qu'apporte l'isolation fournie par les domaines d'application:
  • Si une application fonctionne anormalement et plante, les autres applications continuent à fonctionner correctement comme si de rien n'était. En effet, le code sécurisé ne pouvant provoquer d'erreurs mémoire, l'utilisation des domaines assure que le code qui s'exécute dans un domaine ne peut affecter les autres applications du même processus, celles-ci tournant dans d'autres domaines d'application.
  • Les applications peuvent être stoppées de manière individuelle, sans pour autant avoir à arrêter tout le processus, car il est possible de stopper un domaine uniquement.
  • Un code qui s'exécute dans une application ne peut directement accéder au code et aux ressources d'une autre application. Le CLR oblige cette isolation en empêchant les appels directs entre objets appartenant à des domaines d'applications différents. Les objets qui passent entre domaines sont soit copiés, soit accédés par proxy. Si l'objet est copié, l'appel devient local, puisque l'appelant et l'appelé font, après la copie, partie du même domaine. Si l'objet est appelé via un proxy, il s'agit d'un appel distant. Dans ce cas, l'appelant et l'appelé sont dans des domaines différents. Les appels inter-domaines utilisent la même procédure que les appels entre deux processus ou entre deux machines. Ainsi, les métadonnées de l'objet appelé doivent être accessibles par les deux domaines afin que l'appel à la méthode puisse être compilé correctement par le compilateur JIT. Si le domaine appelant n'a pas accès aux métadonnées, la compilation peut éventuellement échouer, levant une exception de type System.IO.FileNotFound.
  • Le comportement du code est géré par l'application dans laquelle il s'exécute. En d'autre termes, le domaine d'application fournit les paramètres de configuration, tout comme la politique de gestion des versions de l'application, l'endroit où trouver les assemblages qu'il accède, et l'endroit ou trouver les assemblages chargés dans le domaine. Les permissions allouées au code peuvent être contrôlées par le domaine d'application dans lequel le code s'exécute.


Domaines d'application et assemblages

Avant de pouvoir exécuter une application, un assemblage doit être chargé dans le domaine. En règle générale, le fait de lancer un programme implique le chargement de plusieurs assemblages. Par défaut, le CLR charge l'assemblage dans le domaine qui contient le code qui référence cet assemblage. De cette façon, le code et les données de l'assemblage sont isolés pour l'application qui les utilise.
Si un assemblage est utilisé par plusieurs domaines dans un processus, le code de l'assemblage (mais pas ses données) peut être partagé par tous les domaines qui la référencent. Cela réduit l'utilisation mémoire.

Un assemblage est dit "indépendant du domaine" (domain-neutral) si son code peut être partagé par tous les domaines dans le processus. C'est le runtime host qui décide de charger les assemblages comme indépendants du domaine au moment de leur chargement dans le processus.

Il existe trois politiques à ce sujet:
  • Ne charger aucun assemblage comme indépendant du domaine, excepté Mscorlib, qui est toujours chargé comme domain neutral. C'est ce qu'on appelle le domaine seul, (single domain) car c'est ce qui se passe lorsque le runtime host n'exécute qu'une seule application dans un processus.
  • Charger tous les assemblages comme indépendants du domaine. Cela est de rigueur quand plusieurs domaines d'application appartenant au même processus exécutent le même code.
  • Charger les assemblages à nom fort comme indépendants du domaine. Cette politique est utilisée quand il y a plus d'un application dans le même pocessus.

Charger un assemblage comme indépendant du domaine consomme des ressources. En effet, même si cela épargne de la place en mémoire, cela se fait au prix d'une vitesse d'exécution diminuée si l'assemblage contient des méthodes ou données statiques accédées fréquemment. L'accès au données ou méthodes statiques est plus lent à cause du besoin d'isoler les applications, puisque chaque domaine qui accède à l'assemblage doit avoir sa propre copie de ces méthodes et données statiques, afin d'éviter que les références soient les même entre des domaines différents. Ainsi le runtime doit effectuer plus d'opérations pour diriger un appelant vers la bonne copie de la donnée ou méthode statique, ce qui ralentit l'appel.
Un runtime host peut définir une politique de sécurité au niveau des domaines, qui peut être différente pour chaque domaine. Si deux domaines ont un niveau de sécurité différent, aucun assemblage ne pourra être partagé entre eux.


Domaines d'application et threads

"Un domaine" ne signifie pas "un thread". Il peut y avoir plusieurs threads dans le même domaine, et ajoutons qu'un thread n'est pas confiné à un domaine. Ceux-ci sont libres de traverser les limites des domaines. Evidemment, à un instant donné, chaque thread s'exécute dans un domaine bien précis. C'est le runtime qui tient à jour les correspondances thread - domaine.

Image:cours1-02.gif
Les threads ne sont pas enfermés dans un domaine d'application


Programmer les domaines d'application

En règle générale, on laisse la gestion des domaines d'applications au runtime host. Cela dit, il est possible de manipuler les appdomains au sein d'une application pour, par exemple, décharger un composant de la mémoire sans avoir à stopper l'application toute entière.
Les méthodes relatives aux domaines d'appilcation font partie de la classe AppDomain, présente dans le namespace System.
Nous nous baserons sur une série d'exemples destinés à résoudre des problèmes précis plutôt que sur une étude complète et approfondie de la classe, car on ne l'utilise en général que pour des opérations bien définies.


Créer un domaine

Il existe quatre manières différentes de créer un AppDomain, suivant que vous désirez spécifier un nombre important de paramètres ou non. Ces quatre manières sont en fait quatre versions surchargées de la méthode CreateDomain.
Voyons dès maintenant la version la plus simple. Celle-ci ne prend qu'une string en paramètre, qui va définir le nom "humain" du domaine. Ce nom est en fait une façon d'identifier simplement le domaine. Ce constructeur lèvera une ArgumentNullException si vous passez null à la place de la chaine attendue.

monDomaine = AppDomain.CreateDomain("Le Domaine des Dieux");

La deuxième version de la méthode prend en compte la notion de sécurité. En effet, celle-ci prend toujours une chaine de caractères en premier paramètre, mais à cela vient s'ajouter en deuxième paramètre un objet de type Evidence, qui a justement pour objet la sécurité. (namespace System.Security.Policy) Cette classe contient en fait les données qui définissent la politique à appliquer quant à la sécurité du code.
Avant d'utiliser la classe Evidence, arrêtons-nous quelques instants afin de présenter rapidement le fonctionnement de la sécurité. Lorsqu'un code est chargé dans un domaine, celui-ci est lié à un assemblage. L'assemblage lui-même contient certaines informations, comme sa provenance, une éventuelle clé cryptographique, etc. Il contient également les permissions minimum dont il a besoin pour s'exécuter. Ainsi, avant d'exécuter le code, le CLR regarde si la politique de sécurité et la provenance du code lui permettent de lui accorder les permissions minimum qu'il requiert. Si c'est le cas, il lui accorde les permissions autorisées par la politique, moins celles dont le code n'a pas besoin, puis exécute le code. Par contre, si les permissions minimum demandées par le code ne peuvent lui être accordées, il ne sera pas exécuté.
Cette procédure peut devenir très complexe, surtout quand un assemblage en appelle un autre, qui en appelle un autre, etc. Le CLR doit parcourir toute cette hiérarchie pour déterminer ce que l'assemblage peut faire. Cette procédure est appelée "stack walking". Si un des assemblages ne peut se voir accorder les permissions demandées, la "stack walk" échoue et le CLR lève une exception de sécurité.
Ici, l'objet Evidence que nous passons en paramètre va permettre de contrôler la politique de sécurité à appliquer au niveau du domaine. Il comporte deux méthodes principales, que sont AddHost et AddAssembly. Créons un objet Evidence:

Evidence preuve = new Evidence();
preuve.AddHost( new Url("http://www.microapp.com") );
preuve.AddAssembly("monAssembly");

Ce code appelle le contructeur de Evidence, puis y ajoute les preuves du site www.microapp.com. Cela signifie que le code qui sera exécuté dans ce domaine n'aura pas plus de permissions qu'un code qui viendrait de ce site. Nous ajoutons ensuite les preuves d'un assemblage, ce qui fait que le code qui s'exécutera dans ce domaine n'aura pas plus de permissions qu'un code qui serait lié à cet assemblage.
Nous pouvons maintenant créer notre domaine:

monDomaine = AppDomain.CreateDomain("Le Domaine des Dieux", preuve);

La troisième version de la méthode prend en paramètre supplémentaire un objet AppDomainSetup, qui permet de spécifier la configuration du domaine. Grâce à lui, nous pouvons configurer la gestion du cache de l'application, spécifier certains répertoires, et mêmes configurer la politique d'optimisation. Nous ne rentrerons pas dans les détails, car les paramètres par défaut ont rarement besoin d'être modifiés.
En général, on se contente de spécifier le répertoire de l'application, et éventuellement de charger un fichier de configuration:

AppDomainSetup config = new AppDomainSetup();
config.ApplicationBase = "repertoire de l'application";
config.ConfigurationFile = "fichier de configuration"; 

C'est dans le répertoire qu'on a spécifié que le domaine ira chercher lorsqu'il voudra charger un assemblage.
La création du domaine devient alors:

AppDomain monDomaine = AppDomain.CreateDomain("Le Domaine des Dieux", preuve, config);

La quatrième et dernière version de la méthode de création prend comme deux premiers paramètres le nom du domaine et un objet Evidence. Suivent deux chaines de caractères, qui spécifient le répertoire de l'application (où seront donc cherchés les assemblages) et un chemin relatif au premier, où le programme ira chercher les éventuels assemblages privés. Pour finir, la méthode prend un booléen qui indique si oui on non une copie des assemblages chargés sera faite à l'intérieur du domaine.
Cela donnera par exemple:

AppDomain monDomaine = AppDomain.CreateDomain("Le Domaine des Dieux",
                                    preuve, "repertoire", "../un autre repertoire", true);


Utiliser un domaine

Maintenant que nous savons créer un domaine, voyons les deux principales méthodes qui permettent de l'utiliser.
Tout d'abord, la méthode CreateInstanceFrom. Elle permet de charger une instance d'un type défini dans un assemblage. En effet, si aucun assemblage n'a été chargé, il faudra, avant de pouvoir utiliser un type de données, charger ses spécificités dans le domaine. Tout comme la méthode de création de domaine, celle-ci comporte plusieurs versions.

La version la plus simple prend deux chaines de caractères en paramètre: la nom du fichier qui contient l'assemblage, et le nom du type à charger:

ObjectHandle obj = monDomaine.CreateInstanceFrom( "monAssembly.dll", "monNamespace.maClasse" );
maClasse mc = (maClasse) obj.Unwrap();

Vous remarquerez que le nom de type doit être qualifié, c'est à dire que si le type fait partie d'un namespace, celui-ci doit apparaitre. Cette méthode peut lever une ArgumentNullException si un des deux paramètre est à null.
Cette méthode retourne un ObjectHandle, qui n'est pas directement utilisable. En effet, à ce stade, l'objet n'est pas encore chargé dans le domaine. C'est uniquement lorsque nous aurons appelé sa méthode Unwrap que nous obtiendrons un Object, que nous convertirons dans le type voulu.
Cela permet de préparer ses types à l'avance, mais de ne les charger dans le domaine qu'au moment voulu.

Voyons à présent la méthode Load. Son objectif est de charger un assemblage dans le domaine. Comme il est possible de trouver un assemblage sous des formats différents, elle a été surchargée pour garantir qu'on puisse se servir d'un assemblage quel que soit la forme sous laquelle on le rencontre. Nous ne traiterons ici qu'un jeu de surcharges, les autres étant identiques.

Assembly asm = monDomaine.Load("System.Web, Version=1.0.3300.0,
                                       Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a ");

Ici, nous avons passé en paramètre le nom de l'assembly. La méthode va donc ouvrir le répertoire qui lui a été indiqué lors de la création du domaine, et va chercher l'assemblage System.Web. Attention, le nom passé en paramètre n'est pas le nom de fichier, mais le "display name" de l'assemblage. Il est constitué du nom simple de l'assembly, (ici "System.Web") du numéro de version, des informations de culture, et de la clé publique si l'assemblage a un nom fort. Par exemple, le display name de System.Web est "System.Web, Version=1.0.3300.0, Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a".
Nous pouvons aussi, en plus du nom de l'assembly, passer un objet Evidence afin de fournir des preuves supplémentaires à celles contenues dans l'assembly:

Assembly asm = monDomaine.Load("System.Web, Version=1.0.3300.0, 
                                       Culture=neutral, PublicKeyToken=b03f5f7f11d50a3a", preuve);


Détruire un domaine

La fermeture d'un domaine possède l'avantage, si on a réparti une application sur plusieurs d'entre eux, de pouvoir décharger une partie d'un programme de la mémoire sans pour autant stopper l'exécution de ce programme.
Le fermeture d'un domaine est le fruit de la méthode Unload, qui prend en unique paramètre l'AppDomain à détruire. Elle peut lever deux types d'exception:
  • ArgumentNullException: si le handle sur une instance de la classe AppDomain passé en paramètre est en fait à null.
  • CannotUnloadAppDomainException: si le domaine passé en paramètre ne peut être détruit.

Il peut arriver que la fermeture du domaine prenne du temps, car il est parfois difficile de terminer les threads qui s'exécutent.
Si le thread qui a appelé Unload s'exécute dans le domaine à fermer, un autre thread sera démarré pour mener l'opération à bien. Dans ce cas, si Unload lève une exception, c'est ce thread, et pas le thread de départ, qui la recevra.
Outils personels