A la découverte de IL
- Introduction & historique
- Démystifier le code IL
- Premiers pas avec l’émission de code IL
- Naviguer dans la complexité du code IL
- Applications concrètes de l’émission de code IL
- Création de types dynamiques à l’exécution
- Optimisation des performances en éliminant les surcoûts des appels de méthode
- Exploration plus profonde de la création de types dynamiques
- Exploration plus profonde de l’optimisation des performances
- Exploration de cas d’utilisation réels
- Conclusion
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.
Naviguer dans la complexité du code IL
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.