Utilisation des AppDomain en .NET

Lainé Vincent

Dans cet article nous allons aborder la programmation en .NET avec les AppDomains. Nous verrons les raisons de cette technique de programmation ainsi que ses applications le tout au travers d'exemple concret. ♪

- Titre : Utilisation des AppDomain en .NET

- Auteur : Lainé Vincent

- Parution : 29 octobre 2005

- Mise à jour : 29 octobre 2005

- Licence : 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 Lainé Vincent. Aucune reproduction, même partielle, ne peut être faite de ce site et de l'ensemble de son contenu : textes, documents, images, etc. sans l'autorisation expresse de l'auteur. Sinon vous encourez selon la loi jusqu'à trois ans de prison et jusqu'à 300 000 € de dommages et intérêts.

Sommaire

- I. Introduction historique à la séparation des données d'une application

- I-A. Les processus

- I-B. La séparation des données dans le framework .NET

- II. Les AppDomain : Pourquoi et comment ?

- II-A. Pourquoi ?

- II-A-1. La sécurité

- II-A-2. Les performances et la mémoire

- II-B. Comment

- II-B-1. Mettre en œuvre les domaines d'application

- II-B-2. Les classes utilisées

- II-B-2-a. System.AppDomain

- II-B-2-b. System.AppDomainSetup

- II-B-2-c. System.Reflection.Assembly

- II-B-2-d. MonEspace.Loader

- III. Cas concret d'utilisation des domaines d'application

- III-A. Cas de la sécurisation des données

- III-B. Retour sur l'article de la création d'une application modulaire et amélioration de celui-ci

- III-C. Application autopatchable

- IV. Bibliographie et remerciements

- IV-A. Bibliographie

- IV-B. Remerciements

- IV-C. Téléchargement

I - Introduction historique à la séparation des données d'une application

Historiquement les données des applications, dans les systèmes monotâche, disposait de la totalité de la mémoire et des ressources de l'ordinateur pour s'exécuter. Or avec l'arrivée des systèmes multitâche, cette situation devint dangereuse pour le système et surtout pour les applications résidant en mémoire. En effet si chaque application se considère comme la seule dans le système, elle peut à n'importe quel moment décider d'écrire dans la zone mémoire d'une autre application. Pour pallier ce problème, la notion de processus et de séparation des données est apparue.

I-A - Les processus

Un processus est l'image mémoire d'un programme. Cette image contient les instructions du programme ainsi que les informations sur les zones mémoire qui sont et qui peuvent être utilisées par le processus.
À partir de là, la zone mémoire pour le processus ne correspond pas forcément (et même jamais) à la zone mémoire physique réelle, le système d'exploitation réalisant la transformation des adresses au moment de l'exécution.

I-B - La séparation des données dans le framework .NET

Dans le framework .NET la séparation des données par les processus est conservée, mais une autre couche de séparation des données a été introduite : les domaines d'application. Les domaines d'application fournissent le même niveau d'isolation que les processus, mais à un cout de performance bien moindre. Les domaines d'application sont contrôlés par l'interpréteur en temps réel en accord avec les règles de sécurités de la machine cible.

II - Les AppDomain : Pourquoi et comment ?

II-A - Pourquoi ?

II-A-1 - La sécurité

Les domaines d'application de par la séparation des données amènent une notion de sécurité supplémentaire. En effet si vous utilisez une librairie chargée dans un domaine d'application différent de l'application principale, vous êtes assuré que cette librairie ne pourra pas voir les données autres que celles que vous lui fournissez. Ainsi dans le cas de plugins, l'utilisation des domaines d'application vous permet d'être assuré que les plugins ne toucheront pas à autre chose que les données que vous lui fournissez.

II-A-2 - Les performances et la mémoire

L'utilisation des domaines d'application peut dans une certaine mesure améliorer les performances de votre application. En effet toujours dans le cas des plugins, si vous chargez 200 plugins pour un traitement spontané, une fois ce traitement fini ils restent en mémoire tant que l'application n'est pas stoppée. Si vous utilisez un domaine d'application pour charger ces plugins vous pouvez à la fin du traitement le décharger et ainsi libérer la mémoire utilisée par les plugins.

II-B - Comment

II-B-1 - Mettre en œuvre les domaines d'application

