Michel Perfetti
Mapping de données par attributs: comment éviter les pertes de performance grâce à la génération de MSIL à l'exécution
Cet article présente une classe qui permet le mapping par attributs sur un IDataReader en générant à l'exécution du code MSIL spécifique à la classe à
Par Michel Perfetti publié le 27/09/2005 à 23:03, lu 5772 fois, 5 pages
 3 | Génération du mapping
Téléchargez le code source - 11 Kb
Génération du mapping
Les 3 arguments de la fonction de mapping sont les suivants :
  • La liste à remplir : Ldarg.0 (Load Argument 0)
  • L'objet IDataReader: Ldarg.1
  • Les index de champs : Ldarg.2
La génération du mapping est réalisée par la fonction ILGenerator qui permet de créer le code d'une méthode en ajoutant des opcodes les uns à la suite des autres. Il simplifie aussi la création de labels et de variables locales.

Mapper<T>::GenerateCollectionFromDataReader
Cette méthode génère le squelette de la méthode de mapping. Elle utilise ensuite la méthode GenerateIL de chaque MappedMember pour charger l'objet à mapper avec les données venant de l'objet IDataReader.

Voici le schéma de fonctionnement de la méthode. Les opcodes générés sont écrit du haut vers le bas, et les flèches indiquent le sens du flux d'exécution des opcodes. Les champs en oranges contiennent du code global à plusieurs algorithmes et sont expliqués à part. Pour gagner en lisibilité dans les schémas, le code des opcodes n'est pas vraiment conforme à ce que l'on devrait écrire avec un vrai assembleur. Voici un petit pense-bête sur le MSIL avant de rentrer dans le vif du sujet :
  • Ldarg.n charge les arguments de la méthode dans la pile. N = 0 pour la collection, n = 1 pour le IDataReader et n = 2 pour le tableau d'index
  • Ldloc V charge la valeur de la variable locale V dans la pile, et Stloc charge le contenue du haut de la pile dans la variable locale V
  • Le schéma de pile d'une méthode est toujours le suivant : objet, parametres1, parametres2...
  • Les données sont sorties de la pile avant d'être traitées
  • Br* sont les instructions de branchement
Voici donc le schéma :




Et maintenant le code :
private static CreateCollectionFromDataReaderDelegate GenerateCollectionFromDataReader()
{

    dynamicMethodCreateCollectionFromDataReader = 
        new DynamicMethod("CreateCollectionFromDataReader", 
                            typeof(void), 
                            new Type[] { typeof(ICollection<T>), 
                                        typeof(IDataReader), 
                                        typeof(int[]) }, typeof(T));

    ILGenerator ILout = dynamicMethodCreateCollectionFromDataReader.GetILGenerator();
    MethodInfo DataReaderRead = typeof(IDataReader).GetMethod("Read");

    MethodInfo CollectionAdd = typeof(ICollection<T>).GetMethod("Add");
    ConstructorInfo TConstructor = typeof(T).GetConstructor(new Type[0]);
    LocalBuilder objectToMap = ILout.DeclareLocal(typeof(T));
    LocalBuilder memberIndex = null;

    // Mark the datareader.read() with a label for looping
    Label LoopLabel = ILout.DefineLabel();
    Label EndLabel = ILout.DefineLabel();
    ILout.MarkLabel(LoopLabel);

    // calling IDataReader.Read(), if false go to EndLabel
    ILout.Emit(OpCodes.Ldarg_1);
    ILout.Emit(OpCodes.Callvirt, DataReaderRead);
    ILout.Emit(OpCodes.Brfalse, EndLabel);



    // constructing the objet
    ILout.Emit(OpCodes.Newobj, TConstructor);
    ILout.Emit(OpCodes.Stloc, objectToMap);

    for (int index = 0; index < classEntryToMapArray.Length; ++index)
    {
        MappedMember member = classEntryToMapArray[index];
        
        // Push the objet from the local to the stack

        member.GenerateIL(ILout, index, objectToMap, ref memberIndex);                
    }

    // pushing the list on the stack
    ILout.Emit(OpCodes.Ldarg_0);

    // adding the object on the map;
    ILout.Emit(OpCodes.Ldloc, objectToMap);
    ILout.Emit(OpCodes.Callvirt, CollectionAdd);

    // go back to IDataReader.Read()
    ILout.Emit(OpCodes.Br, LoopLabel);

    // the end of the function
    ILout.MarkLabel(EndLabel);
    ILout.Emit(OpCodes.Ret);

    return (CreateCollectionFromDataReaderDelegate)dynamicMethodCreateCollectionFromDataReader.CreateDelegate(
            typeof(CreateCollectionFromDataReaderDelegate));
}
Ensuite en fonction du type du membre, une des 3 fonctions suivantes de MappedMember est appelée :
  • GenerateILForMember pour un membre qui n'est ni Nullable<> et dont aucune entrée sera nulle
  • GenerateILForMemberWithIgnoringNullValuePour un membre qui n'est pas Nullable<> et pour qui il faut ignorer les champs nulls
  • GenerateILForNullableMemberPour les champs Nullables<>
