Formation implémenter ETW en .NET, tutoriel & guide de travaux pratiques en pdf.
Import de l’API ETW
La première chose à faire est d’écrire une classe pour appeler les fonctions de l’API ETW. Nous n’écrivons que le provider ETW, on peut se limiter à quelques fonctions. Il n’est pas nécessaire de convertir tout le fichierEvnTrace.h
Respect des structures physiques
La plupart des fonctions des API Win32 utilisent des structures en paramètre. Ces structures doivent être déclarées et initialisées en .NET. Lorsqu’elles sont transmises à une API native, elles ne sont pas converties. Aussi, il faut que le format de stockage en mémoire de la structure .NET corresponde strictement à ce qu’attend le code natif, à l’octet près.
Il faut respecter l’alignement de chaque champ et choisir des types de données CLR qui correspondent aux types de données natifs.
Il faut également être sûr que le compilateur traitera nos structures telles qu’elles sont déclarées, sans ajouter d’octets de calage pour aligner les données en mémoire. Pour cela, on marque chaque structure avec l’attribut [StructLayout(LayoutKind.Sequential)].
Utilisation de pointeurs : code unsafe
Certaines structures ou fonctions utilisent des pointeurs. C’est par exemple le cas de TRACE_GUID_REGISTRATION. Hors les pointeurs sont interdits lorsqu’on écrit du code managé. Chaque fois qu’on a besoin d’utiliser un pointeur, on doit utiliser du code ditunsafe.
Le code unsafe est tout simplement du code que la CLR n’est pas en mesure de contrôler. Lorsqu’on utilise des pointeurs, la CLR ne peut pas savoir ce qu’on fait avec. Elle ne peut pas détecter les débordements par exemple. C’est pourquoi, le codeunsafe est à éviter autant que possible en temps normal.
Par défaut, le compilateur interdit le code unsafe dans une assembly. Pour pouvoir compiler du code unsafe, il faut commencer par l’autoriser au niveau du projet.
Dans les options du projet, il faut cocher la checkbox Allow unsafe code sur l’ongletBuild :
Le provider ETW
Pour écrire le provider, nous allons simplement porter la classe TGenericLogger de l’implémentation Delphi en C#.
Comme pour TGenericLogger, c’est le constructeur de la classe qui va s’occuper d’appelerRegisterTraceGuids.
/// <summary>
/// Crée et initialise une nouvelle instance de la classe GenericLogger.
/// Déclare le provider comme fournisseur d’événements ETW.
/// </summary>
/// <param name= »pProviderGUID »>GUID identifiant le provider ETW</param>
/// <param name= »pEventClassGuid »>GUID définissant l’EventClass des événements
/// qui seront générés.</param>
public GenericLogger(Guid pProviderGUID, Guid pEventClassGuid)
{
// On commence par mémoriser la classe d’événement à générer. eventClass = pEventClassGuid;
// On va devoir passer une référence à l’instance dans le Context de
// la méthode RegistrerTraceGuids.
// Il faut qu’on fournisse une référence sur un objet managé à du code non
// managé.
// Le GC est succeptible de déclencher entre l’appel à RegisterTraceGuids
// et l’appel de la méthode ControlCallback. La référence fournie en retour
// par le code non managé risque de ne plus être valide.
// Pour éviter ce problème, on doit utiliser un GCHandle.
loggerHandle = GCHandle.Alloc(this);
unsafe
{
// Il faut définir un tableau de type TRACE_GUID_REGISTRATION pour
// déclarer les eventClass qui seront générés. Ici on ne génère qu’un
// seul eventClass. En guise de tableau, on peut se contenter d’une seule
// structure TRACE_GUID_REGISTRATION.
Etw.TRACE_GUID_REGISTRATION reg;
// On utilise une référence sur le paramètre. Le paramètre ne peut pas
// être collecté par le GC. Par conséquent, il ne peut pas être déplacé
// pendant toute sa durée de vie. C’est pourquoi on peut utiliser son
// adresse directement, sans être obligé d’utiliser un bloc fixed() reg.guid = new IntPtr(&pEventClassGuid);
reg.RegHandle = 0;
// De même, pour définir la méthode callback, il faut passer par un délégué.
// Si on passe ControlCallback directement en paramètre à RegisterTraceGuids,
// le compilateur va passer par un délégué anonyme et fournir ce délégué à
// RegisterTraceGuids.
// Hors comme on ne garde pas de référence sur ce dernier, il a toute les
// chances d’être collecté par le GC et de ne plus être valide lorsque le code
// non managé voudra appeler la méthode callback.
// Pour éviter ce problème, il faut créer explicitement le délégué et
// conserver une référence dessus pendant toute la durée de vie de l’instance. control = new Etw.EtwProc(ControlCallback);
uint result = Etw.RegisterTraceGuids(control, GCHandle.ToIntPtr(loggerHandle), ref pProviderGUID, 1, ref reg, null, null, out hTraceReg);
if (result != Etw.ERROR_SUCCESS)
{
throw new LoggerException(« Erreur lors de l’appel à RegisterTraceGuids »);
}
}
}
Le garbage collector n’est pas ton ami
On doit faire face à trois particularités, toutes liées au GC et à la machine virtuelle.
Tout d’abord, on doit fournir leGuid de l’EventClass sous la forme d’un pointeur sur sa valeur, renseigné dans la structure TRACE_GUID_REGISTRATION.
Pour pouvoir définir cette valeur, on est obligé de passer par du code unsafe.
Le GC pose un problème particulier avec les pointeurs. En effet, à l’issu d’une collecte, le GC peut reloger les objets en mémoire afin que l’application n’utilise que des blocs mémoires continus. Du fait du mode de gestion de la mémoire dans la CLR, cette relocation est même indispensable pour libérer la mémoire inutilisée.
En temps normal, lorsque le GC déplace un objet en mémoire, il se charge également de mettre à jour toutes les références sur cet objet. Mais si on utilise des pointeurs et du code unsafe, le résultat est incertain comme le montre le schéma suivant :
Avant la collecte, la mémoire est utilisée par trois objets : Obj1, Obj2 et Obj3. Obj1 est référencé par R1. Obj3 est référencé par R3. Obj2 n’est plus référencé.
On a définit un pointeur unsafe qui pointe sur Obj3.
Lorsque la collecte déclenche, le GC va détecter que Obj2 n’est plus accessible et va le détruire :
Sauf que s’il s’arrête là, le pointeur du début de la mémoire libre est toujours à la même place. Cela signifie que la mémoire libérée n’est pas utilisable.
Le GC va procéder à un compactage mémoire pour récupérer les trous dans la mémoire. Obj3 est alors déplacé à la suite de Obj1. Les références R1 et R3 sont gérées par du code managé. Le GC va automatiquement les mettre à jour de sorte que la collecte est totalement transparente.
Sauf que les pointeurs unsafe ne peuvent pas être mis à jour automatiquement. Le pointeur unsafe devient invalide, ce qui risque d’entrainer un crash complet de la CLR.
Pour éviter ce problème, le compilateur interdit purement et simplement de définir un pointeur sur un élément susceptible d’être relogé.
Normalement, on ne peut définir un pointeur unsafe qu’à l’intérieur d’un blocfixed. Dans ce cas, les objets pointés sont alors figés en mémoire, ce qui interdit au GC de les déplacer.
Dans l’implémentation Delphi, la valeur de EventClassl’ est mémorisée dans un attribut de la classe et TRACE_GUID_REGISTRATION est initialisée avec une référence sur cet attribut.
En .NET on ne peut pas faire la même chose à cause du GC. La référence ne peut être définie que sur un objet fixed.
On peut cependant s’en sortir facilement avec une petite subtilité : Les valeurs passées en paramètre sont placées sur la pile et non pas dans le tas. De fait, elles ne sont pas gérées par le GC, elles ne risquent pas d’être relogées, elles sont automatiquement fixed. Aussi, au lieu de définir une variable intermédiaire fixed avec la valeur du guid pour ensuite la référencer dans TRACE_GUID_REGISTRATION on peut se contenter de référencer directement le paramètre.
Appel de la fonction callback
L’appel de la fonction callback pose également un problème. Cette dernière est écrite en C#. Il s’agit de code managé qui s’exécute dans la CLR. En revanche, elle doit être appelée en callback par du code natif : ETW.
Cet appel callback n’est pas totalement immédiat. Il s’effectue par l’intermédiaire d’un déléguédelegate(). Ce délégué n’est pas seulement un type permettant de définir le prototype de la méthode appelée. Il s’agit bel et bien d’une classe intermédiaire utilisée pour effectuer l’appel. Elle est instanciée par le compilateur et se charge d’effectuer la transition code natif->code managé.
Si on indique directement la méthode callback lors de l’appel àRegisterTraceGuids, le compilateur va instancier une classe anonyme pour la donner en paramètre à RegisterTraceGuids. Par définition cette dernière n’est pas référencée du côté du code managé.
Seul ETW va conserver une référence qui servira au moment du démarrage ou de l’arrêt de la trace.
Il se peut que le GC déclenche une collecte entre l’appel àRegisterTraceGuids et l’utilisation du délégué par ETW. Si le cas se présente, le délégué aura toutes les chances d’être collecté, sans qu’ETW ne le sache. Au final ETW va essayer d’invoquer du code aléatoirement dans la CLR ce qui aura probablement pour effet de provoquer un crash de cette dernière, ou produira des effets indésirables.
Depuis le Framework 2.0, ce genre d’anomalie est automatiquement détecté grâce aux Managed Debugging Assistants (MDAs).
Lorsque le GC collecte un délégué en débogage, il le remplace par une classe spéciale qui sait que la collecte a été faite, et que le délégué n’est plus valide. Si le délégué est ensuite invoqué depuis le code natif, cette classe va réagir en provoquant l’affichage d’un message d’erreur dans le débogueur. Ainsi on obtient le message suivant :
CallbackOnCollectedDelegate was detected
Message: A callback was made on a garbage collected delegate of type ‘ETW!Fso.Diagnostic.Etw+EtwProc::Invoke’. This may cause application crashes, corruption and data loss. When passing delegates to unmanaged code, they must be kept alive by the managed application until it is guaranteed that they will never be called.
Pour résoudre le problème, la solution est très simple : Il suffit d’instancier le délégué explicitement et de conserver la référence jusqu’à ce queUnregisterTraceGuids soit appelée.
Transmission de l’instance de GenericLogger à la méthode callback
La méthode callback est appelée par ETW pour notifier le provider du démarrage ou de l’arrêt de la trace. Cette méthode est une méthode statique. Cependant, elle doit transmettre cette notification à l’instance du provider. C’est ce qu’elle fait en appelant les méthodesEnableEvents etDisableEvents.
En Win32, on se servait du paramètre Context de RegisterTraceGuids pour transmettre le pointeur de l’instance à la méthode callback.
Avec du code managé, on rencontre à nouveau le même problème qu’avec tout pointeur. Ce dernier risque de devenir invalide si une collecte déclenche et que l’objet est relogé. Sauf que cette fois, il n’est pas possible de fixer l’instance en mémoire. En tout cas, pas avec un bloc fixed.
Pour résoudre le problème, il faut utiliser un GCHandle. Sur le principe, le GCHandle est un moyen que nous fournit le GC pour verrouiller un objet en mémoire et obtenir un pointeur qui restera valide tout au long de la vie du GCHandle. En fait, sans aller jusqu’à verrouiller l’objet en mémoire, GCHandle permet d’associer un handle à un objet. Ce handle est un identifiant qui permettra de retrouver l’instance le moment voulu.
Pour s’en servir, on commence par l’allouer avec la ligne suivante :
loggerHandle = GCHandle.Alloc(this);
Puis on peut le convertir en pointeur IntPtr avec :
IntPtr context = GCHandle.ToIntPtr(loggerHandle);
Context peut alors être passé en paramètre pour appeler du code natif. La méthode callback recevra cette valeur en paramètre. Il ne reste plus qu’à faire l’opération inverse pour retrouver l’instance GenericLoggerde :
GenericLogger log = (GenericLogger) GCHandle.FromIntPtr(context).Target;
Ce handle devra être libéré dans le destructeur avec :
loggerHandle.Free();
Ecriture d’un événement dans la trace
Schéma de l’événement
Nous allons garder sensiblement le même format d’événement que pour l’implémentation Delphi. La seule particularité, c’est que .NET travaillant en unicode pour les chaînes de caractères, le message de l’événement sera également en unicode.
Le fichier MOF décrivant le provider et l’événement est le suivant :
#pragma namespace(« \\\\.\\root\\wmi »)
[dynamic: ToInstance, Description(« .NET Generic Trace »), Guid(« {2F5162CC-79F0-4181-B42F-B98B6CF8FABC} »)]
class NetDefaultProvider : EventTrace
{
};
[dynamic: ToInstance, Description(« MSGEVENT »): Amended, Guid(« {9A327179-7EE6-4186-A579-2BE6BA7B95BE} »), DisplayName(« MSG »),
EventVersion(0)]
class NetDefaultEventClass : NetDefaultProvider
{
};
[dynamic: ToInstance, Description(« MSG »): Amended,
EventType{0, 1, 2, 3},
EventTypeName{« INFO »,
« START »,
« END »,
« ERROR »}]
class NetDefaultEventClass_EventType1 : NetDefaultEventClass
{
[WmiDataId(1), Extension(« Noprint »),
Description(« OperationTime »): Amended, read]
sint64 OperationTime;
[WmiDataId(2), Description(« TextData »): Amended, read,
StringTermination(« NullTerminated »), Format(« w »)]
string TextData;
};
Comme pour l’implémentation Delphi, il faut installer le fichier MOF avec la commande :
Mofcomp nomDuFichier.mof
La structure qui sera fournie à TraceEvent est la suivante :
[StructLayout(LayoutKind.Sequential)]
public struct EVENT_GENERIC
{
public Etw.EVENT_TRACE_HEADER Header;
public Etw.MOF_FIELD OperationTime;
public Etw.MOF_FIELD Message;
}
I – Introduction
I-A – Présentation
I-B – Télécharger les sources de l’article
II – Import de l’API ETW
II-A – Respect des structures physiques
II-B – Utilisation de pointeurs : code unsafe
III – Le provider ETW
III-A – Le garbage collector n’est pas ton ami
III-B – Appel de la fonction callback
III-C – Transmission de l’instance de GenericLogger à la méthode callback
III-D – Ecriture d’un événement dans la trace
III-D-1 – Schéma de l’événement
III-D-2 – Les chaînes de caractères
III-D-3 – Les autres données
IV – Intégration avec System.Diagnostics
IV-A – Framework 3.5 et Windows Vista
V – Evaluation des performances
V-A – Test 1 : Ecriture de 10000 messages
V-B – Test 2 : Dégradation des performances liée à la trace
V-C – Test 3 : System.Diagnostics.Trace
V-D – Test 4 : System.Diagnostics.Trace à vide
VI – Utilisation avec ASP.NET
VII – Référence
VIII – Conclusion