Illuminator 0.4.0
See the version list below for details.
dotnet add package Illuminator --version 0.4.0
NuGet\Install-Package Illuminator -Version 0.4.0
<PackageReference Include="Illuminator" Version="0.4.0" />
paket add Illuminator --version 0.4.0
#r "nuget: Illuminator, 0.4.0"
// Install Illuminator as a Cake Addin #addin nuget:?package=Illuminator&version=0.4.0 // Install Illuminator as a Cake Tool #tool nuget:?package=Illuminator&version=0.4.0
Illuminator
Illuminator is yet another wrapper around ILGenerator
, but with some interesting features:
- Fluent, convenient API with functional programming flavor.
- Tracing generated code.
- Set of useful helpers.
- Scopes to optimize local variables usage.
Fluent API
Let imagine we need to generate the following code:
int Foo(int value) {
if (value == 2) {
return 1;
}
return value + 3;
}
Using vanilla ILGenerator
you may write something like this:
...
var method = new DynamicMethod("Foo", typeof(int), new[] { typeof(int) });
var generator = method.GetILGenerator();
var label = generator.DefineLabel();
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldc_I4_2);
generator.Emit(OpCodes.Ceq);
generator.Emit(OpCodes.Brfalse_S, label); // if (value == 2)
generator.Emit(OpCodes.Ldc_I4_1);
generator.Emit(OpCodes.Ret); // return 1
generator.MarkLabel(label);
generator.Emit(OpCodes.Ldarg_0);
generator.Emit(OpCodes.Ldc_I4_3);
generator.Emit(OpCodes.Add);
generator.Emit(OpCodes.Ret); // return value + 3
var foo = method.CreateDelegate<Func<int, int>>();
...
So much code for such simple function! When you need to write more complex thing, it becomes not possible to maintain and understand it.
There are few problems with this code:
- It's very verbose and hard to read. All this
Emit
andObCodes
are just unnecessary noise. - It's very hard to write such code. You need to remember specification for each instruction and keep in mind the state of the stack. You need to know exactly how much parameters each instruction needs.
- It's error prone. It's very easy to make a mistake and an exception that you get in runtime does not really informative.
The simplest thing that we can do to improve the situation is to introduce "fluent" API:
...
var method = new DynamicMethod("Foo", typeof(int), new[] { typeof(int) });
var generator = method.GetILGenerator();
var label = generator.DefineLabel();
var il = generator.UseIlluminator(); // Creates wrapper
il.Ldarg_0()
.Ldc_I4_2()
.Ceq()
.Brfalse_S(label) // if (value == 2)
.Ldc_I4_1()
.Ret() // return 1
.MarkLabel(label)
.Ldarg_0()
.Ldc_I4_3()
.Add()
.Ret(); // return value + 3
var foo = method.CreateDelegate<Func<int, int>>();
...
Much better this time:
- We don't have to memorise all
OpCodes
and write those endlessEmit
. Intellisense helps us. - It less verbose and much easier to read.
But still this does not solve all problems. It still possible to have an invalid stack or misuse the short versions for branching instructions (Brfalse_S
).
Lets try one more time with some functional helpers:
using static Illuminator.Functions;
...
var foo =
new DynamicMethod("Foo", typeof(int), new[] { typeof(int) })
.GetILGenerator()
.UseIlluminator()
.Brfalse_S( // if (value == 2)
Ceq(Ldarg_0(), Ldc_I4_2()),
out var label)
.Ret(Ldc_I4_1()) // return 1
.MarkLabel(label)
.Ret(Add(Ldarg_0(), Ldc_I4_3())) // return value + 3
.CreateDelegate<Func<int, int>>();
...
You may think: What?! Wait, try to read it again: it checks (Brfalse_S
) the result of the comparison (Ceq
) of the first argument (Ldarg_0
) and the constant two (Ldc_I4_2
); if they are equal, return 1 (Ret(Ldc_I4_1())
), else return the sum of the argument and three (Ret(Add(Ldarg_0(), Ldc_I4_3()))
).
The code now is much shorter and close to the target c# version. It's nicer to write such code, because all methods have exact amount of parameters that they need, and with output parameters we don't have to break "fluent flow" to create labels and locals.
Hot does it work
Lets look at this line:
Add(Ldarg_0(), Ldc_I4_3())
Add
is the static function from the Functions
class:
public static ILEmitterFunc Add(ILEmitterFunc func1, ILEmitterFunc func2) =>
(in ILEmitter il) => il.Add(func1, func2);
We can use it directly thanks to using static directive feature. That why we need to include using static Illuminator.Functions;
at the beginning.
This function uses the extension for ILEmitter
class which calls ILEmitterFunc
functions before it calls the actual Add
methods:
public static ILEmitter Add(this ILEmitter self, in ILEmitterFunc func1, in ILEmitterFunc func2)
{
func1(self);
func2(self);
return self.Add(); // and this finally does the real generator.Emit(OpCodes.Add);
}
ILEmitterFunc
functions are responsible for preparing values in the stack. And as you may guess, it can be much complex constructions to prepare the stack than a simple Ldc_I4
.
As result we get the fluent, convenient API with functional programming flavor:
- It uses the original naming of
MSIL
instructions, so you don't need to guess whatLdc_I4_2
for example does.
It does exactlygenerator.Emit(OpCodes.Ldc_I4_2);
. - All methods have helpers with exact amount of
ILEmitterFunc
parameters that they need to execute. - We can use output parameters to not break the fluent flow.
- A flow of instructions can be reused many times as it is a first class function now.
Product | Versions Compatible and additional computed target framework versions. |
---|---|
.NET | net5.0 is compatible. net5.0-windows was computed. net6.0 was computed. net6.0-android was computed. net6.0-ios was computed. net6.0-maccatalyst was computed. net6.0-macos was computed. net6.0-tvos was computed. net6.0-windows was computed. net7.0 was computed. net7.0-android was computed. net7.0-ios was computed. net7.0-maccatalyst was computed. net7.0-macos was computed. net7.0-tvos was computed. net7.0-windows was computed. net8.0 was computed. net8.0-android was computed. net8.0-browser was computed. net8.0-ios was computed. net8.0-maccatalyst was computed. net8.0-macos was computed. net8.0-tvos was computed. net8.0-windows was computed. |
.NET Core | netcoreapp2.0 was computed. netcoreapp2.1 was computed. netcoreapp2.2 was computed. netcoreapp3.0 was computed. netcoreapp3.1 was computed. |
.NET Standard | netstandard2.0 is compatible. netstandard2.1 was computed. |
.NET Framework | net461 was computed. net462 was computed. net463 was computed. net47 was computed. net471 was computed. net472 was computed. net48 was computed. net481 was computed. |
MonoAndroid | monoandroid was computed. |
MonoMac | monomac was computed. |
MonoTouch | monotouch was computed. |
Tizen | tizen40 was computed. tizen60 was computed. |
Xamarin.iOS | xamarinios was computed. |
Xamarin.Mac | xamarinmac was computed. |
Xamarin.TVOS | xamarintvos was computed. |
Xamarin.WatchOS | xamarinwatchos was computed. |
-
.NETStandard 2.0
- System.Reflection.Emit.ILGeneration (>= 4.7.0)
-
net5.0
- System.Reflection.Emit.ILGeneration (>= 4.7.0)
NuGet packages (1)
Showing the top 1 NuGet packages that depend on Illuminator:
Package | Downloads |
---|---|
ILLightenComparer
ILLightenComparer is a flexible library that can generate very effective and comprehensive IComparer<T> and IEqualityComparer<T> implementations on runtime using advantages of IL code emission. |
GitHub repositories
This package is not used by any popular GitHub repositories.