Les domaines d'application ne sont pas si compliqués à utiliser que peut le laisser croire une lecture de la documentation officielle. Nous allons ici nous attacher à expliquer le cheminement à faire pour mettre en œuvre les domaines d'application.
Tout d'abord vous devez comprendre que l'utilisation des domaines d'application ne remet pas en cause tout ce que vous connaissez sur la programmation.

L'utilisation des domaines d'application se fait en trois temps.
- 1°) Vous devez créer le nouveau domaine d'application et y charger une assembly de « chargement ». Cela est nécessaire, car ainsi vous ne liez pas les assemblies chargées au domaine principal.
- 2°) Vous allez alors charger par l'intermédiaire de la classe définie au-dessus les assemblies que vous voulez.
- 3°) Enfin par l'intermédiaire de la classe de chargement, et si besoin est, vous allez récupérer une instance d'un objet contenu dans une assembly chargée dans le nouveau domaine d'application.

L'utilisation des domaines d'application fait intervenir certaines notions du « Remoting ». Toutefois ces notions restant très basiques et ponctuelles si vous ne connaissez pas le Remoting ce n'est pas un problème.

Voyons maintenant comment au niveau du code nous allons réaliser ces trois étapes pour cela nous allons écrire :

- 1°) une classe dont le rôle est de charger les assemblies dans le nouveau domaine d'application.

// La classe Loader sert à charger les assemblies dans un domaine d'application particulier. Elle garde également une trace des assemblies chargée.
public class Loader : MarshalByRefObject
{
//On déclare un tableau dynamique pour contenir toutes les assemblies que l'on va charger
ArrayList assemblies;

public Loader()
{
this.assemblies = new ArrayList();
}

public void Load(string assembly)
{
//On charge l'assembly demandée et on l'ajoute à la collection
this.assemblies.Add(Assembly.Load(assembly));
}

public object CreateInstanceOf(int index, string typeName)
{
//On crée une instance de l'objet demandé et on le retourne
return ((Assembly)assemblies[index]).CreateInstance(typeName,true);

}

}

Voyons en détail le code de l'assembly de chargement et ses spécificités.

Commençons par l'explication de l'extension de la classe. En effet vous avez certainement remarqué que la classe Loader étend MarshalByRefObject.
Cette extension ne signifie rien d'autre que le fait que l'on veut passer cette classe par référence dans un autre domaine d'application. Nous ne redéfinissons aucune méthode et nous ne nous préoccuperons pas de savoir comment fonctionne cette classe.

Sachez seulement que pour passer des objets entre domaines d'application vous avez deux choix :
- Vous pouvez les passer par référence, grâce à MarshalByRefObject, c'est-à-dire que le domaine qui « reçoit » l'objet ne reçoit en fait qu'une référence sur cet objet. Toute modification effectuée dans un domaine et reportée dans l'autre ce qui est normal, car c'est le même objet dans les deux domaines.
- Vous pouvez les passer par copie, grâce à [Serializable], c'est-à-dire que l'objet est copié intégralement que les deux domainesne travail pas sur la même copie de l'objet. Si des modifications sont faites dans un domaine, elles ne se répercuteront pas dans l'autre.

Le constructeur ne présente aucun intérêt particulier, par contre la méthode Load va plus nous intéresser. C'est dans cette méthode que le chargement proprement dit d'une assembly va être effectué. La classe Assembly définie dans System.reflection nous permet de faire cela très simplement. Il suffit de donner le nom de l'assembly que l'on veut charger. Toutefois attention à ne pas tomber dans le piège de cette fonction. En effet ce n'est pas le nom tel que nous le voyons dans le système de fichiers, mais bien le nom de l'assembly, qui pratiquement tout le temps est le nom visible à partir système de fichier sans l'extension.
L'appel à Assembly.Load() charge donc l'assembly cible en mémoire dans le domaine d'application courant.

La méthode CreateInstanceOf nous permet de demander la création d'une instance d'un type particulier dans le domaine où s'exécute l'objet Loader.
L'appel de cette méthode nécessite que l'on connaisse le type que l'on veut créer. À propos du type il faut savoir que c'est le type complet qui est demandé. C'est-à-dire que, par exemple, Assembly devient System.Reflection.Assembly.
La classe dont on créer l'objet doit être marquée 'sérializable' ou étendre MarshalByRefObject ou vous aurez une belle exception.
Pourquoi ? Tout simplement, car l'objet va être transféré d'un domaine à l'autre et que .NET doit savoir comment faire ce transfert.

