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 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 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.
interface
uses
System.Collections, System.Reflection;
type
// La classe Loader sert à charger les assemblies dans un domaine d'application particulier.
// Elle garde également une trace des assemblies chargée.
Loader = class
(MarshalByRefObject)
// Declarer un tableau dynamique pour toutes les assemblies à charger
strict
private
var
assemblies : ArrayList;
public
constructor
Create;
procedure
Load(varassembly : string
);
function
CreateInstanceOf(index
: Integer
; typeName : string
):TObject;
end
;
implementation
constructor
Loader.Create;
begin
inherited
Create;
self
.assemblies := ArrayList.Create;
end
;
procedure
Loader.Load(varassembly : string
);
begin
//Charger l'assembly demandée et Ajouter à la collection.
self
.assemblies.Add(Assembly.Load(varassembly))
end
;
function
Loader.CreateInstanceOf(index
: Integer
; typeName : string
):TObject;
var
ass : Assembly;
begin
ass := (self
.assemblies[index
]) as
Assembly;
//Fournir une instance de l'objet demandé .
Result := ass.CreateInstance(typeName, True
);
end
;
end
.
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 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.
var
setup : AppDomainSetup;
begin
setup := AppDomain.CurrentDomain.SetupInformation;
setup.PrivateBinPath := path;
setup.ShadowCopyFiles := 'false'
;
self
.pluginsDomain := AppDomain.CreateDomain('pluginsDomain'
, nil
, setup);
self
.loader := (self
.pluginsDomain.CreateInstanceFromAndUnwrap('AssemblyLoader.dll'
, 'AssemblyLoader.Loader'
)) as
Loader;
end
;
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é 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) :
procedure
TWinForm.LoadPlugins;
var
dir : DirectoryInfo;
var
i : Integer
;
var
iplug : IPlugins;
var
menuI : MenuItem;
var
path : string
;
var
setup : AppDomainSetup;
var
vartype : string
;
var
varfile: FileInfo;
begin
path := Application.StartupPath + '/Plugins/'
;
{$REGION 'Création du domaine d''application et chargement de la librairie de chargement '}
setup := AppDomain.CurrentDomain.SetupInformation;
setup.PrivateBinPath := path;
setup.ShadowCopyFiles := 'false'
;
self
.pluginsDomain := AppDomain.CreateDomain('pluginsDomain'
, nil
, setup);
self
.loader := (self
.pluginsDomain.CreateInstanceFromAndUnwrap('AssemblyLoader.dll'
, 'AssemblyLoader.Loader'
)) as
Loader;
{$ENDREGION}
dir := DirectoryInfo.Create(path);
i := 0
;
for
varfile in
dir.GetFiles('*.dll'
) do
begin
vartype := varfile.Name.Replace(varfile.Extension, ''
);
self
.loader.Load(vartype);
iplug := self
.loader.CreateInstanceOf(i, vartype + '.'
+ vartype) as
IPlugins;
menuI := MenuItem.Create(iplug.Text);
Include(menuI.Click,self
.MenuItemClick);
if
(iplug.IndexMenuItem = 0
) then
begin
self
.menuItem1.MenuItems.Add(menuI);
end
else
begin
self
.menuItem2.MenuItems.Add(menuI);
end
;
self
.plugins.Add(menuI, iplug);
Math.Min(Interlocked.Increment(i), i - 1
);
end
;
end
;
procedure
TWinForm.MenuItemClick(o : System.Object
; e : System.EventArgs);
begin
IPlugins(self
.plugins[o]).Traitement(self
.data);
//Pour l'actualisation des données dans le propertyDataGrid
self
.propertyGrid1.SelectedObject := self
.data
end
;
procedure
TWinForm.MainFormClosing(sender: System.Object
; e: System.ComponentModel.CancelEventArgs);
begin
self
.loader := nil
;
self
.plugins := nil
;
AppDomain.Unload(self
.pluginsDomain);
end
;
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 :
unit
appDomainSecureData;
interface
uses
datasecure;
type
IPlugins = interface
function
get_IndexMenuItem : Integer
;
function
get_Text : string
;
function
Traitement(data : VeryImportanteData) : boolean
;
property
IndexMenuItem : Integer
read
get_IndexMenuItem;
property
Text : string
read
get_Text;
end
;
implementation
end
.
Là aussi rien de compliqué.
Ensuite le code de la classe de données :
unit
datasecure;
interface
type
[Serializable]
VeryImportanteData = class
strict
private
var
varname : string
;
var
varfirstName : string
;
var
vartel : string
;
public
function
get_Name : string
;
function
get_FirstName : string
;
function
get_Tel : string
;
procedure
set_Name(value : string
);
procedure
set_FirstName(value : string
);
procedure
set_Tel(value : string
);
constructor
Create;
property
Name : string
read
get_Name write
set_Name;
property
FirstName : string
read
get_FirstName write
set_FirstName;
property
Tel : string
read
get_Tel write
set_Tel;
end
;
implementation
constructor
VeryImportanteData.Create;
begin
inherited
Create()
end
;
function
VeryImportanteData.get_Name : string
;
begin
Result := self
.varname
end
;
function
VeryImportanteData.get_FirstName : string
;
begin
Result := self
.varfirstName
end
;
function
VeryImportanteData.get_Tel : string
;
begin
Result := self
.vartel
end
;
procedure
VeryImportanteData.set_Name(value: string
);
begin
self
.varname := value
end
;
procedure
VeryImportanteData.set_FirstName(value: string
);
begin
self
.varfirstName := value
end
;
procedure
VeryImportanteData.set_Tel(value: string
);
begin
self
.vartel := value
end
;
end
.
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 :
unit
appPluginsInconnu;
interface
uses
appDomainSecureData, dataSecure;
type
PluginsInconnu = Class
(MarshalByRefObject, IPlugins)
public
constructor
Create;
function
get_IndexMenuItem : Integer
;
function
get_Text : string
;
function
Traitement(data : VeryImportanteData) : Boolean
;
property
IndexMenuItem:Integer
read
get_IndexMenuItem;
property
Text:string
read
get_Text;
end
;
implementation
constructor
PluginsInconnu.Create;
begin
inherited
Create;
end
;
{$REGION ' appDomainSecureData.IPlugins interface implementation '}
function
PluginsInconnu.get_IndexMenuItem : Integer
;
begin
Result := 1
end
;
function
PluginsInconnu.get_Text : string
;
begin
Result := 'PluginsInconnu'
end
;
function
PluginsInconnu.Traitement(data : VeryImportanteData) : Boolean
;
begin
data.Name := 'Je fais n''importe quoi avec le nom'
;
Console.WriteLine('Traitement effectué'
);
Result := true
;
end
;
{$ENDREGION}
end
.
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 :
type
PluginClass = class
(System.Windows.Forms.UserControl, IPlugin)
//Nous ne modifions rien au reste de la classe ...
Code de la classe principale :
//...
uses
//...
datasecure;
type
frmMain = class
(System.Windows.Forms.Form)
strict
private
Components: System.ComponentModel.Container;
tabControl1 : TabControl;
procedure
InitializeComponent;
var
pluginsDomain: AppDomain;
procedure
UnLoadAppDomain;
//...
public
constructor
Create;
end
;
//...
constructor
frmMain.Create;
var
loader : appLoader.Loader;
var
obj : TObject;
var
setup : AppDomainSetup;
var
tp : TabPage;
begin
inherited
Create;
InitializeComponent;
setup := AppDomain.CurrentDomain.SetupInformation;
setup.ShadowCopyFiles := 'false'
;
//Creer un nouveau domaine d'application.
self
.pluginsDomain := AppDomain.CreateDomain('PluginsDomain'
, nil
, setup);
// Créer une instance du plugin dans le domaine principal
// CreateInstanceFromAndUnWrap retourne un objet de type object.
loader := (pluginsDomain.CreateInstanceFromAndUnwrap('AssemblyLoader.dll'
, 'AssemblyLoader.Loader'
)) as
Loader;
loader.Load('Plugin1.dll'
);
obj := loader.CreateInstanceOf(0
,'PluginApp.PluginClass'
);
//Exécuter la fonction de traitement..
MessageBox.Show('Chargement de '
+ IPlugin(obj).Traitement);
//Instancier un tabPage ayant comme titre "Ma page".
tp := TabPage.Create('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(IPlugin(obj).VisualComponent);
// Ajout du tabPage au TabControl.
tabControl1.TabPages.Add(tp);
end
;
procedure
frmMain.UnLoadAppDomain;
begin
AppDomain.Unload(self
.pluginsDomain);
end
;
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 :
[STAThread]
procedure
Main(args : array
of
string
);
//Declarer un objet de données pour les tests de transfert entre les deux versions de l'application
var
data : DataObject;
//Declarer un setup de domaine
var
setup : AppDomainSetup;
var
loader : Loader;
begin
data := DataObject.Create('text1'
, 4
as
TObject);
//Initialiser setup à partir du domaine courant, car les assemblies sont dans le même répertoire
setup := 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'
, nil
, setup);
//Instancier un loader dans le nouveau domaine d'application.
loader := (MainClass.formDomain.CreateInstanceFromAndUnwrap('AssemblyLoader.dll'
,'AssemblyLoader.Loader'
)) as
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
iform := (loader.CreateInstanceOf(0
, 'appDomainAutoPatchable.MainForm'
)) as
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'
, nil
, setup);
loader := (MainClass.formDomain.CreateInstanceFromAndUnwrap('AssemblyLoader.dll'
, 'AssemblyLoader.Loader'
)) as
Loader;
loader.Load('appDomainAutoPatchable'
);
iform := (loader.CreateInstanceOf(0
, 'appDomainAutoPatchable.MainForm'
)) as
IForm;
iform.Data := data;
iform.Run;
//La form à été mise à jour sans perte de données.
end
;
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.