A la découverte de IL

Introduction & historique

Lorsque vous écrivez du code en C# (ou tout autre langage .NET), ce que vous créez n’est pas directement compris par votre machine. Votre code est en réalité traduit en un ‘langage secret’ que l’ordinateur peut comprendre. Ce langage s’appelle le code Intermediate Language (IL), aussi connu sous le nom de Common Intermediate Language (CIL).

Pourquoi cela ? Pour répondre à cette question, il faut remonter à la création de .NET par Microsoft à la fin des années 1990. À cette époque, la plupart des langages de programmation se compilaient en code machine spécifique à la plateforme. Cela signifiait que si vous vouliez que votre programme fonctionne sur des systèmes d’exploitation ou des architectures de processeurs différents, vous deviez le recompiler pour chaque plateforme, parfois en réécrivant même certaines parties du code.

Microsoft a voulu résoudre ce problème en créant un environnement d’exécution qui pourrait exécuter du code sur n’importe quelle plateforme, à condition qu’une version du runtime .NET soit installée. Pour ce faire, ils ont créé le langage IL, qui est une sorte de langage intermédiaire entre le code source et le code machine. Lorsque vous compilez votre code en C#, il est en réalité traduit en IL, qui est ensuite compilé en code machine par le Just-In-Time (JIT) compiler au moment de l’exécution.

Mais l’IL n’était pas seulement une solution au problème de la portabilité du code. Il a également permis la création de plusieurs langages de programmation différents qui pouvaient tous être compilés en IL et exécutés sur la même machine virtuelle. Cela signifie que vous pouvez avoir du code en C#, Visual Basic .NET, F#, et même en Python ou Ruby (grâce à IronPython et IronRuby), tous fonctionnant ensemble dans la même application.

C’est pourquoi comprendre le code IL est si important. Non seulement il vous permet de voir ce qui se passe ‘sous le capot’ lorsque votre code est exécuté, mais il vous donne également une meilleure compréhension de la manière dont les différents langages .NET interagissent entre eux. De plus, en générant du code IL directement, vous pouvez parfois optimiser votre code de manière plus efficace que si vous deviez écrire le même code en C# ou dans un autre langage .NET.

Démystifier le code IL

Maintenant que nous avons une idée de ce qu’est le code IL et pourquoi il est important, il est temps de le déchiffrer. À première vue, le code IL peut ressembler à un mélange confus d’opcodes et de valeurs, mais une fois que vous comprenez comment il fonctionne, il devient beaucoup plus facile à lire.

Le code IL est un langage bas niveau, c’est-à-dire qu’il est plus proche du langage machine que le C#. Il est constitué d’une série d’instructions, également appelées ‘opcodes’, qui sont exécutées en séquence. Chaque opcode effectue une action spécifique, comme charger une valeur en mémoire, effectuer une opération arithmétique ou appeler une méthode.

Prenons par exemple l’instruction ldstr. ldstr est l’opcode IL pour load string, c’est-à-dire qu’il charge une chaîne en mémoire. Si vous avez l’instruction ldstr "Bonjour", cela signifie que la chaîne « Bonjour » est chargée dans la pile d’exécution IL.

Comparons cela à la déclaration d’une variable de chaîne en C#. Si vous écrivez string greeting = "Bonjour"; en C#, vous déclarez une variable greeting et lui attribuez la valeur « Bonjour ». Lorsque vous compilez votre code C#, cette instruction est traduite en IL, qui ressemble à ceci :

IL_0000: ldstr "Bonjour"
IL_0005: stloc.0

Ici, ldstr "Bonjour" charge la chaîne « Bonjour » dans la pile, et stloc.0 la stocke dans la première variable locale, qui correspond à notre variable greeting.

Mais ce n’est pas tout. Le code IL comporte de nombreux autres types d’instructions. Par exemple, call est utilisé pour appeler une méthode, add pour ajouter deux valeurs, brtrue pour sauter à un autre point du code si la valeur actuelle est vraie, et bien d’autres.

Comme vous pouvez le voir, chaque instruction en C# correspond à une ou plusieurs instructions en IL. En apprenant à lire le code IL, vous pouvez obtenir une vision beaucoup plus détaillée de ce qui se passe réellement lorsque votre code C# est exécuté.

Premiers pas avec l’émission de code IL

Pour illustrer la puissance de l’émission de code IL, nous allons prendre un exemple concret d’un cas où l’utilisation de cette technique peut améliorer significativement les performances. Prenons le cas des appels de méthodes via la réflexion.