- 2°) une classe qui va charger l'assembly de chargement dans le domaine qu'elle va créer.

AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
setup.PrivateBinPath = path;
setup.ShadowCopyFiles = "false";

this.pluginsDomain = AppDomain.CreateDomain("pluginsDomain",null,setup);

this.loader = (Loader)this.pluginsDomain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.Loader");

Ce code peut être mis dans n'importe quelle méthode.
Voyons comment il fonctionne :
Tout d'abord nous créons un objet de type AppDomainSetup. Cet objet définit les paramètres de notre nouveau domaine d'application. Les plus importants étant :
- ApplicationBase : contient le chemin du répertoire ou se trouve l'assembly principale ;
- PrivateBinPath : contient la liste des répertoires ou doivent être cherché les assemblies privées. Ces répertoires sont combinés à ApplicationBase ;
- ShadowCopyFiles : obtient ou défini si les copies fantômes sont activées. Les copies fantômes sont des copies des assemblies utilisées dans un répertoire externe ce qui fait que l'on a l'impression de pouvoir modifier les assemblies alors que l'application travaille dessus.

ShadowCopyFiles n'est pas comme on pourrait si attendre de type bool mais de type string.

Une fois le setup créé et configuré nous pouvons créer à proprement parlé le domaine d'application. Cela est fait tout simplement grâce à la méthode statique AppDomain.CreateDomain(). Cette méthode possède plusieurs formes, la plus intéressante étant celle présentée au-dessus : elle prend en argument un nom pour le domaine créé, une évidence, en gros un ensemble de règles de sécurité, et un AppDomainSetup.
Après avoir créé le domaine, il faut charger l'assembly de chargement dedans. Cela est fait grâce à la méthode CreateInstanceFromAndUnwrap. Cette méthode charge l'assembly qu'elle prend en argument et créer un objet du type du second argument qu'elle renvoie. La seule restriction de cette méthode est que le type qui est créé doit posséder un constructeur sans argument.

- 3°) rien ;-)
En effet à ce stade un simple appel à notre fonction Load défini nous permet de charger une assembly dans le domaine nouvellement créé.

Je vous ai présenté ici les grands principes de la mise en œuvre des domaines d'application. Ces explications sont assez théoriques, mais les cas d'utilisation au-dessus devraient finir de vous éclairer.
Avant de passer aux exemples d'utilisation, je vais faire un récapitulatif des classes utilisées pour la mise en œuvre des domaines d'application.

II-B-2 - Les classes utilisées

II-B-2-a - System.AppDomain

Cette classe est la classe de base des domaines d'application. Elle vous permet grâce à ses méthodes statiques de créer et décharger un domaine d'application. Elle permet également d'obtenir toutes les informations sur un domaine d'application.

II-B-2-b - System.AppDomainSetup

Cette classe vous permet de déterminer la configuration du domaine que vous allez créer.

II-B-2-c - System.Reflection.Assembly

Cette classe est utilisée pour le chargement des assemblies dans le nouveau domaine d'application.

II-B-2-d - MonEspace.Loader

Cette classe est utilisée comme chargeur d'assembly dans le nouveau domaine. Elle doit être MarshalByRefObject et communique entre les domaines d'application.

III - Cas concret d'utilisation des domaines d'application

III-A - Cas de la sécurisation des données

Dans cette partie nous allons voir comment les domaines d'application peuvent vous aider à mettre à l'abri vos données en mémoire de traitements incontrôlés.
Nous partirons d'une application qui manipule un certain nombre de données et qui est facilement extensible grâce à des plugins. Les plugins sont chargés au démarrage de l'application et ne dispose d'aucune forme de signature permettant d'en assurer la provenance.
Voyons tout de suite le code du formulaire principal :

III-B - Retour sur l'article de la création d'une application modulaire et amélioration de celui-ci

III-C - Application autopatchable

Dans cette partie nous allons voir comment créer une application autopatchable. Une application autopatchable est une application qui n'a besoin d'aucune intervention de l'utilisateur pour se mettre à jour, et cela, sans perte de données. En effet si au moment de la mise à jour l'utilisateur est en train de travailler sur l'application, il ne doit rien perdre de son travail. Ce comportement peut être atteint par différents moyens, mais le plus simple, une fois que l'on connait la technique, reste encore de passer par les domaines d'application.

