I. Introduction historique à la séparation des données d'une application▲
Historiquement, les données des applications dans les systèmes dits « monotâches » 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âches », 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 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 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 par 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. 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 œuvre les domaines d'application▲
Les domaines d'application ne sont pas tant complexes d'utilisation que peut paraître une lecture de la documentation officielle. Nous nous attacherons à expliquer le parcours de mise en œuvre 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 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 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 possibilité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 répercuté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 évidemment 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 trois étapes. Pour ce faire, écrivons :
- 1°) Une classe dont le rôle est de charger les assemblies dans le nouveau domaine d'application.
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 classe 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 veut 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.
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éfinit 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ées 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 intéressante prend en argument un nom pour le domaine créé, une Evidence (un ensemble de règles 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 œuvre des domaines d'application. Ces explications restent cependant théoriques. Les cas d'utilisation décrits 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 œ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 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 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 vos données en mémoire, à l'abri 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 disposent d'aucune forme de signature permettant d'en assurer la provenance.
Voyons tout de suite le code de chargement des plugins (MainForm.cs) :
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 appelé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 :
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 :
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éfinit 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 :
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 :
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ée au plugins. C'est donc cette copie que le plugins modifie 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.
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 pallier 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éfinie 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 :
'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 :
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
Voilà, avec ces quelques modifications vous avez un système qui vous permet 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érents 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 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
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 à 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 a é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 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.