Форум программистов, компьютерный форум, киберфорум
C# .NET
Войти
Регистрация
Восстановить пароль
Блоги Сообщество Поиск Заказать работу  
 
Рейтинг 4.67/15: Рейтинг темы: голосов - 15, средняя оценка - 4.67
 Аватар для Anklav
447 / 305 / 47
Регистрация: 23.01.2013
Сообщений: 661

Emit, Reflection, кодогенерация

21.01.2015, 18:55. Показов 3107. Ответов 3
Метки нет (Все метки)

Студворк — интернет-сервис помощи студентам
Допустим у вас есть некий клиент, который может отправлять сообщения, при этом для отправки ему нужны айдишник сообщения и контент.

Выглядит это примерно так:
C#
1
2
3
4
5
6
7
8
  public class Client
  {
    public void SendMessage(int messageType, object messageContent)
    {
      // .. тут мы должны отправить сообщения, но мы просто сделаем запись в консоль.
      Console.WriteLine("messageSended [Type: {0}, ContentType: {1}]", messageType, messageContent);
    }
  }
И мы хотим сделать для этого клиента какую то обертку, что бы вызывать сервер, как методы объекта.
Предположим вот так:

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
  public class FirstMethodContent
  {
    public int SomeFirstParam { get; set; }
    public int SomeSecondParam { get; set; }
  }
 
  public class SecondMethodContent
  {
    public int SomeFirstParam { get; set; }
  }
 
  public class ClientAPI
  {
    Client client;
 
    void FirstMethod(int someFirstParam, int someSecondParam)
    {
      var content = new FirstMethodContent() { SomeFirstParam = someFirstParam, SomeSecondParam = someSecondParam };
      client.SendMessage(1, content);
    }
 
    void SecondMethod(int someFirstParam)
    {
      var content = new SecondMethodContent() { SomeFirstParam = someFirstParam };
      client.SendMessage(2, content);
    }
  }
Как мы видим реализация этих методов тривиальная, и я думаю их можно генерировать на лету, по интерфейсу. Чем и займемся. Причем скорость вызова этих методов не будет уступать явной реализации.

Для начала определим атрибут, в котором мы укажем генератору типа, какой у нас тип у контента, а также айдишник.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
  // с помощью атрибута ограничиваем его использование
  // только для методов, и только 1 раз
  [AttributeUsage(AttributeTargets.Method, AllowMultiple = false)] 
  public class ClientMethodAttribute : Attribute   // Объявляем атрибут
  {
    public int MessageType { get; private set; } // Айдишник сообщения
    public Type ContentType { get; private set; } // Тип класса в который мы будем оборачивать параметры
 
    public ClientMethodAttribute(int messageType, Type contentType)
    {
      MessageType = messageType;
      ContentType = contentType;
    }
  }
Объявим наш интерфейс.
Как видим объявление методов занимает намного меньше усилий.

C#
1
2
3
4
5
6
7
8
  public interface IClientAPI
  {
    [ClientMethod(1, typeof(FirstMethodContent))]
    void FirstMethod(int someFirstParam, int someSecondParam);
 
    [ClientMethod(2, typeof(SecondMethodContent))]
    void SecondMethod(int someFirstParam);
  }
И так, в этом интерфейсе есть все данные, что бы можно было на лету сгенерировать для него реализацию.
Приступим.

Создадим генератор TypeGenerator, там будет всего 1 метод: public IInterface MakeAPI<TInterface>().

Общий план действий в методе такой:
  • Создать генератор типа.
  • Добавить типу интерфейс.
  • Достать из интерфейса все методы.
  • С помощью атрибута составить таблицу соответствия методов и полей контента.
  • Также с помощью атрибута мы достанем айдишник.
  • Пробежаться по методам, и реализовать их.

План действий в реализации методы
  • В первую очередь методы должен создать инстанс контента.
  • Далее присвоить полям контента значения параметров (сопоставления будет по именам)
  • Далее вызвать метод клиента.

