Le code d’une application .NET est déployé sous la forme de code IL (i.e. Intermediate Language) dans des unités déployables appelées assemblies. Ces assemblies sont des fichiers avec une extension .exe
pour un exécutable ou .dll
pour une bibliothèque de classes. L’intérêt de pouvoir organiser le code dans des assemblies différentes est, par exemple, de partager du code semblable entre plusieurs applications ou de rendre une application modulable en permettant de charger du code sous la forme de plug-in.
Durant l’exécution d’une application, par défaut, les assemblies sont chargées en mémoire en mode “lazy-loading” c’est-à-dire qu’elles ne sont chargées que si le code qui s’y trouve est appelé.
Dans le cadre du framework .NET, les assemblies sont chargées en mémoire dans une couche d’isolation appelée application domain. Cette couche peut être unique pour tout le processus ou suivant les besoins il peut en exister plusieurs. L’implémentation plus récente de .NET (anciennement appelée .NET Core) n’utilise pas les application domains mais une notion améliorée équivalente appelée assembly load context.
Le but de cet article est de passer en revue les caractéristiques des application domains. Dans un prochain article, on explicitera les fonctionnalités principales des assembly load contexts.
Caractéristiques principales
Espace mémoire isolé
Partage des threads
Marshalling et Sérialisation
Manipulation des application domains
Exemples d’utilisation des application domains
Charger et décharger du code sous la forme d’un plug-in avec une interface
Exemple sans marshalling
Charger et décharger du code sous la forme d’un plug-in avec la reflection
Passage d’argument par valeur
Passage d’argument par référence
Serialization vs marshalling
Dans un 1er temps, on va passer en revue les caractéristiques principales des application domains puis dans un 2e temps, on va illustrer quelques cas d’utilisation avec des exemples.
Caractéristiques principales
Les application domains sont des couches d’isolation dans un processus qui permettent:
- D’isoler le code: il est possible de charger des assemblies dans des application domains différents. L’isolation des application domains permet de charger, le cas échéant, de même assemblies dans des versions différentes.
- D’implémenter des plug-ins: les assemblies correspondant au plug-in peuvent être chargées dans un application domain différent de l’application domain principal. Par la suite, ces assemblies ne peuvent pas être déchargées mais l’application domain dans lequel elles se trouvent peut, en revanche, être déchargés.
- D’apporter une isolation en terme de sécurité: des portions de code dans des assemblies particulières peuvent être exécutées avec un niveau de sécurité différent.
Espace mémoire isolé
Les application domains permettent d’isoler des espaces contigués de mémoire virtuelle et d’y placer du code et des ressources auxquels on pourra accéder et les référencer sans, toutefois, partager ces espaces. Par exemple, d’un application domain à l’autre:
- Il n’est pas possible de référencer directement un contenu dans un application domain différent,
- Les données ne peuvent pas être passées d’un application domain à un autre directement, elles sont copiées par valeur en utilisant la sérialisation. Dans le cadre des objets de type référence qui sont accessibles avec des références (les références sont des objets de type valeur), les références pourraient être copiées par valeur d’un application domain à l’autre. Toutefois pour que les méthodes de l’objet soit réellement “visible” dans un autre application domain, il faut utiliser le mécanisme de marshalling: un objet proxy sert d’intermédiaire entre l’application domain où l’objet a été créé et l’application domain où l’objet est utilisé.
Enfin, les application domains partagent le même tas managé toutefois ils sont assez isolés pour qu’un application domain ne puisse interférer directement les objets d’un autre application domain.
Partage des threads
Dans le système d’exploitation, les processus sont isolés en terme de mémoire et en terme de thread.
Ainsi:
- Pour exécuter du code de façon isolée, par exemple dans un autre processus, il faudrait créer ce processus et le détruire à la fin de son utilisation;
- Le partage de données entre ces processus ne se fait pas directement, il faudrait prévoir des mécanismes de type Named Pipes, Memory mapped file ou le système de fichiers.
- Enfin le lancement de code dans un processus séparé puis la récupération du résultat le cas échéant nécessite des mécanismes de synchronisation et d’appels comme le remoting, WCF, des communications réseaux ou RPC (Remote Procédure Call).
Tous ces mécanismes sont couteux en ressource, en temps d’exécution et en complexité d’implémentation. Les application domains proposent une solution pour s’affranchir de la difficulté de devoir faire des appels à du code dans des processus différents en partageant les mêmes threads. Le gros intérêt est d’éviter de devoir implémenter des mécanismes de synchronisation lorsqu’on appelle du code dans un application domain différent.
Marshalling et sérialisation
Le passage d’objets entre des application domains se fait par des copies par valeur, les objets sont sérialisés dans leur application domain d’origine puis désérialisés dans l’application domain où ils seront utilisés. Même si entre des application domains, les mêmes threads sont partagés, la frontière est assez importante pour qu’un application domain ne puisse pas accéder et exécuter du code se trouvant dans un autre application domain. Les appels se font par un mécanisme appelé marshalling qui consiste à créer un objet proxy dans l’application domain dans lequel on veut exécuter une méthode, l’appel à travers la frontière des application domains consistera à appeler une méthode dans l’objet proxy.
Le marshalling utilise la sérialisation lors du passage des objets.
Manipulation des application domains
La plupart des manipulations concernant les application domains se font par l’intermédiaire de la classe statique System.AppDomain:
- Accéder à l’application domain principal:
AppDomain.CurrentDomain
. - Créer un nouvel application domain:
AppDomain.CreateDomain()
- Instancier une classe dans un application domain particulier:
<instance AppDomain>.CreateInstanceAndUnwrap(<nom assembly>, <nom complet de la classe à instancier>)
.
On peut voir dans la fonctionCreationInstanceAndUnwrap()
qu’on manipule les noms des objets sous la forme d’une chaîne de caractères. Par exemple pour l’argument correspondant au nom du type de la classe à instancier, on n’utilise pas un objetType
car le code qui exécute la fonctionCreateInstanceAndUnwrap()
se trouve dans l’application domain courant. Ainsi si on manipule un objetType
, cela signifie que l’assembly contenant ce type est chargé dans l’application domain courant ce qui n’est pas le but recherché. - Lister les assemblies chargées dans un application domain:
<AppDomain>.GetAssemblies()
. - Décharger un application domain (et toutes les assemblies qu’il contient):
AppDomain.Unload(<AppDomain à décharger>)
Exemples d’utilisation des application domains
Les cas d’utilisation les plus fréquents où on souhaite manipuler les application domains sont pour:
- Plug-in: avoir du code qu’il est possible de charger sous la forme d’un plug-in. Le plug-in est ainsi modulaire, on peut le charger et le décharger à sa guise suivant les besoins.
- Charger plusieurs versions d’une même assembly: normalement une seule version d’une assembly est chargée en mémoire toutefois pour satisfaire des dépendances indirectes, il peut être nécessaire de charger des versions différentes d’une même assembly.
Les application domains ne sont disponibles que si on cible le framework .NET (<= 4.8). Cette fonctionnalité n’est pas disponible avec .NET (>= 5.0).
Au travers de quelques exemples, on va montrer comment on peut manipuler les application domains. Ces exemples comportent 3 assemblies:
FxDotNetExamples.exe
: c’est le “main” qui permet de lancer l’exécution de l’exemple.SimplePlugIn.dll
: c’est l’assembly qui sera chargée dans un application domain séparé. Cette assembly contient la classeSimpleClassMarshalByRef
.FxDotNetCommonInterfaces.dll
: cette assembly est référencée dansFxDotNetExamples.exe
etSimplePlugIn.dll
. Elle contient l’interfaceISimpleClass
.
Charger et décharger du code sous la forme d’un plug-in avec une interface
On se propose de:
- Charger du code se trouvant dans une assembly nommée
SimplePlugIn
dans un application domain différent, - Exécuter du code dans
SimplePlugIn
puis - Décharger cet application domain.
Tout au long des étapes, on affiche les assemblies chargées dans l’application domain courant en exécutant:
static void PrintLoadedAssemblies()
{
Assembly[] assemblies = AppDomain.CurrentDomain.GetAssemblies();
foreach (Assembly assembly in assemblies)
Console.WriteLine(assembly.FullName);
}
On crée une assembly nommée FxDotCommonInterfaces
contenant une interface définissant la méthode à exécuter dans un autre application domain:
public interface ISimpleClass
{
void HelloWorldExample();
}
FxDotCommonInterfaces
est ajoutée en référence du programme principal et de SimplePlugIn
.
Le classe concrète à exécuter qui satisfait ISimpleClass
est:
public class SimpleClassMarshalByRef : MarshalByRefObject, ISimpleClass
{
public void HelloWorldExample()
{
Console.WriteLine($"{nameof(SimpleClassMarshalByRef.HelloWorldExample)} executed");
}
}
Cet objet permet de mettre en œuvre le marshalling en . NET (voir plus haut). En dérivant de MarshalByRefObject
, il sera possible d’appeler des fonctions dans la classe SimpleClassMarshalByRef
par l’intermédiaire d’un objet proxy. SimpleClassMarshalByRef
et son proxy satisfont la même interface. SimpleClassMarshalByRef
reste dans l’application domain supplémentaire, le proxy est utilisé dans l’application domain courant. Quand la fonction HelloWorldExample()
est appelée, l’appel se fait dans l’objet proxy et cet appel est répercuté par référence sur l’objet réel dans l’application domain supplémentaire.
Comme on peut le voir dans le code de cet objet mscorlib/system/marshalbyrefobject.cs#L46, les appels se font par référence. Ainsi s’il n’y a pas d’arguments lors de l’appel de fonction, l’appel d’une méthode par l’intermédiaire du proxy n’est pas significativement plus couteux. Malheureusement des appels de fonction se font rarement sans arguments impliquant des mécanismes de sérialisation couteux en performance.
Le code permettant d’exécuter les différentes étapes est:
Console.WriteLine("Before loading FxDotNetDependency");
PrintLoadedAssemblies();
Console.ReadLine();
// 1. Création de l'app domain différent
Console.WriteLine("Creating app domain");
AppDomain ad = AppDomain.CreateDomain(appDomainName);
PrintLoadedAssemblies();
// 2. Instanciation d'une classe dans cet app domain
var o = ad.CreateInstanceAndUnwrap(dependencyAssemblyName, simpleClassTypeName);
Console.WriteLine("Before executing SimpleClass");
PrintLoadedAssemblies();
Console.ReadLine();
// 3. Appel d'une fonction dans cette classe
this.CallFunctionWithInterface(o);
Console.WriteLine("Before app domain unload");
PrintLoadedAssemblies();
Console.ReadLine();
// 4. Déchargement de l'app domain différent
AppDomain.Unload(ad);
Console.WriteLine("After app domain unload");
PrintLoadedAssemblies();
Console.ReadLine();
Le code de CallFunctionWithInterface()
est:
private void CallFunctionWithInterface(object instance)
{
var simpleClass = instance as ISimpleClass;
simpleClass.HelloWorldExample();
}
A l’exécution, on peut voir que l’assembly SimplePlugIn
n’est jamais chargée dans l’application domain courant:
Before loading FxDotNetDependency ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- Creating app domain ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- Before executing SimpleClass ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- HelloWorldExample executed Before app domain unload ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- After app domain unload ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ----------------------------------
L’interface permet de manipuler le type SimpleClassMarshalByRef
sans avoir à charger explicitement son type (dans le méthode CallFunctionWithInterface()
).
Exemple sans marshalling
Sans marshalling il n’est pas possible d’exécuter une méthode dans la classe SimpleClassMarshalByRef
dans l’application domain supplémentaire sans charger le type SimpleClassMarshalByRef
dans l’application domain courant.
Par exemple si on modifie la classe SimpleClassMarshalByRef
pour qu’elle ne dérive pas de MarshalByRefObject
:
[Serializable]
public class SimpleClassMarshalByRef : ISimpleClass
{
// ...
}
En exécutant le même code permettant d’exécuter la méthode SimpleClass.HelloWorldExample()
, on peut voir que l’assembly SimplePlugIn
est chargée dans l’application domain courant:
Before loading FxDotNetDependency ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- Creating app domain ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- Before executing SimpleClass ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null SimplePlugIn, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- HelloWorldExample executed Before app domain unload ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null SimplePlugIn, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- After app domain unload ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null SimplePlugIn, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ----------------------------------
Charger et décharger du code sous la forme d’un plug-in avec la reflection
On se propose d’exécuter le même code que précédemment à la différence qu’on n’utilise pas une interface mais on essaie d’exécuter la méthode SimpleClassMarshalByRef.HelloWorldExample()
avec la reflection. On remplace l’appel à CallFunctionWithInterface()
par:
private void CallFunctionWithReflection(object instance)
{
Type type = instance.GetType();
MethodInfo methodInfo = type.GetMethod("HelloWorldExample");
methodInfo.Invoke(instance, Array.Empty<object>());
}
Si on exécute ce code (très similaire au code précédemment à l’exception de l’appel à CallFunctionWithReflection()
):
Console.WriteLine("Before loading FxDotNetDependency");
PrintLoadedAssemblies();
Console.ReadLine();
// 1. Création de l'app domain différent
Console.WriteLine("Creating app domain");
AppDomain ad = AppDomain.CreateDomain(appDomainName);
PrintLoadedAssemblies();
// 2. Instanciation d'une classe dans cet app domain
var o = ad.CreateInstanceAndUnwrap(dependencyAssemblyName, simpleClassTypeName);
Console.WriteLine("Before executing SimpleClass");
PrintLoadedAssemblies();
Console.ReadLine();
// 3. Appel d'une fonction avec la reflection
this.CallFunctionWithReflection(o);
Console.WriteLine("Before app domain unload");
PrintLoadedAssemblies();
Console.ReadLine();
// 4. Déchargement de l'app domain différent
AppDomain.Unload(ad);
Console.WriteLine("After app domain unload");
PrintLoadedAssemblies();
Console.ReadLine();
On peut voir que le comportement est différent de précédemment, l’assembly contenant la classe SimpleClassMarshalByRef
est bien chargée dans l’application domain courant:
Before loading FxDotNetDependency ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- Creating app domain ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- Before executing SimpleClass ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- HelloWorldExample executed Before app domain unload ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null SimplePlugIn, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ---------------------------------- After app domain unload ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null SimplePlugIn, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ----------------------------------
La reflection ne permet pas d’isoler la manipulation du code à l’application domain supplémentaire. La mise en œuvre de la reflection entraîne le chargement du type SimpleClassMarshalByRef
et donc le chargement de l’assembly SimplePlugIn
dans l’application domain courant. Ceci s’explique par le fait que la reflection doit charger le type SimpleClassMarshalByRef
pour l’instancier, il n’y a pas d’utilisation du marshalling.
Passage d’argument par valeur
Dans cet exemple, on se propose d’effectuer un passage d’un argument sérialisable par valeur. Volontairement le type de l’objet passé en argument ne permet d’effectuer du marshalling (i.e. il ne dérive pas de MarshalByRefObject
) toutefois il est sérialisable.
On rajoute une fonction dans l’interface ISimpleClass
permettant le passage d’une liste générique en argument et la modification de cette liste:
public interface ISimpleClass
{
void HelloWorldExample();
void ChangeArgumentPassedByValue(int newValue, List<int> intValues);
}
On rajoute l’implémentation correspondante dans la classe SimpleClassMarshalByRef
:
[Serializable]
public class SimpleClassMarshalByRef : MarshalByRefObject, ISimpleClass
{
// ...
public void ChangeArgumentPassedByValue(int newValue, List<int> intValues)
{
Console.WriteLine($"Hash code: {intValues.GetHashCode()}");
intValues.Add(newValue);
}
}
La liste générique est un objet sérialisable:
[Serializable]
[DebuggerTypeProxy(typeof(Mscorlib_CollectionDebugView<>))]
[DebuggerDisplay("Count = {Count}")]
[__DynamicallyInvokable]
public class List<T> : IList<T>, ICollection<T>, IEnumerable<T>, IEnumerable, IList, ICollection, IReadOnlyList<T>, IReadOnlyCollection<T>
{
// ...
}
Si on exécute le code suivant:
AppDomain ad = AppDomain.CreateDomain(appDomainName);
var instance = ad.CreateInstanceAndUnwrap(dependencyAssemblyName, simpleClassTypeName);
var simpleClass = instance as ISimpleClass;
var integerList = new List<int> { 1, 2, 3 };
Console.WriteLine($"Hash code: {integerList.GetHashCode()}");
simpleClass.ChangeArgumentPassedByValue(4, integerList);
Console.WriteLine(string.Join(", ", integerList.Select(l => l.ToString())));
PrintLoadedAssemblies();
Console.ReadLine();
On obtient le résultat:
Hash code: 19575591 Hash code: 47096010 1, 2, 3 ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null System.Core, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 System, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 ----------------------------------
On peut voir que le hash code n’est pas le même car lors du passage en argument, il s’est produit une copie par valeur de la liste. Même si on rajoute un élément dans la liste dans la fonction ChangeArgumentPassedByValue()
, on peut voir que la liste d’origine n’est pas modifiée car il ne s’agit du même objet.
Passage d’argument par référence
Il est possible de passer un objet en argument d’un application domain à un autre à condition qu’il soit possible d’effectuer du marshalling. Par exemple, si on considère l’objet suivant autorisant le marshalling:
public class MarshalByRefList<T> : MarshalByRefObject
{
private List<T> values;
public MarshalByRefList(params T[] values)
{
this.values = new List<T>(values);
}
public void AddValue(T value)
{
this.values.Add(value);
}
public void DisplayValues()
{
foreach (T value in this.values)
Console.WriteLine(value);
}
public T this[int key]
{
get => this.values[key];
set => this.values[key] = value;
}
}
En reprenant l’exemple précédent permettant:
- De passer un objet en argument d’une méthode d’un application domain à l’autre et
- De modifier dans un application domain pour vérifier si les modifications sont visibles dans l’autre application domain.
On rajoute la méthode ChangeArgumentPassedByRef<T>()
dans l’interface ISimpleClass
et la classe SimpleClassMarshalByRef
:
public interface ISimpleClass
{
// ...
void ChangeArgumentPassedByRef<T>(T newValue, MarshalByRefList<T> intValues);
}
public class SimpleClassMarshalByRef : MarshalByRefObject, ISimpleClass
{
// ...
public void ChangeArgumentPassedByRef<T>(T newValue, MarshalByRefList<T> values)
{
Console.WriteLine($"Hash code: {values.GetHashCode()}");
values.AddValue(newValue);
}
}
Si on exécute le code suivant:
AppDomain ad = AppDomain.CreateDomain(appDomainName);
var instance = ad.CreateInstanceAndUnwrap(dependencyAssemblyName, simpleClassTypeName);
var simpleClass = instance as ISimpleClass;
var marshalByRefList = new MarshalByRefList<int>(1, 2, 3);
Console.WriteLine($"Hash code: {marshalByRefList.GetHashCode()}");
marshalByRefList.DisplayValues();
simpleClass.ChangeArgumentPassedByRef(4, marshalByRefList);
marshalByRefList.DisplayValues();
PrintLoadedAssemblies();
Console.ReadLine();
On obtient le résultat suivant:
Hash code: 19575591 1 2 3 Hash code: 19575591 1 2 3 4 ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ----------------------------------
La structure modifiée est la même entre les 2 application domains. Le marshalling permet de faire passer l’objet par référence, il n’y a pas de copie par valeur.
Serialization vs marshalling
Cet exemple permet d’illustrer la différence entre la sérialisation et le marshalling. Il consiste à observer la différence de comportement lorsqu’on passe une liste d’objets d’un application domain à un autre suivant si le marshalling est appliqué ou non à ces objets.
On considère l’objet sérialisable suivant sur lequel on peut appliquer le marshalling (grâce à MarshalByRefObject
):
[Serializable]
public class CustomMarshalByRefObject: MarshalByRefObject
{
public int InnerValue { get; set; }
public override string ToString()
{
return this.InnerValue.ToString();
}
}
On modifie le classe SimpleClassMarshalByRef
et l’interface ISimpleClass
pour permettre le passage d’une liste de type CustomMarshalByRefObject
d’un application domain à un autre:
public interface ISimpleClass
{
// ...
void PassArgumentByRef(List<CustomMarshalByRefObject> values);
}
[Serializable]
public class SimpleClassMarshalByRef : MarshalByRefObject, ISimpleClass
{
private List<CustomMarshalByRefObject> marshalByRefObjectList;
public void DisplayInnerValues()
{
if (marshalByRefObjectList != null)
{
foreach (var value in this.marshalByRefObjectList)
{
Console.WriteLine(value);
}
}
}
// ...
public void PassArgumentByRef(List<CustomMarshalByRefObject> values)
{
this.marshalByRefObjectList = values;
}
}
Le code suivant permet de passer d’un application domain à un autre une liste de CustomMarshalByRefObject
. On modifie ensuite le contenu d’un objet de la liste dans l’application domain principal et on vérifie si cette modification s’est répercutée dans l’application domain supplémentaire:
public void ChangeMarshalByRefObjectWithList()
{
AppDomain ad = AppDomain.CreateDomain(appDomainName);
var instance = ad.CreateInstanceAndUnwrap(dependencyAssemblyName, simpleClassTypeName);
var simpleClass = instance as ISimpleClass;
var thirdObject = new CustomMarshalByRefObject { InnerValue = 3 };
var marshalByRefList = new List<CustomMarshalByRefObject>{
new CustomMarshalByRefObject { InnerValue = 1 },
new CustomMarshalByRefObject { InnerValue = 2 },
thirdObject };
simpleClass.PassArgumentByRef(marshalByRefList);
simpleClass.DisplayInnerValues();
thirdObject.InnerValue = 5;
simpleClass.DisplayInnerValues();
PrintLoadedAssemblies();
Console.ReadLine();
}
En exécutant ce code, on obtient:
1 2 3 1 2 5 ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ----------------------------------
On constate que la valeur modifiée dans l’application domain principal a bien été répercutée dans l’application domain principal. Plusieurs éléments expliquent ce comportement:
- Une liste générique est sérialisable mais il n’est pas possible d’y appliquer du marshalling. Ainsi lors du passage en argument dans la fonction
SimpleClassMarshalByRef.PassArgumentByRef()
, la liste est dupliquée dans l’application domain supplémentaire. - Etant donné que la liste ne contient que des références vers des instances d’objets de type
CustomMarshalByRefObject
, les objetsCustomMarshalByRefObject
vers lesquelles pointent les références ne sont pas dupliquées. - Enfin, le marshalling est appliquée sur les instances d’objets
CustomMarshalByRefObject
, ainsi lorsqu’on modifie le contenu de ces objets dans un application domain, cette modification est répercutée via le proxy dans la seule instance de cet objet. La modification est alors visible à partir des 2 application domains.
Si on modifie la classe CustomMarshalByRefObject
pour que le marshalling ne puisse plus s’appliquer:
[Serializable]
public class CustomMarshalByRefObject //: MarshalByRefObject
{
public int InnerValue { get; set; }
public override string ToString()
{
return this.InnerValue.ToString();
}
}
En exécutant le même code que précédemment, on obtient:
1 2 3 1 2 3 ---------------------------------- mscorlib, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 AssemblyLoadContextExamples_FxDotNet, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null FxDotCommonInterfaces, Version=1.0.0.0, Culture=neutral, PublicKeyToken=null ----------------------------------
On peut voir que la modification dans l’application domain principal ne s’est pas répercutée dans l’application domain supplémentaire. Cela s’explique par le fait qu’en supprimant le marshalling, le passage en argument de la liste d’objets CustomMarshalByRefObject
a conduit à la duplication de ces objets en utilisant la sérialisation. 2 instances de chaque objet existent dans les 2 application domains. Quand on modifie une instance de CustomMarshalByRefObject
, la modification n’est pas répercutée dans l’autre instance.
En conclusion…
Les application domains sont une solution pour permettre d’isoler des portions de code dans un même processus dans le but de rendre ces portions modulables. Cette fonctionnalité permet, par exemple:
- De mettre en place des mécanismes de plug-in: on peut ainsi charger des assemblies dans des application domains différents et décharger ces application domains par la suite.
- De charger des versions différentes d’une même assembly.
La mise en œuvre des application domains n’est, toutefois, pas tout à fait directe car elle implique que les objets transitants d’un application domain à l’autre soient sérialisables. D’autre part, pour appeler du code d’un application domain à l’autre, il faut utiliser des mécanismes de marshalling. Le marshalling et surtout la sérialisation entraînent un coût en performance non négligeable par rapport à des appels dans un même application domain.
Les application domains ne sont utilisables que dans le cadre du framework .NET car une fonctionnalité plus performante a été implémentée à partir de .NET 5: les assembly load contexts. Les assembly load contexts permettent une isolation moins franche entre des portions de code en permettant de maîtriser le chargement des assemblies. Malgré des contraintes moins fortes que pour les application domains, les assembly load contexts permettent d’implémenter tous les cas d’utilisation adressés par les application domains sans dégrader les performances.
- How to load DLL in separate domain and use its methods?: https://learn.microsoft.com/fr-fr/archive/blogs/asiasupp/c-how-to-load-dll-in-separate-domain-and-use-its-methods
- What is the major use of MarshalByRefObject?: https://stackoverflow.com/questions/4295894/what-is-the-major-use-of-marshalbyrefobject
- Code source marshalbyrefobject.cs: https://github.com/microsoft/referencesource/blob/master/mscorlib/system/marshalbyrefobject.cs
- What is the difference between Serialization and Marshaling?: https://stackoverflow.com/questions/770474/what-is-the-difference-between-serialization-and-marshaling
- Serialization vs. Marshaling: https://www.baeldung.com/cs/serialization-vs-marshaling
- The Nuances of Loading and Unloading Assemblies with AppDomain: https://www.codeproject.com/Articles/1091726/The-Nuances-of-Loading-and-Unloading-Assemblies-wi#References_or_Value
- Usage of AppDomain in C#: https://stackoverflow.com/questions/665668/usage-of-appdomain-in-c-sharp
- What is AppDomain?: https://stackoverflow.com/questions/574708/what-is-appdomain
- How to start Sending events through Application Domain boundary (C# AppDomain): https://blog.vcillusion.co.in/sending-events-through-application-domain-boundary/