La réflexion est un outil puissant en .NET qui permet d’inspecter et de manipuler le code à l’exécution. Cependant, son utilisation peut être coûteuse en termes de performances, notamment lorsqu’il s’agit d’appels de méthode. Pour illustrer cela, considérons l’exemple suivant, où nous utilisons la réflexion pour appeler une méthode :

public object CallMethodWithReflection(object target, MethodInfo method)
{
    return method.Invoke(target, null);
}

Si cette méthode est appelée fréquemment dans votre code, les performances peuvent en pâtir. Une solution à cela est d’utiliser l’émission de code IL pour générer un délégué qui effectue l’appel de méthode à votre place, éliminant ainsi le coût de l’appel de méthode via la réflexion. Voici comment cela pourrait être fait :

public static Func<object, object> CreateGetter(MethodInfo method)
{
    var dynamicMethod = new DynamicMethod(
        "CreateGetter" + method.Name,
        typeof(object),
        new Type[] { typeof(object) },
        method.DeclaringType);
        
    var generator = dynamicMethod.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Call, method);
    generator.Emit(OpCodes.Box, method.ReturnType);
    generator.Emit(OpCodes.Ret);

    return (Func<object, object>)dynamicMethod.CreateDelegate(typeof(Func<object, object>));
}

Dans cet exemple, nous créons une méthode dynamique qui prend un objet en argument et renvoie un objet. Nous utilisons ILGenerator pour émettre du code IL qui charge l’argument sur la pile (Ldarg_0), appelle la méthode (Call), convertit le résultat en un objet (Box) et retourne le résultat (Ret).

Avec cette méthode, vous pouvez créer un getter dynamique pour n’importe quelle méthode et l’appeler sans utiliser la réflexion. C’est un moyen simple et efficace d’améliorer les performances de votre application lorsque vous devez effectuer de nombreux appels de méthode dynamiques.

L’émission de code IL vous donne accès à des instructions de bas niveau qui peuvent aller au-delà de ce que vous êtes habitué à faire avec le C#. Avec ILGenerator, vous pouvez émettre non seulement des instructions simples, mais également des structures de contrôle plus complexes comme les boucles et les conditions. Cependant, avec une plus grande puissance vient une plus grande responsabilité. Les erreurs dans le code IL peuvent être difficiles à repérer et peuvent entraîner des comportements inattendus ou même causer des plantages.

Ldarg

Ldarg, qui signifie « Load Argument », est un opcode qui joue un rôle crucial lors de l’émission de code IL. Il permet de charger les arguments d’une méthode sur la pile d’évaluation. Comprendre son fonctionnement requiert une familiarité préalable avec le concept de pile en IL.

La pile en IL est l’endroit où sont stockées toutes les valeurs pendant l’exécution de votre code. Chaque fois que vous effectuez une opération en IL, les valeurs sont prises de la pile et les résultats sont remis dessus.

Ldarg joue un rôle essentiel en déplaçant les arguments d’une méthode sur la pile afin qu’ils soient accessibles pour d’autres opcodes. Illustrons son fonctionnement à travers un exemple concret en C#.

Envisagez le scénario où vous souhaitez créer une méthode dynamique qui concatène « Bonjour,  » à une chaîne d’entrée. Voici comment vous pourriez procéder :

public static Func<string, string> CreateGreeting()
{
    var dynamicMethod = new DynamicMethod(
        "CreateGreeting",
        typeof(string),
        new Type[] { typeof(string) },
        typeof(Program).Module);

    var generator = dynamicMethod.GetILGenerator();
    generator.Emit(OpCodes.Ldstr, "Bonjour, ");
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Call, typeof(string).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }));
    generator.Emit(OpCodes.Ret);

    return (Func<string, string>)dynamicMethod.CreateDelegate(typeof(Func<string, string>));
}

.Dans cet exemple, l’opcode Ldarg_0 est utilisé pour charger le premier (et unique) argument de la méthode sur la pile. Cette valeur est ensuite utilisée par l’opcode Call pour concaténer la chaîne « Bonjour,  » avec l’argument de la méthode.

Il est essentiel de noter que l’indexation des arguments en IL commence à zéro, tout comme pour les tableaux en C#. Ainsi, le premier argument d’une méthode est à l’index 0, le deuxième à l’index 1, etc.

Ldarg possède également des variantes spécifiques pour les indices d’arguments plus petits, tels que ldarg.0, ldarg.1, ldarg.2, et ldarg.3, qui sont utilisés pour charger les arguments aux indices respectifs. D’autres variantes incluent ldarg.s pour charger les arguments avec des indices de 4 à 255 et ldarg pour les indices supérieurs à 255.

