Illuminator 0.4.0

.NET 5.0 .NET Standard 2.0
There is a newer version of this package available.
See the version list below for details.
dotnet add package Illuminator --version 0.4.0
NuGet\Install-Package Illuminator -Version 0.4.0
This command is intended to be used within the Package Manager Console in Visual Studio, as it uses the NuGet module's version of Install-Package.
<PackageReference Include="Illuminator" Version="0.4.0" />
For projects that support PackageReference, copy this XML node into the project file to reference the package.
paket add Illuminator --version 0.4.0
#r "nuget: Illuminator, 0.4.0"
#r directive can be used in F# Interactive and Polyglot Notebooks. Copy this into the interactive tool or source code of the script to reference the package.
// 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


Build status NuGet Badge

Illuminator is yet another wrapper around ILGenerator, but with some interesting features:

  1. Fluent, convenient API with functional programming flavor.
  2. Tracing generated code.
  3. Set of useful helpers.
  4. 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.Brfalse_S, label); // if (value == 2)
generator.Emit(OpCodes.Ret); // return 1
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:

  1. It's very verbose and hard to read. All this Emit and ObCodes are just unnecessary noise.
  2. 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.
  3. 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

  .Brfalse_S(label) // if (value == 2)
  .Ret() // return 1
  .Ret(); // return value + 3

var foo = method.CreateDelegate<Func<int, int>>();

Much better this time:

  1. We don't have to memorise all OpCodes and write those endless Emit. Intellisense helps us.
  2. 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) })
        .Brfalse_S( // if (value == 2)
            Ceq(Ldarg_0(), Ldc_I4_2()),
            out var label)
        .Ret(Ldc_I4_1()) // return 1
        .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)

    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:

  1. It uses the original naming of MSIL instructions, so you don't need to guess what Ldc_I4_2 for example does.
    It does exactly generator.Emit(OpCodes.Ldc_I4_2);.
  2. All methods have helpers with exact amount of ILEmitterFunc parameters that they need to execute.
  3. We can use output parameters to not break the fluent flow.
  4. A flow of instructions can be reused many times as it is a first class function now.
Product 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. 
.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. 
Compatible target framework(s)
Additional computed target framework(s)
Learn more about Target Frameworks and .NET Standard.

NuGet packages (1)

Showing the top 1 NuGet packages that depend on Illuminator:

Package Downloads

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.

Version Downloads Last updated
0.4.2 298 4/28/2021
0.4.1 283 4/25/2021
0.4.0 365 4/11/2021
0.3.2 309 4/11/2021
0.3.1 260 4/11/2021
0.3.0 276 4/10/2021
0.2.1 279 4/5/2021
0.1.0 275 4/4/2021