La méthode renvoie directement le délégué qui sera utilisé par les méthodes publiques.
Mapper<T>::MappedMember::GenerateILForMember
C'est le cas le plus simple :



La récupération de la valeur dans l'objet IDataReader et son stockage dans le champ sont expliqués un peu plus bas.

Voici le code :
private void GenerateILForMember(ILGenerator ILout, int index, LocalBuilder objectToMap, LocalBuilder fieldIndex)
{
    // pushing the object to fill on the stack
    ILout.Emit(OpCodes.Ldloc, objectToMap);

    // pushing the datareader on the stack
    ILout.Emit(OpCodes.Ldarg_1);

    // Getting the ordinal value for the method/field
    ILout.Emit(OpCodes.Ldarg_2);
    ILout.Emit(OpCodes.Ldc_I4, index);
    ILout.Emit(OpCodes.Ldelem_I);

    GenerateILToGetDataFromDataReader(ILout, index, entryValueType);

    GenerateILToStoreDataInMember(ILout);
}
Mapper<T>::MappedMember::GenerateILForMemberWithIgnoringNullValue
Cette partie est un peu plus compliquée car il faut charger la valeur de l'objet IDataReader si et seulement si cette valeur n'est pas nulle. Pour cela la méthode IsDBNull permet de connaître la nullité de la valeur et de l'ignorer par un branchement si nécessaire :




Voici le code :
private void GenerateILForMemberWithIgnoringNullValue(    ILGenerator ILout, 
                                                        int index, 
                                                        LocalBuilder objectToMap, 
                                                        ref LocalBuilder memberIndex)
{
    MethodInfo DataReaderIsDBNull = typeof(IDataRecord).GetMethod("IsDBNull");
    if (memberIndex == null)
    {
        memberIndex = ILout.DeclareLocal(typeof(int));
    }
    Label IgnoreNullLabel = ILout.DefineLabel();

    // pushing the datareader on the stack
    ILout.Emit(OpCodes.Ldarg_1);

    //Retrieving the field index                    
    ILout.Emit(OpCodes.Ldarg_2);
    ILout.Emit(OpCodes.Ldc_I4, index);
    ILout.Emit(OpCodes.Ldelem_I);


    //Storing it in the fieldIndex local variable
    ILout.Emit(OpCodes.Dup);
    ILout.Emit(OpCodes.Stloc, memberIndex);

    // Is NULL?
    ILout.Emit(OpCodes.Callvirt, DataReaderIsDBNull);

    // If yes go to IgnoreNullLabel
    ILout.Emit(OpCodes.Brtrue, IgnoreNullLabel);

    // pushing the object to fill on the stack
    ILout.Emit(OpCodes.Ldloc, objectToMap);

    // pushing the datareader on the stack
    ILout.Emit(OpCodes.Ldarg_1);

    // Getting the ordinal value for the method/field
    ILout.Emit(OpCodes.Ldloc, memberIndex);

    GenerateILToGetDataFromDataReader(ILout, index, entryValueType);

    GenerateILToStoreDataInMember(ILout);

    ILout.MarkLabel(IgnoreNullLabel);
}
Comme nous pouvons le voir, la variable memberIndex n'est crée que si elle est vraiment nécessaire au fonctionnement de la méthode générée. Cela évite la création d'une variable inutile.
Mapper<T>::MappedMember::GenerateILForNullableMember
Cette partie était la plus compliqué à cause de la classe générique Nullable :
  • Nullable<> n'a pas de constructeur pour des données nulles
  • Dans ce cas ci, il ne faut pas récuperer de l'objet IDataReader un objet de type Nullable<T> mais simplement T
Pour le deuxieme problème, les fonctions de réflexions ou Ildasm suffisent à resoudre le problème. Pour le premier cas, l'utilisation de Reflector et la lecture de MSIL généré pour la création d'un Nullable<> nul permet de résoudre le problème. Voila comment fait le compilateur pour mettre sur la pile un Nullable<> null :
  • Chargement de l'adresse de la variable locale associée à l'objet Nullable<> grace à Ldloca
  • Initialisation de la variable à 0 avec Initobj
  • Chargement de la variable dans la pile par Ldloc.x
