Генерация IL кода с помощью Sigil

Наверняка вам уже приходилось генерировать код. Иногда задача позволяет использовать что-то наподобии Roslyn для скриптового движка, CSharpCodeProvider или что-то похожее, но иногда это лишний оверхед, работать с компилятором, ведь мы можем генерировать CIL на-лету.



Генерировать CIL может потребоваться в разных случаях, например при реализации сериализатора. Сериализатор Jil к примеру, как раз использует данный подход.


Но работать непосредственно с IL генерацией через System.Reflection.Emit довольно трудно, т.к. по-сути валидации созданного тобой IL-кода никто не производит, любая опечатка таким образом может стать головной болью.


Как раз для этого разработчик Jil (работает в StackOverflow) создал свою обертку-валидатор Sigil над ILGenerator, позволяя валидировать CIL, который мы сгенерировали.


Сам разработчик описывает свою библиотеку так:


Sigil is a roughly 1-to-1 replacement for ILGenerator. Rather than calling ILGenerator.Emit(OpCode, ...), you call Emit.OpCode(...).
Unlike ILGenerator, Sigil will fail as soon as an error is detected in the emitted IL.

На самом деле Sigil так же может выбирать некоторые опкоды исходя из переданных параметров автоматически, так что работать с ним будет намного приятнее чем с ILGenerator.


Кстати саму валидацию мы можем отключить например в RELEASE билде, оставив только проверки в DEBUG сборке.


Get started


Давайте попробуем Sigil для генерации IL кода в деле. Мы будем создавать тип по описанию его полей, а так же реализуем какой-нибудь простенький интерфейс для созданного нами типа. Описание полей мы будем брать из простенького массива объектов, такого типа:


public class FieldDesc
{
    public string Name;
    public Type Type;
    public bool IsKey;
}

Ну что ж, давайте добавим пакет Sigil к нашему проекту:


PM> Install-Package Sigil

И напишем небольшой пример использования:


using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using Sigil;

namespace ConsoleApp1
{

    public interface IHasKey
    {
        string Key { get; }
    }

    public class FieldDesc
    {
        public string Name;
        public Type Type;
        public bool IsKey;
    }
    class Program
    {

        public static Type CreateTypeFromFieldsDesc(string typeNamespace, string typeName, IEnumerable<FieldDesc> fields)
        {
            var assemblyName = new AssemblyName() { Name = typeName };

            // Используем [выгружаемые сборки](https://msdn.microsoft.com/en-us/library/dd554932(v=vs.100).aspx)
            var assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.RunAndCollect);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule(assemblyName.Name);
            var typeBuilder = moduleBuilder.DefineType($"{typeNamespace}.{typeName}", TypeAttributes.Public | TypeAttributes.Class );

            //! В релизе выключаем проверки Sigil генерации IL-кода
#if DEBUG
            bool allowUnverifiableCode = false;
            bool doVerify = true;
#else
            bool allowUnverifiableCode = true;
            bool doVerify = false;
#endif

            // Понадобится нам для реализации интерфейса IHasKey
            FieldBuilder keyFieldBuilder = null;
            FieldDesc keyField = null;
            // Создаем поля
            foreach (var field in fields)
            {
                if (field.IsKey)
                {
                    keyFieldBuilder = typeBuilder.DefineField(field.Name, field.Type, FieldAttributes.Public);
                    keyField = field;
                    continue;
                }
                typeBuilder.DefineField(field.Name, field.Type, FieldAttributes.Public);
            }
            if (keyField == null || keyFieldBuilder == null)
                throw new ArgumentException("One of fields must have IsKey = true!", nameof(fields));

            typeBuilder.AddInterfaceImplementation(typeof(IHasKey));
            // Создаем реализацию IHasKey
            var keyGetter = Emit<Func<string>>.BuildInstanceMethod(typeBuilder,"get_Key", MethodAttributes.Public | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.Virtual, allowUnverifiableCode, doVerify);
            keyGetter.LoadArgument(0);
            if (keyField.Type == typeof(string))
            {
                keyGetter.LoadField(keyFieldBuilder);
            }
            // Если ключевой элемент имеет тип не string приведем его к string, вызвав метод ToString()
            else
            {
                if (keyField.Type.IsValueType)
                    keyGetter.LoadFieldAddress(keyFieldBuilder);
                else
                    keyGetter.LoadField(keyFieldBuilder);
                // Будем вызывать ToString() без параметров (100% будет хотябы отнаследованный от object)
                keyGetter.CallVirtual(keyField.Type.GetMethods().FirstOrDefault(m => m.Name == "ToString" && !m.GetParameters().Any()));
            }
            keyGetter.Return();
            keyGetter.CreateMethod();

            return typeBuilder.CreateType();
        }

    static void Main(string[] args)
        {

            var fields = new List<FieldDesc>()
            {
                new FieldDesc { Name = "Test1", Type = typeof(string),   IsKey = false},
                new FieldDesc { Name = "Test2", Type = typeof(int),      IsKey = false},
                new FieldDesc { Name = "Test3", Type = typeof(DateTime), IsKey = true }
            };

            Type generatedType = CreateTypeFromFieldsDesc("ConsoleApp1","GeneratedType", fields);
            IHasKey obj = (IHasKey) Activator.CreateInstance(generatedType);

            dynamic just4test = obj;
            just4test.Test3 = DateTime.Now;

            Console.WriteLine($"Magic happends, object key is: {obj.Key} Object type is {obj.GetType()}");

            Console.ReadLine();

        }
    }
}

Вообщем генерация IL кода — это штука довольно занятная и порой дает хороший прирост производительности.


Однако написание IL-а дело довольно кропотливое и трудоемкое, Sigil может существенно скрасить несговорчивость ILGenerator-а и свалидировать генерируемый код по мере его создания (fail-fast), что значительно ускорит вашу работу.


Однозначно вам стоит попробовать Sigil!

Tags:
C#, IL, CIL, Sigil, ilgenerator

Данная статья не подлежит комментированию, поскольку её автор ещё не является полноправным участником сообщества. Вы сможете связаться с автором только после того, как он получит приглашение от кого-либо из участников сообщества. До этого момента его username будет скрыт псевдонимом.