Comprendre comment ldarg fonctionne et comment il interagit avec la pile vous offre un aperçu de la manière dont les opcodes IL s’articulent pour former des méthodes complètes.

Ldc

Ldc signifie « Load Constant », et c’est un opcode essentiel qui est utilisé pour charger une constante sur la pile en IL. Que vous travailliez avec des entiers, des flottants ou des longs, Ldc a une variante pour chaque type de constante.

Imaginons que nous voulons créer une méthode dynamique qui multiplie un nombre par deux. Voici comment cela pourrait être réalisé avec ILGenerator en utilisant l’opcode Ldc :

public static Func<int, int> Double()
{
    var dynamicMethod = new DynamicMethod(
        "Double",
        typeof(int),
        new Type[] { typeof(int) },
        typeof(Program).Module);

    var generator = dynamicMethod.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0); // Charger le premier argument
    generator.Emit(OpCodes.Ldc_I4_2); // Charger la constante 2
    generator.Emit(OpCodes.Mul); // Multiplier les deux valeurs en haut de la pile
    generator.Emit(OpCodes.Ret); // Retourner le résultat

    return (Func<int, int>)dynamicMethod.CreateDelegate(typeof(Func<int, int>));
}

Dans cet exemple, nous utilisons Ldc_I4_2 pour charger la constante 2 sur la pile. Cette constante est ensuite utilisée par l’opcode Mul pour multiplier l’argument de la méthode (chargé auparavant par Ldarg_0) par 2.

Ldc a plusieurs variantes pour charger différentes constantes. Par exemple, Ldc_I4 charge un entier 32 bits, tandis que Ldc_I8 charge un entier 64 bits. Il y a aussi Ldc_R4 et Ldc_R8 pour charger respectivement des nombres flottants de 32 bits et de 64 bits.

Il existe également des variantes spécifiques de Ldc_I4 pour les petites constantes. Par exemple, Ldc_I4_0, Ldc_I4_1, Ldc_I4_2, etc., sont utilisées pour charger les constantes de 0 à 8, respectivement. Ldc_I4_M1 est utilisé pour charger la constante -1. Pour les constantes de -128 à 127, il y a Ldc_I4_S. Pour toutes les autres constantes, Ldc_I4 ou Ldc_I8 peuvent être utilisés.

Comprendre Ldc est essentiel pour travailler avec des constantes en IL, et c’est un excellent exemple de la flexibilité qu’offre l’émission de code IL.

Add, Sub, Mul, Div, Rem

Ces opcodes sont les opérations arithmétiques de base en IL. Ils ajoutent, soustraient, multiplient, divisent, ou calculent le reste de deux valeurs. Ces opcodes s’attendent à ce que les deux opérandes soient déjà sur la pile. Ils prennent les deux valeurs du dessus de la pile, effectuent l’opération, et poussent le résultat de retour sur la pile.

Add

L’opcode Add est utilisé pour additionner les deux valeurs en haut de la pile. Un exemple simple serait l’addition de deux nombres.

generator.Emit(OpCodes.Ldc_I4, 5); // Charger le nombre 5 sur la pile
generator.Emit(OpCodes.Ldc_I4, 3); // Charger le nombre 3 sur la pile
generator.Emit(OpCodes.Add); // Ajouter les deux nombres
generator.Emit(OpCodes.Ret); // Retourner le résultat

Dans cet exemple, le code IL additionne 5 et 3 pour donner 8.

Sub

Sub soustrait la valeur en haut de la pile de celle qui est juste en dessous.

generator.Emit(OpCodes.Ldc_I4, 5); // Charger le nombre 5 sur la pile
generator.Emit(OpCodes.Ldc_I4, 3); // Charger le nombre 3 sur la pile
generator.Emit(OpCodes.Sub); // Soustraire le deuxième nombre du premier
generator.Emit(OpCodes.Ret); // Retourner le résultat

Ici, le code IL soustrait 3 de 5 pour donner 2.

Mul

Mul multiplie les deux valeurs en haut de la pile

generator.Emit(OpCodes.Ldc_I4, 5); // Charger le nombre 5 sur la pile
generator.Emit(OpCodes.Ldc_I4, 3); // Charger le nombre 3 sur la pile
generator.Emit(OpCodes.Mul); // Multiplier les deux nombres
generator.Emit(OpCodes.Ret); // Retourner le résultat

