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

Historiquement, les données des applications dans les systèmes dit "monotâche", disposaient 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 dits "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 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 palier à 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 de la mémoire qui sont et qui peuvent être utilisées par le processus. En conséquence, 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éalise 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 est introduite : les domaines d'application. Les domaines d'application fournissent le même niveau d'isolation que les processus mais à un coût de performance bien moindre. Les domaines d'application sont contrôlés par l'interpréteur en temps réel et en accord avec les règles de sécurité de la machine cible.

II. Les AppDomain : Pourquoi et comment ?

II-A. Pourquoi ?

II-A-1. La sécurité

De part la séparation des données, les domaines d'application 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 voir de données autres que celles que vous lui fournissez. Ainsi, par exemple dans le cas de plugins, l'utilisation des domaines d'application vous assure que les ceux-ci ne toucheront pas à d'autres données que celles fournies.

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 de plugins, si vous en chargez 200 pour un traitement spontané. Une fois ce traitement terminé, ceux-ci restent en mémoire tant que l'application n'est pas arrêtée. Par contre, si vous utilisez un domaine d'application pour les charger, vous pouvez à la fin du traitement les décharger et ainsi libérer la mémoire utilisée par les plugins.

II-B. Comment ?

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

Les domaines d'application ne sont pas tant complexe d'utilisation que peut paraître une lecture de la documentation officielle. Nous nous attacherons à expliquer le parcours de mise en oeuvre des 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 3 temps.
- 1°) Vous devez créer le nouveau domaine d'application et y charger une assembly de "chargement". Cela est necessaire, car ainsi vous ne liez pas les assemblies chargées au domaine principal.
- 2°) Vous chargez par l'intermédiaire de l'objet de chargement, les assemblies que vous souhaitez.
- 3°) Enfin, si besoin est, par l'intermédiaire de la classe de chargement, vous récupérez 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 de "Remoting".
Qu'est ce que le "Remoting" ?

C'est tout simplement le fait d'appeler des objets situés sur une autre machine ou dans un autre processus comme si c'était un objet local.
Pour faire cela, il faut définir des règles et utiliser un proxi qui va se charger de faire la liaison entre votre appel local et l'objet distant.
Les règles à définir, concernent toutes l'objet.
Pour passer des objets entre domaines d'application vous avez deux possiblités :
- Vous les passez par référence, grâce à MarshalByRefObject, c'est à dire que le domaine qui "reçoit" l'objet reçoit en fait une référence sur cet objet. Toute modification effectuée dans un domaine et repercutée dans l'autre puisque c'est le même objet dans les deux domaines.
- Vous les passez par copie, grâce à [Serializable], c'est à dire que l'objet est copié intégralement et que les deux domaines ne travaillent donc pas sur le même objet. Si des modifications sont effectuées sur l'objet dans un domaine, elles ne sont bien evidemment pas répercutées dans l'objet de l'autre domaine d'application.

Nous nous arrêterons à ces notions de base pour ce qui concerne le "Remoting".


Voyons maintenant comment implémenter ces 3 étapes. Pour ce faire, ecrivons:

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

 
Sélectionnez

 
Imports System.Reflection
' 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
  Inherits MarshalByRefObject
 
  ' Declarer un tableau dynamique pour toutes les assemblies à charger
  Private assemblies As ArrayList
 
  Public Sub New()
    MyBase.New()
    Me.assemblies = New ArrayList()
  End Sub
  Public Sub Load(ByVal varassembly As String)
    ' Charger l'assembly demandée et Ajouter à la collection
    Me.assemblies.Add([Assembly].Load(varassembly))
  End Sub
  Public Function CreateInstanceOf(ByVal index As Integer, ByVal typeName As String) As Object
    ' Fournir une instance de l'objet demandé
    Return CType(assemblies(index), [Assembly]).CreateInstance(typeName, True)
  End Function
End Class 

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 classes Loader étend MarshalByRefObject.
Cette extension signifie que l'on souhaite 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.

Le constructeur ne présente aucun intérêt particulier. Par contre, la méthode Load va effectué proprement dit le chargement d'une assembly . La classe Assembly définie dans System.reflection nous permet de le faire très simplement. Il suffit de donner le nom de l'assembly que l'on veux charger. Toutefois, attention ce n'est pas le nom tel que nous le voyons dans le système de fichier mais bien le nom de l'assembly, qui pratiquement tout le temps est le nom visible à partir du 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.
La classe, dont on crée l'objet, doit être marquée 'sérializable' ou étendre MarshalByRefObject, sinon une exception sera levée. Pourquoi ? Tout simplement parce que .NET a besoin de savoir comment effectuer le transfert de l'objet.

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

 
Sélectionnez