Cette application se compose de trois modules distincts.
- le premier, MainAssembly, est le cœur de l'application. C'est cette assembly qui contient le main et qui s'occupe de charger et décharger les domaines d'application. Elle contient également le code des classes de « données » ;
- le deuxième, AssemblyLoader, est le module de chargement des assemblies dans le domaine d'application ;
- le troisième, appDomainAutoPatchable, est le module contenant le code du formulaire. C'est le module qui va être mis à jour par l'application.

Voyons le code du premier module :

public static void Main(string[] args)
{
//On déclare un objet de données pour les tests de transfert entre les deux versions de l'application
DataObject data = new DataObject("text1",4);

//On crée un nouveau setup de domaine à partir du domaine courant, car les assemblies sont dans le même répertoire
AppDomainSetup setup = AppDomain.CurrentDomain.SetupInformation;
//On interdit les copies fantômes des assemblies
setup.ShadowCopyFiles = "false";

//On crée le nouveau domaine d'application qui va charger la form
MainClass.formDomain = AppDomain.CreateDomain("formDomain",null, setup);

//On cré une instance de Loader dans le nouveau domaine d'application
Loader loader = (Loader)MainClass.formDomain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.Loader");

//On exécute la fonction load dans le domaine d'application de formDomain
loader.Load("appDomainAutoPatchable");

//On récupère une référence sur une instance de IForm à travers les appDomains
IForm iform = (IForm)loader.CreateInstanceOf(0,"appDomainAutoPatchable.MainForm");

//on passe l'objet data
iform.Data = data;

//On lance la form dans l'autre appDomain
iform.Run();

//Une fois la form fermée on décharge l'appDomain formDomain
AppDomain.Unload(MainClass.formDomain);

//On update la librairie contenant la form mise à jour
MainClass.Update();

//On recrée le domaine d'application
MainClass.formDomain = AppDomain.CreateDomain("formDomain",null, setup);
loader = (Loader)MainClass.formDomain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll","AssemblyLoader.Loader");

loader.Load("appDomainAutoPatchable");

iform = (IForm)loader.CreateInstanceOf(0,"appDomainAutoPatchable.MainForm");

iform.Data = data;
iform.Run();

//La form a été mise à jour sans perte de données.
}

Ce code est le plus complexe de l'application.
Revenons point par point sur les morceaux un peu complexes de ce code.
Il faut bien comprendre que ce type d'application doit être pensé dès le départ. En effet, cette approche implique que votre application effectue clairement la distinction entre les données et leurs traitements. De plus, il faut tenir compte des contraintes, comme le fait d'avoir des assemblies séparées pour le chargeur, le traitement et le « main ».
Sur le plan du code, il n'y a rien de nouveau concernant les domaines d'applications, il faut juste remarquer que l'on utilise une interface pour accéder à la form. Pourquoi ? Tout simplement pour ne pas lier l'assembly chargée dans le nouveau domaine et l'assembly « Main ». En effet, si vous définissez un objet d'un type contenu dans l'assembly à charger, vous serez alors obligé de référencer cette dernière dans l'assembly « Main ». Or, pour des questions de versionning, il est préférable d'éviter cela, car vous ne seriez plus en mesure de lancer votre assembly « Main ».

Le versionning n'est pas l'objet de cet article, cependant sachez que le framework .NET considère comme compatible des librairies dont les deux premiers nombres sont identiques. Le numéro de build (le troisième) n'est là que pour distinguer les versions compatibles entre elles. Ainsi, la version 1.0.3201 et la version 1.0.9023 sont compatibles, mais pas les versions 1.0.3985 et 1.1.4858

La mise à jour est faite par la méthode Update. Elle est une simple copie de librairie du répertoire update vers le répertoire de l'application.
Après la mise à jour, on recrée le domaine d'application et on recharge l'assembly. Une fois qu'une référence est récupérée, on lui rend les données afin de poursuivre le traitement.

IV - Bibliographie et remerciements

IV-A - Bibliographie

L'article de Sébastien Curutchet sur les applications modulaires
Introduction au remoting avec .NET

IV-B - Remerciements

Merci à neguib pour les versions VB.NET et Deplhi.NET et la (grosse) relecture.

IV-C - Téléchargement

Code source des exemples :
Source SharpDevelop (C#) de l'exemple de sécurisation des données
Sources SharpDevelop (C#) de l'exemple d'application autoPatchable