Dans cet exemple, le code IL multiplie 5 par 3 pour donner 15.

Div

Div divise la deuxième valeur en haut de la pile par la première valeur en haut de la pile.

generator.Emit(OpCodes.Ldc_I4, 5); // Charger le nombre 5 sur la pile
generator.Emit(OpCodes.Ldc_I4, 2); // Charger le nombre 2 sur la pile
generator.Emit(OpCodes.Div); // Diviser le premier nombre par le second
generator.Emit(OpCodes.Ret); // Retourner le résultat

Ici, le code IL divise 5 par 2 pour donner 2 (puisque nous travaillons avec des entiers).

Rem

Rem calcule le reste de la division de la deuxième valeur en haut de la pile par la première valeur en haut de la pile.

generator.Emit(OpCodes.Ldc_I4, 5); // Charger le nombre 5 sur la pile
generator.Emit(OpCodes.Ldc_I4, 2); // Charger le nombre 2 sur la pile
generator.Emit(OpCodes.Rem); // Calculer le reste de la division du premier nombre par le second
generator.Emit(OpCodes.Ret); // Retourner le résultat

Dans cet exemple, le code IL calcule le reste de la division de 5 par 2 pour donner 1.

Il est essentiel de noter que l’ordre dans lequel les opérations sont effectuées suit la logique de la pile : le premier élément poussé sur la pile est le dernier à être retiré, et vice versa. C’est un concept fondamental en IL et c’est la raison pour laquelle ces opcodes fonctionnent comme ils le font.

Call

Call est un opcode crucial en IL qui est utilisé pour appeler une méthode. Son fonctionnement est directement lié à la pile d’évaluation, qui est un espace de stockage temporaire pour les valeurs pendant l’exécution de votre code.

Dans un contexte plus large, Call déclenche une méthode dont les paramètres proviennent de la pile d’évaluation. Après avoir exécuté la méthode, le résultat, s’il y en a un, est placé de nouveau sur la pile. C’est là que des opcodes comme Ldarg et Ldstr entrent en jeu, car ils sont responsables de charger les valeurs sur la pile.

Prenons l’exemple d’une méthode qui concatène deux chaînes :

public static Func<string, string, string> CreateMessage()
{
    var dynamicMethod = new DynamicMethod(
        "CreateMessage",
        typeof(string),
        new Type[] { typeof(string), typeof(string) },
        typeof(Program).Module);

    var generator = dynamicMethod.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldarg_1);
    generator.Emit(OpCodes.Call, typeof(string).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }));
    generator.Emit(OpCodes.Ret);

    return (Func<string, string, string>)dynamicMethod.CreateDelegate(typeof(Func<string, string, string>));
}

Dans cet exemple, nous utilisons Ldarg_0 et Ldarg_1 pour charger les deux arguments de la méthode sur la pile. Ensuite, l’opcode Call est utilisé pour invoquer la méthode Concat de la classe string, qui prend deux chaînes et les concatène. La méthode Concat utilise les deux valeurs en haut de la pile comme arguments. Le résultat de l’opération Concat est ensuite placé sur la pile.

Finalement, l’opcode Ret est utilisé pour renvoyer le résultat de la méthode Concat, qui est la chaîne concaténée.

Il est important de souligner que Call n’est pas la seule façon d’appeler une méthode en IL. Il y a aussi Callvirt, qui est utilisé pour appeler des méthodes virtuelles et des méthodes d’instance, et Newobj, qui est utilisé pour appeler des constructeurs.

En comprenant comment l’opcode Call fonctionne et comment il interagit avec la pile, vous pouvez commencer à comprendre comment les différentes instructions IL s’assemblent pour former des méthodes complètes.

Newobj

L’opcode Newobj est une autre instruction IL importante qui est utilisée pour créer de nouvelles instances d’objets. Comme son nom l’indique, Newobj crée un nouvel objet, mais il fait plus que cela. En effet, Newobj crée une nouvelle instance d’un objet et appelle également son constructeur.

L’utilisation de Newobj est un peu plus complexe que certains autres opcodes, car elle nécessite que vous spécifiiez le constructeur que vous souhaitez appeler. Cette spécification doit correspondre exactement au nombre et aux types d’arguments du constructeur que vous voulez utiliser.

Pour illustrer comment Newobj fonctionne, supposons que nous ayons une classe simple comme celle-ci en C# :

public class Greeting
{
    private string _greeting;

    public Greeting(string greeting)
    {
        _greeting = greeting;
    }