Dim setup As AppDomainSetup = AppDomain.CurrentDomain.SetupInformation
setup.PrivateBinPath = path
setup.ShadowCopyFiles = "false"
Me.pluginsDomain = AppDomain.CreateDomain("pluginsDomain", Nothing, setup)
Me.loader = CType(Me.pluginsDomain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll", "AssemblyLoader.Loader"), Loader)

Ce code peut être écrit dans n'importe quelle méthode.
Voyons comment il fonctionne :
Tout d'abord, nous créons un objet de type AppDomainSetup. Cet objet défini les paramètres de notre nouveau domaine d'application. Les plus importants sont :
- ApplicationBase : Contient le chemin du répertoire où se trouve l'assembly principale.
- PrivateBinPath : Contient la liste des répertoires où 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 s'y 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 static : AppDomain.CreateDomain(). Cette méthode possède plusieurs surcharges, la plus interressante prend en argument un nom pour le domaine créé, une Evidence (un ensemble de règle de sécurité) , et un AppDomainSetup .
Après avoir créé le domaine, il faut charger l'assembly de chargement. 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. Seule restriction à cette méthode, le type créé doit posséder un constructeur sans argument.


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

Je vous ai présenté ici les grands principes de la mise en oeuvre des domaines d'application. Ces explications restent cependant théoriques. Les cas d'utilisation decrits ci-dessous devraient mieux vous éclairer.
Avant de passer aux exemples d'utilisation je fais un récapitulatif des classes utilisées pour la mise en oeuvre 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 static 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 permez 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 vos données en mémoire, à l'abri de traitement 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 disposent d'aucune forme de signature permettant d'en assurer la provenance.
Voyons tout de suite le code de chargement des plugins (MainForm.cs):

 
Sélectionnez

Imports appDomainSecureData
Imports dataSecure
Imports System
Imports System.IO
Imports System.Threading
'...
  Public Sub LoadPlugins()
    Dim path As String = Application.StartupPath & "/Plugins/"
 
    ' Création du domaine d'application et chargement de la librairie de chargement
    Dim setup As AppDomainSetup
    setup = AppDomain.CurrentDomain.SetupInformation
 
    setup.PrivateBinPath = path
    setup.ShadowCopyFiles = "false"
    Me.pluginsDomain = AppDomain.CreateDomain("pluginsDomain", Nothing, setup)
    Me.loader = CType(Me.pluginsDomain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll", "AssemblyLoader.Loader"), Loader)
 
    Dim dir As DirectoryInfo = New DirectoryInfo(path)
    Dim iplugins As IPlugins
    Dim menuItem As MenuItem
    Dim i As Integer = 0
    Dim file As FileInfo
    Dim type As String
 
    For Each file In dir.GetFiles("*.dll")
      type = file.Name.Replace(file.Extension, "")
      Me.loader.Load(type)
      iplugins = CType(Me.loader.CreateInstanceOf(i, type & "." & type), IPlugins)
      menuItem = New MenuItem(iplugins.Text)
      AddHandler menuItem.Click, AddressOf Me.MenuItemClick
 
      If iplugins.IndexMenuItem = 0 Then : Me.menuItem1.MenuItems.Add(menuItem)
      Else : Me.menuItem2.MenuItems.Add(menuItem)
      End If
 
     Me.plugins.Add(menuItem, iplugins)
     Math.Min(Interlocked.Increment(i), i - 1)
    Next
  End Sub
 
  Private Sub MenuItemClick(ByVal o As Object, ByVal e As System.EventArgs)
     CType(Me.plugins(o), IPlugins).Traitement(Me.data)
     ' Pour l'actualisation des données dans le propertyDataGrid
     Me.propertyGrid1.SelectedObject = Me.data
  End Sub
 
  Sub MainFormClosing(ByVal sender As Object, ByVal e As System.ComponentModel.CancelEventArgs)
    Me.loader = Nothing
    Me.plugins = Nothing
    AppDomain.Unload(Me.pluginsDomain)
  End Sub
 

Cette fonction est appellée par l'évènement OnLoad du formulaire. Elle se charge de créer le domaine d'application et d'y charger tous les plugins présents dans le répertoire Plugins. Elle crée ensuite le menuItem correspondant et l'ajoute au menu en fonction de la valeur définie par l'interface.

Voyons maintenant le code de l'interface des plugins :

 
Sélectionnez

Namespace appDomainSecureData
  Public Interface IPlugins
    Function Traitement(ByVal data As dataSecure.VeryImportanteData) As Boolean
    ReadOnly Property IndexMenuItem() As Integer
    ReadOnly Property Text() As String
  End Interface
End Namespace 

Là aussi rien de compliqué.
Ensuite le code de la classe de données :

 
Sélectionnez

Namespace dataSecure
  <Serializable()> _
  Public Class VeryImportanteData
 
    Private _name As String
    Private _firstName As String
    Private _tel As String
 
    Public Sub New()
    End Sub
 
    Public Property Name() As String
      Get
        Return Me._name
      End Get
      Set(ByVal Value As String)
        Me._name = Value
      End Set
    End Property
    Public Property FirstName() As String
      Get
        Return Me._firstName
      End Get
      Set(ByVal Value As String)
        Me._firstName = Value
      End Set
    End Property
    Public Property Tel() As String
      Get
        Return Me._tel
      End Get
      Set(ByVal Value As String)
        Me._tel = Value
      End Set
    End Property
  End Class
End Namespace 

La seule chose importante dans cette classe est l'instruction [Serializable] qui défini explicitement que le transfert de l'objet dans un autre domaine d'application se fera par copie.

Voyons enfin le code du (méchant) plugins :

 
Sélectionnez

Imports appDomainSecureData
Imports dataSecure
Namespace PluginsInconnu
  Public Class PluginsInconnu
    Inherits MarshalByRefObject
    Implements IPlugins
 
    Public Sub New()
      MyBase.New()
    End Sub
 
    Public ReadOnly Property IndexMenuItem() As Integer Implements IPlugins.IndexMenuItem
      Get
        Return 1
      End Get
    End Property
    Public ReadOnly Property Text() As String Implements IPlugins.Text
      Get
        Return "PluginsInconnu"
      End Get
    End Property
    Public Function Traitement(ByVal data As VeryImportanteData) As Boolean Implements IPlugins.Traitement
      data.Name = "Je fais n'importe quoi avec le nom"
      MessageBox.Show("Traitement effectué")
      Return True
    End Function
 
  End Class
End Namespace 

Le plugins est marqué MarshalByRefObject, car le domaine principal communique avec lui, il faut donc indiquer au framework .Net que nous voulons travailler sur la référence de l'objet et non pas sur sa copie. Voyons maintenant le résultat de l'exécution de ce premier programme utilisant les domaines d'application :

Informations remplies avant l'action du plugins
Informations remplies avant l'action du plugins
Le plugins a fait son traitement
Le plugins a fait son traitement
Les données sont toujours les même bien que le plugins les ai apparament modifiées
Les données sont toujours les même bien que le plugins les ai apparament modifiées

Explications : Bien que vous ayez l'impression de passer l'objet data par référence (comme vous le faites d'habitude avec les objets), celui-ci est copié par .NET et c'est la copie qui est envoyé au plugins. C'est donc cette copie que le plugins modifient et non l'objet original ce qui fait que les informations restent les mêmes après exécution du plugins.
Si vous voulez vous amuser un peu vous pouvez marquer la classe VeryImportanteData comme héritant de MarshalByRefObject à la place de [Serializable] et constater les dégâts.

On réentre les informations
On réentre les informations
Les informations ont été modifiées par le plugins
Les informations ont été modifiées par le plugins

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

Dans cette partie, nous revenons sur l'article de Sébastien Curutchet sur le développement d'une application Winform de façon modulaire. (Disponible ici ).
Un problème soulevé par cet article est qu'une fois les plugins chargés, on ne peut les décharger sans fermer l'application.
Nous allons donc voir comment palier à ce problème grâce aux domaines d'application.

Tout d'abord il faut savoir que toute la base reste la même. Ainsi nous ne toucherons pas à l'interface défini et nous ne ferons qu'une petite modification du plugin exemple.
Le gros de la modification sera donc dans la façon de charger les plugins dans notre application.

Voyons le code ainsi modifié :

Code du plugins :

 
Sélectionnez

'Classe du plugin respectant le modèle : hérite de IPlugin et pour les appDomains on étend MarshalByRefObject
'On ne touche à rien de la classe plugins car les composants WinForm heritent déja de MarshalByRefObject
Public Class PluginClass Inherits System.Windows.Forms.UserControl Implements IPlugin
	'Nous ne modifions rien au reste de la classe
	'....
End Class

Code de la classe principale :

 
Sélectionnez

Imports appDomainSecureData
Imports dataSecure
'...
Public Class frmMain
  '...
  Public Sub New()
    MyBase.New()
    Me.InitializeComponent
 
    Dim loader As Loader
    Dim obj As Object
    Dim setup As AppDomainSetup
 
    setup = AppDomain.CurrentDomain.SetupInformation
    setup.ShadowCopyFiles = "false"
    'Creer un nouveau domaine d'application
    Me.pluginsDomain = AppDomain.CreateDomain("PluginsDomain", Nothing, setup)
    ' Créer une instance du plugin dans le domaine principal
    ' La fonction CreateInstanceFromAndUnWrap retourne un objet de type object
    loader = CType(pluginsDomain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll", "AssemblyLoader.Loader"),
 
Loader)
    loader.Load("Plugin1.dll")
    obj = loader.CreateInstanceOf(0, "PluginApp.PluginClass")
    ' Exécuter la fonction de traitement
    MessageBox.Show("Chargement de " + CType((obj), IPlugin).Traitement)
 
    ' Instancier un tabPage ayant comme titre "Ma page"
    Dim tp As TabPage = New TabPage("Ma page")
    ' Ajout au tabPage d'un composant visuel du plugin
    ' obj étant de type object, le transTypage en IPlugin est nécessaire
    tp.Controls.Add(CType(obj, IPlugin).VisualComponent)
    ' Ajout du tabPage au TabControl
    tabControl1.TabPages.Add(tp)
  End Sub
 
  Private Sub UnLoadAppDomain()
    AppDomain.Unload(Me.pluginsDomain)
  End Sub
 
   '...
End Class 

Voila, avec ces quelques modifications vous avez un système qui vous permez de décharger les plugins quand vous le souhaitez. Une petite restriction tout de même : Veillez à correctement supprimer toutes les instances des classes définies dans les plugins avant de décharger le domaine d'application.

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 justement en train de travailler sur l'application, il ne doit rien perdre de son travail. Ce comportement peut être atteint par différent moyens mais le plus simple, une fois que l'on connait la technique, reste encore d'utiliser les domaines d'application.

Cette application se compose de 3 modules distincts.
- Le premier, MainAssembly, est le coeur 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 :

 
Sélectionnez

Public Shared Sub Main(ByVal args As String())
  'Declarer un objet de données pour les tests de transfert entre les deux versions de l'application
  Dim data As DataObject = New DataObject("text1", 4)
  'Créer un nouveau setup de domaine à partir du domaine courant car les assemblies sont dans le même répertoire
  Dim setup As AppDomainSetup = AppDomain.CurrentDomain.SetupInformation
  'Interdire les copies fantômes des assemblies
  setup.ShadowCopyFiles = "false"
  'Creer le nouveau domaine d'application qui va charger la form
  MainClass.formDomain = AppDomain.CreateDomain("formDomain", Nothing, setup)
  'Instancier Loader dans le nouveau domaine d'application
  Dim loader As Loader = CType(MainClass.formDomain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll", "AssemblyLoader.Loader"), Loader)
  'Executer le chargement dans le domaine d'application de formDomain
  loader.Load("appDomainAutoPatchable")
  'Recuperer une référence sur une instance de IForm a travers les appDomains
  Dim iform As IForm = CType(loader.CreateInstanceOf(0, "appDomainAutoPatchable.MainForm"), IForm)
  'Passer l'objet data
  iform.Data = data
  'Lancer la form dans l'autre appDomain
  iform.Run
  'Décharger l'appDomain formDomain
  AppDomain.Unload(MainClass.formDomain)
  'Mettre à jour la librairie contenant la form
  MainClass.Update
  'Recréer le domaine d'application
  MainClass.formDomain = AppDomain.CreateDomain("formDomain", Nothing, setup)
  loader = CType(MainClass.formDomain.CreateInstanceFromAndUnwrap("AssemblyLoader.dll", "AssemblyLoader.Loader"), Loader)
  loader.Load("appDomainAutoPatchable")
  iform = CType(loader.CreateInstanceOf(0, "appDomainAutoPatchable.MainForm"), IForm)
  iform.Data = data
  iform.Run
  'La form à été mise à jour sans perte de données
End Sub 

Ce code est le plus complexe de l'application.
Revenons point par point sur les morceaux un peu complexe 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 compatible 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

IV-B. Remerciements

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

IV-C. Téléchargement