Ничего сложного, поехали:
Реализация метода будет без кеширования уже готовых типов, динамической сборки и модуля, что бы не захламлять код. Его элементарно может добавить каждый.


Первый пункт и второй пункты:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
      // создаем динамическую сборку, в текущем домене (AppDomain) с именем "genered_assembly" и с разрешением только на запуск
      var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("genered_assembly"), AssemblyBuilderAccess.Run);
 
      // создаем динамичкский модуль.
      var moduleBuilder = assemblyBuilder.DefineDynamicModule("genered_module");
 
      // объявляем тип с именем Интерейса и постфиксом genered
      var typeBuilder = moduleBuilder.DefineType(typeof(TInterface).Name + "genered");
 
      // унаследуем тип от клиенского апи (что бы не делать лишних действий создавая поле и конструктор)
      typeBuilder.SetParent(typeof(ClientAPI));
 
      // говорим, что тип будет реализовывать наш интерфейс
      typeBuilder.AddInterfaceImplementation(typeof(TInterface));
 
      // достаем из интерфейса все публичные методы
      var methods = typeof(TInterface).GetMethods();
Базовый тип выглядит так, в него если необходимо можно выносить какие либо методы. Которые можно написать 1 раз.
C#
1
2
3
4
  public class ClientAPI
  {
    protected Client client = new Client();
  }
Дальше все действия будут происходить в цикле, пока я не скажу что действия в цикле закончились.
C#
1
2
3
4
      foreach (var method in methods)
      {
 
      }
Составим таблицу параметров для метода:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
        // достаем наш атрибут
        var clientMethodAttribute = (ClientMethodAttribute) method.GetCustomAttribute(typeof(ClientMethodAttribute));
        if (clientMethodAttribute == null)
          throw new InvalidOperationException("Забыл добавить атрибут! Добавь пожалуйста!");
 
        // достаем тип контекта, объявленого в аттрибуте
        var contentType = clientMethodAttribute.ContentType;
 
        // теперь нам нужно сопоставить свойства с аргементами интерфейса, просто расставим свойства в таком же порядке
        // для этого нужно в начале достать все параметры метода
        var methodParameters = method.GetParameters();
 
        // у свойств будем доставать сразу set методы, по этому объявляем массив MethodInfo
        var contentSetMethods = new MethodInfo[methodParameters.Length];
 
        // достангем все свойства конекта
        var contentProerties = contentType.GetProperties();
 
        for (int i = 0; i < methodParameters.Length; i++)
        {
          // достанем метод установки значения у свойства с таким же именем как и у параметра 
          // (игнорируя загланвые или не загланвые у нас буквы)
          contentSetMethods[i] = contentProerties
            .Single(p => string.Compare(p.Name, methodParameters[i].Name, StringComparison.OrdinalIgnoreCase) == 0)
            .SetMethod;
        }
Дальше начнем генерацию нашего типа. Начнем с объявления метода:
C#
1
2
3
4
5
6
7
8
9
10
11
12
        // объявим метод, с именем таким же как у интерфейсного
        // он будет публичный, виртуальный (обязательно если хотим переопределить интерфейсный метод)
        // также возвращаемое значение и параметры будут такие же как у интерфейса
        var methodBuilder = typeBuilder.DefineMethod(
          method.Name,
          MethodAttributes.Public | MethodAttributes.Virtual,
          method.ReturnType,
          methodParameters.Select(p => p.ParameterType).ToArray()
        );
 
        // Объявим, что этот метод переопределяет интерфейсный метод.
        typeBuilder.DefineMethodOverride(methodBuilder, method);
В CLR стековая виртуальная машина, это значит что бы вызвать какую либо инструкцию, или метод, нужно в начале затолкать в стек параметры в правильном порядке. После выполнения функция (если у нее есть результат) положит его на вершину стека. Также нужно знать, что экземплярных метода всегда есть 1 параметр, это ссылка на объект который вызываем метод (то есть this).