Et c'est exactement cela que nous allons faire :




Voici le code de ma méthode :
private void GenerateILForNullableMember(    ILGenerator ILout, 
                                            int index, 
                                            LocalBuilder objectToMap,
                                            ref  LocalBuilder memberIndex,
                                            Type nullableType)
{
    MethodInfo DataReaderIsDBNull = typeof(IDataRecord).GetMethod("IsDBNull");
    ConstructorInfo NullableConstructor = entryValueType.GetConstructor(new Type[] { nullableType });                            
    LocalBuilder nullLocal = ILout.DeclareLocal(entryValueType);
    if (memberIndex == null)
    {
        memberIndex = ILout.DeclareLocal(typeof(int));
    }

    Label BeginIsNullBlockLabel = ILout.DefineLabel();
    Label EndIsNullBlockLabel = ILout.DefineLabel();

    #region Is Null?
    // pushing the datareader on the stack
    ILout.Emit(OpCodes.Ldarg_1);

    //Retrieving the field index                    
    ILout.Emit(OpCodes.Ldarg_2);
    ILout.Emit(OpCodes.Ldc_I4, index);
    ILout.Emit(OpCodes.Ldelem_I);

    //Storing it in the fieldIndex local variable
    ILout.Emit(OpCodes.Dup);
    ILout.Emit(OpCodes.Stloc, memberIndex);

    // Is NULL?
    ILout.Emit(OpCodes.Callvirt, DataReaderIsDBNull);
    #endregion

    // If yes go to BeginIsNullBlockLabel
    ILout.Emit(OpCodes.Brtrue, BeginIsNullBlockLabel);

    #region Is Not Null
    // pushing the object to fill on the stack
    ILout.Emit(OpCodes.Ldloc, objectToMap);

    // pushing the datareader on the stack
    ILout.Emit(OpCodes.Ldarg_1);

    // Getting the ordinal value for the method/field
    ILout.Emit(OpCodes.Ldloc, memberIndex);

    GenerateILToGetDataFromDataReader(ILout, index, nullableType);

    ILout.Emit(OpCodes.Newobj, NullableConstructor);

    // To branch after the null instruction block
    ILout.Emit(OpCodes.Br, EndIsNullBlockLabel);

    #endregion

    #region Is Null
    ILout.MarkLabel(BeginIsNullBlockLabel);

    // pushing the object to fill on the stack
    ILout.Emit(OpCodes.Ldloc, objectToMap);

    ILout.Emit(OpCodes.Ldloca, nullLocal.LocalIndex);
    // Instantiate a Nullable<T> with no value
    ILout.Emit(OpCodes.Initobj, entryValueType);
    ILout.Emit(OpCodes.Ldloc, nullLocal);

    #endregion

    ILout.MarkLabel(EndIsNullBlockLabel);
    GenerateILToStoreDataInMember(ILout);

    

}
Mapper<T>::MappedMember::GenerateILToGetDataFromDataReader
Cette fonction est chargée de trouver la bonne méthode de IDataRecord pour lire le membre dans l'objet IDataReader. En effet les méthodes GetXXX ne font pas partie de l'interface IDataReader, mais IDataRecord. Ce qui veut dire qu'une recherche dans IDataReader n'aurait rien trouvé :
private void GenerateILToGetDataFromDataReader(ILGenerator ILout, int index, Type memberType)
{
    MethodInfo methodInfo = Tools.getDataReaderMethod(memberType);
    if (methodInfo == null)
    {
        throw new Exception("Type not handled: " + classEntryToMapArray[index].EntryValueType.FullName);
    }
    ILout.Emit(OpCodes.Callvirt, methodInfo);
}
Mapper<T>::MappedMember::GenerateILToStoreDataInMember
Cette méthode charge dans l'objet à mapper la valeur sur la pile, en fonction du type du membre : propriété ou champ.
private void GenerateILToStoreDataInMember(ILGenerator ILout)
{

    switch (entryType)
    {
        case MappedMemberType.field:
            ILout.Emit(OpCodes.Stfld, fieldInfo);
            break;
        case MappedMemberType.property:
            MethodInfo methodInfo = propertyInfo.GetSetMethod();
            ILout.Emit(OpCodes.Callvirt, methodInfo);
            break;
    }
}
 
» Démarrer une discussion
 
Discussion démarée par MickyMax le 18/11/2007 à 14:44, 1 commentaire(s).