Cours
3.1 - Définition d’un assembly .NET
Un assembly .NET est une collection de types et de ressources qui représente une unité logique de fonctionnalités.
Un assembly .NET est un fichier qui contient du code CIL (Common Intermediate Language) et des informations supplémentaires afin de prévenir des erreurs de versions et d’accroitre la sécurité.
Le code CIL contenu dans l’assembly a été compilé par le compilateur VB.NET, C# (ou autre langage supporté par le framework .NET).
Le code CIL contenu dans l’assembly est ensuite compilé par le CLR en code machine exécutable lorsqu’il est chargé en mémoire. C’est pour cela que le compilateur du CLR est aussi appelé compilateur Just In Time ou juste à temps.
L’assembly .NET est le standard pour les composants développés avec le .NET Microsoft.
Les assemblys .NET peuvent être ou non des exécutables, ce sont :
-
Soit des applications (fichiers exécutables) d’extension .exe, aussi appelés PE (Portable Executable)
-
Soit des bibliothèques de types (Dynamic Link Librairies), d’extension .dll
Tous les assemblys contiennent:
-
La définition des types utilisés dans l’assembly
-
Les informations de versions
-
Les métadonnées
-
Le manifeste
-
Le code IL
Il existe 2 sortes d’assemblys : private et shared.
3.2 - Private assembly ou assembly privé.
C’est le type par défaut lorsque l’on crée un assembly.
Ce type d’assembly est copié avec chaque assembly appelant dans le répertoire des assemblys à appeler :
-
répertoire \bin dans une application ASP.NET.
-
répertoire \bin\debug ou \bin\release dans une application WinForm.
3.3 - Shared assembly ou assembly avec nom fort.
Ce type d’assembly est copié à un seul endroit (en général dans le Global Assembly Cache, le GAC).
Tous les assemblys d’une même application appelant une même assembly avec nom fort utilisent la même copie de cette assembly depuis son emplacement d’origine.
Ainsi, les assemblys avec nom fort ne sont pas copiés dans chacun des répertoires privés de chaque assembly appelant.
Un assembly avec nom fort possède un nom qualifié complet qui comprend
-
son nom,
-
sa culture,
-
sa clé publique,
-
son numéro de version
-
optionnellement l’architecture de processeur.
MaLibrairie, Version=1.0.500.0, Culture=fr-FR, PublicKeyToken=b77a5c561934e089c
La clé publique et les informations de version rendent donc pratiquement impossible la confusion entre 2 assemblys de même nom ou de même numéro de version.
Un assembly peut contenir dans un seul fichier ou être réparti dans plusieurs fichiers.
Dans ce cas, un seul module maitre contient le manifeste et les autres modules n’en contiennent pas.
3.4 – Le Global Assembly Cache : GAC
Le Global Assembly Cache est un cache de code à l’échelle de l’ordinateur sur lequel est installé le CLR.
Le Global Assembly Cache stocke les assemblys destinés à être partagés entre plusieurs applications sur l'ordinateur.
Vous ne devez partager des assemblys en les installant dans le Global Assembly Cache qu'en cas de nécessité.
En règle générale, vous devez garder les dépendances d'assembly privées et rechercher les assemblys dans le répertoire de l'application, à moins que le partage d'un assembly soit explicitement requis.
On place un assembly dans le GAC :
-
soit par l’utilisation d’un programme d’installation prévu pour le GAC
-
soit par l’utilitaire gacutil.exe fourni avec le SDK de Windows
-
soit par copier/coller avec l’explorateur Windows vers le répertoire du GAC
Les assemblys déployés dans le Global Assembly Cache doivent avoir un nom fort.
Pour lister les assemblys contenus dans le GAC, on peut ouvrir une invite de commande Visual Studio (depuis le menu Démarrer, Visual Studio 2010, Visual Studio Tools) et lancer la commande suivante :
gacutil –l
Cette commande affiche plus de 2500 assemblys placés dans le GAC pour le framework .NET v4.0 !
Les principales librairies présentes dans le répertoire du GAC sont optimisées.
En effet, elles ont toutes été compilées en code natif lors de l’installation du framework sur la machine.
Etant donné que le compilateur JIT optimise la compilation en code natif par rapport à la configuration de l’ordinateur sur lequel il est installé, ces librairies sont donc compilées avec une optimisation pour l’ordinateur sur lequel elles ont été installées.
Une même librairie de même version pourra donc présenter, une fois compilée, un code natif différent d’un ordinateur à un autre.
3.5 - Structure d’une assembly .NET
Soit l’application console Module1.exe générée à partir du code source suivant vu auparavant:
Public Class Appli1
Public Sub Main()
Console.WriteLine(« Bonjour ! »)
Console.Read()
End Sub
End Class
Compilation depuis VS ou avec ’vbc.exe module1.vb’
Le résultat est un exécutable standard Windows (un Portable Executable ou PE) dont la structure générale est la suivante :
PE Header
|
CLR Header
|
Manifeste
|
Métadonnées
|
Code IL
|
Le PE Header contient les informations standard pour Windows et permet à Windows de reconnaitre l’assembly en tant que programme exécutable.
Dans cette section, figure un code qui indique qu’il faut charger la DLL mscoree.dll (MicroSoft Component Object Runtime Execution Engine). Cette DLL va ensuite déterminer quel type et version du CLR elle doit charger.
Le CLR prend ensuite la main.
Il charge les métadonnées dans le CLR Header, recherche le point d’entrée (par exemple la fonction Main() ), compile le code IL de cette fonction avec son compilateur JIT en code natif du CPU et l’exécute.
Pendant la compilation du code, le JIT vérifie le code IL, d’où la notion de code managé.
S’il détecte des opérations non vérifiables telles que accès direct à la mémoire, il refusera d’exécuter l’application.
Le C# permet de créer du code unsafe qui interdit au JIT d’effectuer ces contrôles. VB ne le permet pas.
L’utilitaire PEVerify.exe permet d’effectuer ces vérifications manuellement.
Syntaxe : PEVerify.exe <nom du PE à tester>
Le CLR gère une table qui contient des pointeurs vers les fonctions inclues dans l’application.
Au chargement de l’application, le CLR remplit cette table avec des pointeurs vers le code IL de l’application.
Lorsqu’une fonction est appelée pour la première fois, le CLR la compile en code natif, puis remplace le contenu du pointeur correspondant non plus vers le code IL, mais vers le code compilé en code natif.
Lors des appels ultérieurs à cette fonction, le CLR appellera directement la version compilée.
C’est pour cette raison que l’on observe un temps d’exécution plus lent lors du premier appel à une fonction.
Il est possible de compiler l’application entière en code natif avec l’utilitaire ngen.exe. Cela permet d’éviter les pertes de vitesse dues à la première compilation de chaque fonction.
Cette opération peut être envisageable si l’application en question n’est utilisée que sur l’ordinateur sur lequel elle est compilée. En effet, comme nous l’avons vu à propos des librairies dans le chapitre du GAC, le CLR optimise le code natif par rapport à l’ordinateur sur lequel s’exécute la compilation.
Le fait de compiler l’application en code natif sur un ordinateur A et de copier cette image sur un ordinateur B annule toute l’optimisation qui pourrait être faite sur l’ordinateur B et peut même entrainer un plantage de l’application.
Le Manifeste est une synthèse des informations contenues dans les métadonnées.
On peut visualiser son contenu grâce à l’outil ILDASM.exe
Le Manifeste indique :
-
La liste des fichiers qui composent l’assembly (1)
-
La liste des autres fichiers nécessaires au fonctionnement de l’assembly (2)
-
La liste des types exportés de l’assembly
-
Le nom, la version et la culture de l’assembly
-
Les informations de nom fort si l’assembly est signé
-
Les informations sur le point d’entrée de l’assembly
Pour tout assembly, il n’existe qu’un seul Manifeste.
Pour un assembly composé de plusieurs fichiers, un seul contiendra le manifeste.
Les métadonnées contiennent des informations détaillées sur le contenu de l’assembly
Elles indiquent en détail :
-
La description de l’assembly (nom, version, …) telle que synthétisée dans le manifeste.
-
La description des assemblys externes utilisés par l’assembly.
-
La description des fichiers composant l’assembly (si assembly multi-fichiers).
-
La description de tous les types contenus dans l’assembly ainsi que leurs membres.
Toutes ces informations permettent au compilateur JIT d’effectuer les vérifications du code IL avant la compilation en code natif. Si le compilateur JIT détecte l’utilisation d’un type qui n’est pas représenté dans les métadonnées, il refusera systématiquement l’exécution de l’assembly.
Les métadonnées fixent en quelques sortes les règles que l’assembly doit respecter afin de pouvoir s’exécuter.
On peut visualiser son contenu grâce à l’outil ILDASM.exe (menu Afficher, Méta-informations, Afficher)
Dans cette fenêtre, on peut rechercher la rubrique relative à notre méthode Main() grâce au menu de recherche.
Prenons par exemple l’application TP010105ProjetCS dont le code principal est le suivant :
usingSystem;
static class MaClasse
{
static void Main()
{
Console.WriteLine("Salut en CS");
Console.Read();
}
}
Ouvrons 1.5.2-UniqueCS dans ILDASM. La recherche de Main nous indique :
TypeDef #1 (02000002)
-------------------------------------------------------
TypDefName: MaClasse (02000002)
Flags : [NotPublic] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100000)
Extends : 01000001 [TypeRef] System.Object
Method #1 (06000001) [ENTRYPOINT]
-------------------------------------------------------
MethodName: Main (06000001)
Flags : [Private] [Static] [HideBySig] [ReuseSlot] (00000096)
RVA : 0x00002050
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: Void
No arguments.
On retrouve bien les éléments que nous avons indiqué dans le code C# :
Le nom de la méthode : Main
le type static de la fonction
le type de retour void
le modificateur d’accès Private qui a été placé par défaut par le compilateur
Si on modifie explicitement ce modificateur d’accès en Public dans le code, on régénère le projet et on ouvre l’assembly avec ILDASM, nous aurons cette fois la rubrique suivante :
Method #1 (06000001) [ENTRYPOINT]
-------------------------------------------------------
MethodName: Main (06000001)
Flags : [Public] [Static] [HideBySig] [ReuseSlot] (00000096)
RVA : 0x00002050
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: Void
No arguments.
Dans l’application 1.5.2-UniqueCS, ajoutons maintenant une seconde méthode MaMethode:
usingSystem;
static class MaClasse
{
private static void Main()
{
Console.WriteLine("Salut en CS");
Console.Read();
}
public Int32 MaMethode(Int16 x, Byte y)
{
Int32 z = x + y;
return z;
}
}
Les métadonnées vues depuis ILDASM nous affichent maintenant :
Method #1 (06000001) [ENTRYPOINT]
-------------------------------------------------------
MethodName: Main (06000001)
Flags : [Private] [Static] [HideBySig] [ReuseSlot] (00000091)
RVA : 0x00002050
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: Void
No arguments.
Method #2 (06000002)
-------------------------------------------------------
MethodName: MaMethode (06000002)
Flags : [Public] [HideBySig] [ReuseSlot] (00000086)
RVA : 0x00002064
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
hasThis
ReturnType: I4
2 Arguments
Argument #1: I2
Argument #2: UI1
2 Parameters
(1) ParamToken : (08000001) Name : x flags: [none] (00000000)
(2) ParamToken : (08000002) Name : y flags: [none] (00000000)
On se rend donc bien compte que les métadonnées décrivent avec précision les types ainsi que la signature de chacune de leurs méthodes, de leurs paramètres et de leur valeur de retour.
Le détail de ses métadonnées rend donc la taille de cette section très importante.
On voit ici que les métadonnées occupent 4 à 5 fois plus de place que le code par lui-même !
La taille occupée par les métadonnées dans un assembly peut représenter plus de la moitié de la taille de l’assembly !
C’est le prix à payer pour disposer d’une application rendue plus sûre.
Les métadonnées sont aussi utilisées par Visual Studio afin de mettre à disposition l’IntelliSense. Grace aux métadonnées, l’éditeur de code de VS connait à l’avance la signature des types et des fonctions que vous souhaitez utiliser.
3.6 – Les Espaces de noms
Les Espaces de noms ou Namespaces permettent de regrouper logiquement des types.
Un Espaces de noms n’est pas un type.
Un Espaces de noms n’étant pas un type, on ne peut pas lui appliquer un modificateur d’accès. Il est traité comme s’il disposait d’un accès Public.
Si l’espace de noms n’est pas déclaré explicitement dans le code, un Espaces de noms par défaut est automatiquement créé en prenant comme nom celui indiqué dans les propriétés du projet, par défaut avec le nom du projet en cours.
Il est possible d’imbriquer les Espaces de noms à l’intérieur d’autres Espaces de noms.
Aucune limite n’est fixée quant au nombre de niveaux d’imbrication.
Déclaration d’un espace de noms
VB
NamespaceMonEspaceDeNoms
code
…
EndNamespace
C#
namespaceMonEspaceDeNoms
{
code
…
}
L’instruction Namespace ne peut être utilisée qu’au niveau d’un fichier ou d’un autre namespace.
On ne peut donc pas déclarer un Namespace à l’intérieur d’une classe, d’une structure, d’un module, d’une interface ou d’une procédure.
Prenons l’exemple d’un développeur qui crée un assembly dans lequel il expose des fonctions dédiées au dessin et d’autres aux fichiers, il pourrait très bien regrouper les fonctions correspondantes dans :
un Namespace nommé MesTypesGraphiques
un Namespace nommé MesTypesFichiers.
Le code pourrait ressembler à celui-ci :
VB
NamespaceMesTypesGraphiques
Public Class MaClasseGraphique1
PublicShared Function MaFonctionGraphique() As Integer
Return 1
End Function
End Class
EndNamespace
C#
namespace MesTypeGraphiques
{
public class MaClasseGraphique1
{
public static int MaFonctionGraphique1() { return 1; }
}
}
Pour utiliser les types définis dans les namespaces, on doit auparavant les référencer dans le fichier depuis lequel on effectue l’appel. Par Exemple :
VB
Imports[namespace de l’assembly].MesTypesGraphiques
PublicClass Class2
Public Sub Test()
MaClasseGraphique1.MaFonctionGraphique()
EndSub
EndClass
C#
usingMesTypeGraphiques;
class Class2
{
public void Test()
{
MaClasseGraphique1.MaFonctionGraphique1();
}
}
Dans les métadonnées, la fonction MaFonctionGraphique1 est maintenant préfixée par l’espace de noms MesTypesGraphiques.
On peut contrôler cela avec ILDASM :
TypeDef #3 (02000004)
-------------------------------------------------------
TypDefName: MesTypeGraphiques.MaClasseGraphique1 (02000004)
Flags : [Public] [AutoLayout] [Class] [AnsiClass] [BeforeFieldInit] (00100001)
Extends : 01000001 [TypeRef] System.Object
Method #1 (06000006)
-------------------------------------------------------
MethodName: MaFonctionGraphique1 (06000006)
Flags : [Public] [Static] [HideBySig] [ReuseSlot] (00000096)
RVA : 0x00002094
ImplFlags : [IL] [Managed] (00000000)
CallCnvntn: [DEFAULT]
ReturnType: I4
No arguments.
Afin d’éviter les confusions au niveau des espaces de noms, Microsoft préconise d’utiliser un nom unique comme nom de namespace principal.
Exemple : MaSociete.ThemePrincipal.ThemeSecondaire…
Les types appartenant à un même espace de noms peuvent être compilés dans des assemblys différents.
Afin de pouvoir utiliser les types exposés dans l’espace de noms MaSociete.Fichiers, il faut ajouter le fichier Assembly1.dll dans les références du projet et référencer (par la directive using en C# ou Imports en VB) l’espace de nom MaSociete.Fichiers dans chaque fichier ou l’ou souhaite utiliser les types F1, F2 ou F3.
Afin de pouvoir utiliser les types G1 à G5 exposés dans l’espace de noms MaSociete.Graphisme, il faut ajouter les 2 fichiers Assembly1.dll et Assembly2.dll dans les références du projet et référencer (par la directive using en C# ou Imports en VB) l’espace de nom MaSociete.Graphisme dans chaque fichier ou l’ou souhaite utiliser les types G1 à G5.
3.7 – Références externes
3.7.1 - Création d’une librairie
Pour créer une librairie de classes, on a 2 solutions :
-
Créer le projet en utilisant le modèle librairie de classes
-
Soit en modifiant les propriétés du projet actuel (onglet Application, type de sortie).
3.7.2 - Utiliser une référence externe
Pour pouvoir utiliser une librairie, l’assembly appelant à d’abord besoin de connaitre l’emplacement du fichier qui contient l’assembly de cette librairie.
On doit donc :
-
Référencer l’assembly au niveau de l’assembly appelant
-
Référencer les espaces de noms à utiliser au niveau du fichier source appelant
Référencer l’assembly au niveau de l’assembly appelant
En VB, on affiche les propriétés du projet, Onglet Références et clic sur ajouter
En C#, clic droit sur le dossier Références dans l’explorateur de solutions, puis ajouter une référence
On a ensuite le choix entre 4 sortes de référencement :
.NET
Affiche la liste de toutes les librairies présentes dans le framework. Ces librairies sont placées dans le GAC.
COM
Affiche la liste de tous les composants COM disponibles sur le système. Cette liste ne se limite donc pas aux composants Microsoft (ex : Adobe Acrobat Type Library)
Projets
Affiche la liste des projets contenus dans la solution
Parcourir
Permet de charger explicitement un assembly situé sur le disque dur.
A ce stade, le compilateur sait comment accéder à l’assembly. Cette information sera stockée dans les métadonnées et le manifeste comme nous l’avons vu auparavant.
S’il s’agit d’une référence externe qui n’est pas dans le GAC, le fichier correspondant sera copié dans le répertoire des références de l’application :
Si projet Console ou WinForms, dans le repertoire bin/debug ou bin/release
Si projet application Web, dans le répertoire bin
Référencer les espaces de noms à utiliser au niveau du fichier source appelant
Pour que le code contenu dans un fichier source puisse accéder aux types contenus dans l’assembly externe, il faut ajouter explicitement au début de ce fichier le nom du namespace contenu dans cet assembly qui contient les types que l’on souhaite utiliser.
On déclare cela avec les directives suivantes
En VB : Imports espace_de_nom
En C# : Using espace_de_nom ;
Les types définis dans l’espace de nom référencé par ces directives permettent ensuite d’utiliser les types qu’il contient.
Une référence pouvant contenir plusieurs espaces de noms, il faut ajouter une directive Imports ou Using pour chacun des espaces de noms que l’on souhaite utiliser.
Il n’y a pas de limite quant au nombre de références et d’espaces de noms externes utilisables.
TP
VB.NET
Créer une solution TP0301VB avec un projet application console TP0301VBApp et un projet librairie de classe TP0301VBLib (avec 1 classe publique qui contient 1 champ public)
Référencer la librairie dans l’appliTP0301VBApp par l’option Projet.
Modifier la librairie TP0301VBLib (ajouter un champ), générer et voir les conséquences sur l’appli.
Créer une solution TP0302VB avec seulement une appli TP0302VBApp qui appelle la librairie TP0301VBLib par l’option Parcourir.
Modifier la librairie TP0301VBLib, générer et voir les conséquences sur l’appli.
Dans l’appli TP0302VBApp, tester l’ajout d’une référence COM (exemple Adobe Acrobat Type Library ).
Pendant l’écriture de la directive Imports (ou using en C#), observer que VS nous propose l’arborescence des espaces de noms de cette librairie. Importer l’espace de noms Acrobat.PDDocFlags ou autre si pas présent.
Créer une variable qui va utiliser un type contenu dans cet espace de noms et l’afficher.
C#
Créer une solution TP0301CS avec un projet application console TP0301CSApp et un projet librairie de classe TP0301CSLib (avec 1 classe publique qui contient 1 champ public)
Référencer la librairie dans l’appliTP0301CSApp par l’option Projet.
Modifier la librairie TP0301CSLib (ajouter un champ), générer et voir les conséquences sur l’appli.
Créer une solution TP0302CS avec seulement une appli TP0302CSApp qui appelle la librairie TP0301CSLib par l’option Parcourir.
Modifier la librairie TP0301CSLib, générer et voir les conséquences sur l’appli.
Dans l’appli TP0302CSApp, tester l’ajout d’une référence COM (exemple Adobe Acrobat Type Library ).
Pendant l’écriture de la directive Imports (ou using en C#), observer que VS nous propose l’arborescence des espaces de noms de cette librairie. Importer l’espace de noms Acrobat.PDDocFlags ou autre si pas présent.
Créer une variable qui va utiliser un type contenu dans cet espace de noms et l’afficher.