    public string GetGreeting()
    {
        return _greeting;
    }
}

En utilisant ILGenerator, nous pourrions créer une nouvelle instance de Greeting comme ceci :

public static Func<string, Greeting> CreateGreeting()
{
    var dynamicMethod = new DynamicMethod(
        "CreateGreeting",
        typeof(Greeting),
        new Type[] { typeof(string) },
        typeof(Program).Module);

    var generator = dynamicMethod.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Newobj, typeof(Greeting).GetConstructor(new Type[] { typeof(string) }));
    generator.Emit(OpCodes.Ret);

    return (Func<string, Greeting>)dynamicMethod.CreateDelegate(typeof(Func<string, Greeting>));
}

Dans cet exemple, l’instruction Ldarg_0 est utilisée pour charger le premier argument de la méthode sur la pile. Ensuite, nous utilisons Newobj pour créer une nouvelle instance de Greeting. Nous spécifions le constructeur à appeler en utilisant typeof(Greeting).GetConstructor(new Type[] { typeof(string) }). Cela signifie que nous voulons appeler le constructeur de Greeting qui prend une seule chaîne comme argument. Le constructeur utilise la valeur en haut de la pile comme argument, c’est-à-dire la chaîne que nous avons précédemment chargée avec Ldarg_0.

Le résultat de Newobj est une nouvelle instance de Greeting qui est placée sur la pile. Enfin, nous utilisons Ret pour retourner cet objet.

Donc, si nous appelons notre délégué avec l’argument « Bonjour », la méthode retournera une nouvelle instance de Greeting dont la propriété _greeting est « Bonjour ». C’est un exemple de la façon dont Newobj peut être utilisé pour créer des objets dynamiquement en émettant du code IL.

Stloc, Ldloc

Tout d’abord, Stloc signifie « Store Local ». Il est utilisé pour stocker une valeur de la pile d’évaluation dans une variable locale. De l’autre côté, nous avons Ldloc, qui signifie « Load Local ». Comme vous pouvez l’imaginer, Ldloc fait exactement le contraire de Stloc – il charge une valeur depuis une variable locale sur la pile d’évaluation.

Supposons que nous voulions modifier notre fonction de salutation précédente pour inclure une salutation personnalisée en fonction de l’heure de la journée. Nous pourrions avoir besoin d’une variable locale pour stocker l’heure actuelle. Voici comment nous pourrions le faire :

public static Func<string, string> CreateGreeting()
{
    var dynamicMethod = new DynamicMethod(
        "CreateGreeting",
        typeof(string),
        new Type[] { typeof(string) },
        typeof(Program).Module);

    var generator = dynamicMethod.GetILGenerator();
    generator.DeclareLocal(typeof(int)); // Declare a local variable to store the hour
    generator.Emit(OpCodes.Call, typeof(DateTime).GetProperty("Now").GetGetMethod());
    generator.Emit(OpCodes.Call, typeof(DateTime).GetProperty("Hour").GetGetMethod());
    generator.Emit(OpCodes.Stloc_0); // Store the hour in the local variable
    generator.Emit(OpCodes.Ldstr, "Bonjour, ");
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Call, typeof(string).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }));
    generator.Emit(OpCodes.Stloc_1); // Store the greeting in a local variable
    generator.Emit(OpCodes.Ldloc_0); // Load the hour
    generator.Emit(OpCodes.Ldc_I4, 12);
    generator.Emit(OpCodes.Blt, labelMorning);
    generator.Emit(OpCodes.Ldloc_1); // Load the greeting
    generator.Emit(OpCodes.Ret);

    var labelMorning = generator.DefineLabel();
    generator.MarkLabel(labelMorning);
    generator.Emit(OpCodes.Ldstr, "Good Morning, ");
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Call, typeof(string).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }));
    generator.Emit(OpCodes.Ret);

    return (Func<string, string>)dynamicMethod.CreateDelegate(typeof(Func<string, string>));
}

Dans cet exemple, nous utilisons Stloc_0 pour stocker l’heure actuelle dans une variable locale. Nous utilisons ensuite Ldloc_0 pour charger cette valeur sur la pile et la comparer à 12. Si l’heure est inférieure à 12, nous sautons à une étiquette où nous créons une salutation du matin. Sinon, nous utilisons la salutation générique.

Comme pour Ldarg, Stloc et Ldloc ont aussi des variantes pour les indices inférieurs (stloc.0, stloc.1, ldloc.0, ldloc.1, etc.) et pour les indices supérieurs (stloc.s, ldloc.s, stloc, ldloc).