Типов инструкций не так много, по сути их можно разделить на несколько типов. Подробнее о них можно почитать в самой студии, а подсказках.
Математические: add, mul, div, and, or и т.д.
Логические (ветвления) br, brfalse, brtrue и т.д.
и т.д.

Самое больше количество занимают инструкции сохранения переменных из стека, и загрузка их в стек.
Загрузка в стек может быть из нескольких источников:
  • Из параметров метода. (ldarg_0, ldarg и т.д.)
  • Из полей объекта. (ldfld - загрузить поле, ldflda - загрузить адрес объекта, нужно для ref, out)
    (stfld - установить поле, st - set; fld - field)
  • Из статических полей. (ldsfld ld - load; s - static; fld - field
  • Из локальных переменных. (ldloc_0, ldloc и т.д.) (stloc_0, stloc)
  • Можно грузить константы. (ldc_I4 и т.д.)

В общем повторюсь, в студии достаточно хорошие описания, что бы понять что инструкция делает.
Также можно посмотреть еще и сюда: http://en.wikipedia.org/wiki/L... structions (там есть описание параметров инструкций)

Перейдем к генерации тела метода.
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
        // Дальше достаем геренатор IL кода, и приступаем к генерации тела метода.
        var il = methodBuilder.GetILGenerator();
        // Объявим локальную переменную, с типом контента. Она будет под 0 индексом.
        // Обращение к переменным идет по их индексам.
        il.DeclareLocal(contentType);
 
        // созданим экземпляр объекта
        il.Emit(OpCodes.Newobj, contentType.GetConstructor(new Type[0]));
        // сохраним его в 0 локальную переменную
        il.Emit(OpCodes.Stloc_0);
 
        // теперь необходимо записать в поля объекта аргументы функции
        // начинаем с еденицы, т.к. первый параметр это this
        for(int i = 1; i <= contentSetMethods.Length; i++)
        {
          // загрузим объект, которому будем устанавливать свойства, он у нас в 0 переменной.
          il.Emit(OpCodes.Ldloc_0);
 
          // для оптимизации будем использовать спецефические инструкции
          // для извлечегтя нескольих первых аргументов
          switch (i)
          {
            case 1:
              il.Emit(OpCodes.Ldarg_1); // загружаем первый аргумент
              break;
            case 2:
              il.Emit(OpCodes.Ldarg_2); // загружаем второй аргумент
              break;
            case 3:
              il.Emit(OpCodes.Ldarg_3); // загружаем третий аргумент
              break;
            default:
              // загружаем производьный аргумент, используем короткую форму.
              // Это значит вторым параметром должен быть байт. (в обычной ushort)
              // О короткой форме говорит S в конце,
              il.Emit(OpCodes.Ldarg_S, (byte)i);
              break;
          }
 
          // теперь осталось вызвать метод сет, у объекта, таблицу методов мы уже создали.
          // вызывать мы будем используя Callvirt (виртуальный вызво). Он используется для вызова методов у объектов.
          // для статических функций есть Call.
          il.Emit(OpCodes.Callvirt, contentSetMethods[i - 1]);
        }
Отлично объект мы создали, и инициализировали, осталось вызвать метод клиента.

C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
        // для вызова метода клиента, нам нужно в начале загрузить в стек объект клиента.
        // используем для этого инструкцию Ldfld (ld - load, fld - field)
        // для вызова этой инструкции нужно в начале загрузить в стек объект, у которого мы будем доставать поле (что очень логично)
        // объект этот это this, тоесть мы можем загрузить его из 0 аргумента.
        il.Emit(OpCodes.Ldarg_0);
        // вторым параметром для инструкции нужно передать FieldInfo
        il.Emit(OpCodes.Ldfld, typeof(ClientAPI).GetField("client", BindingFlags.Instance | BindingFlags.NonPublic));
        // теперь загрузим аргументы для метода клиента
        // загрузим константу
        il.Emit(OpCodes.Ldc_I4, clientMethodAttribute.MessageType);
        // загрузим сохраненную локальную переменную (наш контекнт)
        il.Emit(OpCodes.Ldloc_0);
        // и наконец вызовем функцию
        il.Emit(OpCodes.Callvirt, typeof(Client).GetMethod("SendMessage"));
 
        // самое важное, не забыть завершить метод.
        il.Emit(OpCodes.Ret);
Отлично, с кодогенерацией закончили. Как и с циклом.

Ну и завершающие действия.

C#
1
2
3
4
5
      // создаем тип
      var type = typeBuilder.CreateType();
 
      // создаем инстанс нашего класса
      return (TInterface)Activator.CreateInstance(type);
Проверим что получилось, для этого определим в наших контентах методы ToString и реализуем main:
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
  public class FirstMethodContent
  {
    public int SomeFirstParam { get; set; }
    public int SomeSecondParam { get; set; }
 
    public override string ToString()
    {
      return string.Format("{0}, {1}, Type: {2}", SomeFirstParam, SomeSecondParam, GetType().Name);
    }
  }
 
  public class SecondMethodContent
  {
    public int SomeFirstParam { get; set; }
 
    public override string ToString()
    {
      return string.Format("{0}, Type: {1}", SomeFirstParam, GetType().Name);
    }
  }
 
    static void Main()
    {
      var autoAPI = TypeGenerator.MakeAPI<IClientAPI>();
      autoAPI.FirstMethod(2, 2);
      autoAPI.SecondMethod(5);
 
      Console.ReadLine();
    }
Вывод в консоли:
Миниатюры
Emit, Reflection, кодогенерация  
1
cpp_developer
Эксперт
20123 / 5690 / 1417
Регистрация: 09.04.2010
Сообщений: 22,546
Блог
21.01.2015, 18:55
Ответы с готовыми решениями:

Как создать .exe? (System.Reflection.Emit)
как создать .exe через System.Reflection.Emit?

Как задать атрибут для генерируемого класса, используя Reflection.Emit?
Я пишу код, который должен сгенерировать следующий класс в рантайме, используя System.Reflection.Emit: public class GeneratedClass {...

Кодогенерация на лету C#
Переодически вижу на форуме вопросы про динамическое выполнение С# кода. Например С# Можно ли выполнять код динамически? Поэтому...

3
Master of Orion
Эксперт .NET
 Аватар для Psilon
6102 / 4958 / 905
Регистрация: 10.07.2011
Сообщений: 14,522
Записей в блоге: 5
21.01.2015, 23:11
Anklav, весьма неплохо, однако смущает возможность все грохнуть, если параметр не так назвать/не в том порядке написать А так хорошо, плюс в карму, офк

Добавлено через 59 секунд
ну и неплохо было бы иметь словарь для кэширования уже сгенерированных методов, чтобы не генерировать их при каждом вызове, а только один раз для каждого типа.
0
 Аватар для Anklav
447 / 305 / 47
Регистрация: 23.01.2013
Сообщений: 661
21.01.2015, 23:26  [ТС]
Psilon, это абстрактный пример, естественно в нормальном варианте нужно кэширование. (о чем я и написал)

Также по идее второй вызов этого метода навернется, т.к. сборка с таким именем уже есть, в домене, кстати.

Также было бы неплохо выбирать вручную реализовывать, или автоматически (это делается переводом интерфейса в абстрактный класс).

Со связкой аргумента и свойства решается простым исключением, если не находим нужное свойство.

Но в качестве примера это слишком сложно, как мне кажется.
0
 Аватар для Konctantin
970 / 773 / 171
Регистрация: 12.04.2009
Сообщений: 1,700
22.01.2015, 00:15
C#
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
          // для оптимизации будем использовать спецефические инструкции
          // для извлечегтя нескольих первых аргументов
          switch (i)
          {
            case 1:
              il.Emit(OpCodes.Ldarg_1); // загружаем первый аргумент
              break;
            case 2:
              il.Emit(OpCodes.Ldarg_2); // загружаем второй аргумент
              break;
            case 3:
              il.Emit(OpCodes.Ldarg_3); // загружаем третий аргумент
              break;
            default:
              // загружаем производьный аргумент, используем короткую форму.
              // Это значит вторым параметром должен быть байт. (в обычной ushort)
              // О короткой форме говорит S в конце,
              il.Emit(OpCodes.Ldarg_S, (byte)i);
              break;
          }
Хотелось бы сказать, что инструкция с явно указанным индексом il.Emit(OpCodes.Ldarg_1); выполняется намного быстрее нежели с вычисляемым il.Emit(OpCodes.Ldarg_S, (byte)i);
0
Надоела реклама? Зарегистрируйтесь и она исчезнет полностью.
raxper
Эксперт
30234 / 6612 / 1498
Регистрация: 28.12.2010
Сообщений: 21,154
Блог
22.01.2015, 00:15
Помогаю со студенческими работами здесь

Рефлексия, кодогенерация и обёртка
Доброго времени суток! Сначала прикладная задача: имеется статический класс, для которого необходимо делать не статическую обёртку....

Аналог CodeDom и System.Reflection.Emit
собсна сабж. в шарпе есть пространства имен - System.CodeDom и System.Reflection.Emit. интересует, есть ли их аналог в java. возможно...

Как создать .exe? (System.Reflection.Emit)
как создать .exe через System.Reflection.Emit?

C++ vs Common Lisp: кодогенерация, метапрограммирование
Дано описание произвольного математических выражений на XML вида: &lt;calc&gt; &lt;mul&gt; &lt;var&gt;k&lt;/var&gt; ...

Кодогенерация для Eclipse Juno
Срочно нужен кодогенератор на основе UML модели с реверс-инженерингом. Что можете посоветовать? Добавлено через 1 час 8 минут Ой,...


Искать еще темы с ответами

Или воспользуйтесь поиском по форуму:
4
Ответ Создать тему
Новые блоги и статьи
Первый деплой
lagorue 16.01.2026
Не спеша развернул своё 1ое приложение в kubernetes. А дальше мне интересно создать 1фронтэнд приложения и 2 бэкэнд приложения развернуть 2 деплоя в кубере получится 2 сервиса и что-бы они. . .
Расчёт переходных процессов в цепи постоянного тока
igorrr37 16.01.2026
/ * Дана цепь постоянного тока с R, L, C, k(ключ), U, E, J. Программа составляет систему уравнений по 1 и 2 законам Кирхгофа, решает её и находит токи на L и напряжения на C в установ. режимах до и. . .
Восстановить юзерскрипты Greasemonkey из бэкапа браузера
damix 15.01.2026
Если восстановить из бэкапа профиль Firefox после переустановки винды, то список юзерскриптов в Greasemonkey будет пустым. Но восстановить их можно так. Для этого понадобится консольная утилита. . .
Изучаю kubernetes
lagorue 13.01.2026
А пригодятся-ли мне знания kubernetes в России?
Сукцессия микоризы: основная теория в виде двух уравнений.
anaschu 11.01.2026
https:/ / rutube. ru/ video/ 7a537f578d808e67a3c6fd818a44a5c4/
WordPad для Windows 11
Jel 10.01.2026
WordPad для Windows 11 — это приложение, которое восстанавливает классический текстовый редактор WordPad в операционной системе Windows 11. После того как Microsoft исключила WordPad из. . .
Classic Notepad for Windows 11
Jel 10.01.2026
Old Classic Notepad for Windows 11 Приложение для Windows 11, позволяющее пользователям вернуть классическую версию текстового редактора «Блокнот» из Windows 10. Программа предоставляет более. . .
Почему дизайн решает?
Neotwalker 09.01.2026
В современном мире, где конкуренция за внимание потребителя достигла пика, дизайн становится мощным инструментом для успеха бренда. Это не просто красивый внешний вид продукта или сайта — это. . .
КиберФорум - форум программистов, компьютерный форум, программирование
Powered by vBulletin
Copyright ©2000 - 2026, CyberForum.ru