Ces deux opcodes montrent comment les variables locales sont gérées en IL et comment elles interagissent avec la pile d’évaluation. En maîtrisant leur utilisation, vous pouvez gérer efficacement le stockage des données pendant l’exécution de votre code IL.

Brtrue et Brfalse

Il est maintenant temps de jeter un œil à deux opcodes qui nous permettent d’introduire des décisions logiques dans notre code IL : Brtrue et Brfalse. Ce sont deux opcodes de branchement conditionnel, utilisés pour déterminer le flux de contrôle de votre programme.

Brtrue est l’abréviation de « Branch if true ». Il transfère le contrôle à une instruction cible si la valeur en haut de la pile est non-nulle (true). De l’autre côté, Brfalse signifie « Branch if false ». Il transfère le contrôle si la valeur en haut de la pile est nulle (false).

Supposons que nous voulions ajouter une logique à notre fonction de salutation pour vérifier si le nom donné est nul. Nous pourrions utiliser Brtrue pour sauter à un code qui retourne une erreur si tel est le cas :

public static Func<string, string> CreateGreeting()
{
    var dynamicMethod = new DynamicMethod(
        "CreateGreeting",
        typeof(string),
        new Type[] { typeof(string) },
        typeof(Program).Module);

    var generator = dynamicMethod.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0); // Load the argument
    generator.Emit(OpCodes.Brtrue, labelNameIsNotNull); // Jump to labelNameIsNotNull if the argument is not null
    // If the argument is null, we emit the code for an error message and return
    generator.Emit(OpCodes.Ldstr, "Error: No name provided.");
    generator.Emit(OpCodes.Ret);

    var labelNameIsNotNull = generator.DefineLabel();
    generator.MarkLabel(labelNameIsNotNull);
    generator.Emit(OpCodes.Ldstr, "Bonjour, ");
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Call, typeof(string).GetMethod("Concat", new Type[] { typeof(string), typeof(string) }));
    generator.Emit(OpCodes.Ret);

    return (Func<string, string>)dynamicMethod.CreateDelegate(typeof(Func<string, string>));
}

Dans cet exemple, nous utilisons Ldarg_0 pour charger l’argument sur la pile, puis Brtrue pour vérifier si cette valeur est nulle. Si elle est nulle, nous émettons du code pour retourner un message d’erreur. Sinon, nous sautons à une étiquette où nous créons la salutation.

Les opcodes Brtrue et Brfalse sont des outils puissants pour introduire des décisions logiques dans votre code IL. En les maîtrisant, vous pouvez créer du code IL qui réagit de manière dynamique à différentes conditions d’exécution.

Ret

Ret est l’opcode IL pour return et chaque méthode en IL doit se terminer par une instruction Ret. Si vous oubliez d’émettre Ret à la fin de votre méthode, le CLR (Common Language Runtime) lèvera une InvalidProgramException au moment de l’exécution.

Voyons comment cela pourrait se produire avec un exemple. Supposons que vous vouliez émettre une méthode qui ajoute deux nombres :

public static Func<int, int, int> CreateAdder()
{
    var dynamicMethod = new DynamicMethod(
        "Adder",
        typeof(int),
        new Type[] { typeof(int), typeof(int) },
        typeof(Program).Module);

    var generator = dynamicMethod.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldarg_1);
    generator.Emit(OpCodes.Add);

    return (Func<int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<int, int, int>));
}

Cette méthode charge les deux arguments sur la pile, les ajoute ensemble avec l’opcode Add, mais ne se termine pas par Ret. Lorsque vous essayez d’exécuter le délégué créé par cette méthode, vous obtiendrez une InvalidProgramException.

La correction est simple : ajoutez generator.Emit(OpCodes.Ret); à la fin de la méthode :

public static Func<int, int, int> CreateAdder()
{
    var dynamicMethod = new DynamicMethod(
        "Adder",
        typeof(int),
        new Type[] { typeof(int), typeof(int) },
        typeof(Program).Module);

    var generator = dynamicMethod.GetILGenerator();
    generator.Emit(OpCodes.Ldarg_0);
    generator.Emit(OpCodes.Ldarg_1);
    generator.Emit(OpCodes.Add);
    generator.Emit(OpCodes.Ret);

    return (Func<int, int, int>)dynamicMethod.CreateDelegate(typeof(Func<int, int, int>));
}

En ajoutant l’instruction Ret, la méthode se termine correctement et le CLR peut l’exécuter sans problème.