IL2CPP: обобщенная реализация

Автор: admin от 28-12-2017, 10:55, посмотрело: 33

В предыдущей статье из серии по IL2CPP мы рассмотрели вызовы методов в генерируемом коде C++. Теперь мы поговорим об одной из самых важных особенностей кода IL2CPP – обобщенной реализации методов, позволяющей существенно уменьшить размер исполняемого файла IL2CPP. Стоит отметить, что обобщенная реализация также используется в средах выполнения Mono и .NET. В IL2CPP она изначально не поддерживалась и была добавлена только со временем.



IL2CPP: обобщенная реализация
в первой статье этой серии. Тем не менее, обобщенная реализация происходит, но теперь – автоматически.



Обобщенная реализация ссылочных типов



Для начала рассмотрим самый распространенный случай – ссылочные типы. В управляемом коде такие типы являются производными от System.Object, а в генерируемом коде – от Object_t. Поэтому для их представления в коде C++ можно использовать плейсхолдер Object_t*.



Давайте найдем сгенерированную версию метода DemonstrateGenericSharing. В моем проекте она называется HelloWorld_DemonstrateGenericSharing_m4. Нас интересуют определения четырех методов в классе GenericType. С помощью Ctags мы можем перейти к объявлению метода для GenericType_1__ctor_m8 (конструктора GenericType). Обратите внимание, что это объявление метода является оператором #define, сопоставляющим данный метод с методом GenericType_1__ctor_m10447_gshared.



Теперь давайте найдем объявления методов для типа GenericType. Что интересно, объявление конструктора GenericType_1__ctor_m9 также является оператором #define, связанным с той же функцией – GenericType_1__ctor_m10447_gshared!

Комментарий к коду определения GenericType_1__ctor_m10447_gshared указывает на то, что этот метод соответствует имени управляемого метода HelloWorld/GenericType`1::.ctor(). Это конструктор типа GenericType object, который называется полностью обобщенным – если взять тип GenericType, то для любого ссылочного типа T реализация всех методов будет использовать версию, где T – это object.



Чуть ниже конструктора в генерируемом коде можно увидеть метод UsesGenericParameter:



extern "C" Object_t * GenericType_1_UsesGenericParameter_m10449_gshared (GenericType_1_t2159 * __this, Object_t * ___value, MethodInfo* method)
{
{
Object_t * L_0 = ___value;
return L_0;
}
}


В обоих случаях, где встречается обобщенный параметр T (тип возвращаемого значения и тип отдельного аргумента), в генерируемом коде используется тип Object_t*. А с учетом того, что все ссылочные типы в таком коде могут быть представлены через Object_t*, эту реализацию метода можно вызвать для любого T, являющегося ссылочным типом.



Во второй статье из этой серии я упоминал, что все определения методов в C++ являются свободными функциями. Утилита il2cpp.exe не использует наследование C++ для генерации переопределенных методов C#, однако использует его для типов. Введя в поиск «AnyClass_t», мы можем увидеть, как тип C# AnyClass выглядит в C++:



struct  AnyClass_t1  : public Object_t
{
};


Учитывая, что AnyClass_t1 является производным от Object_t, мы можем просто передать ему указатель в качестве аргумента к функции GenericType_1_UsesGenericParameter_m10449_gshared.



Но что насчет возвращаемого значения? Мы не можем возвратить указатель на базовый класс там, где предполагается указатель на производный, разве не так? Взгляните на объявление метода GenericType::UsesGenericParameter:



#define GenericType_1_UsesGenericParameter_m10452(__this, ___value, method) (( AnyClass_t1 * (*) (GenericType_1_t6 *, AnyClass_t1 *, MethodInfo*))GenericType_1_UsesGenericParameter_m10449_gshared)(__this, ___value, method)




В генерируемом коде возвращаемое значение (тип Object_t*) фактически становится производным типом AnyClass_t1*. Получается, IL2CPP обманывает компилятор C++, чтобы избежать системы типов C++.



Обобщенная реализация с ограничениями



Предположим, нам нужно разрешить вызов некоторых методов для объекта типа Т, но разве использование Object_t* не будет этому препятствовать? Будет, но для начала мы должны сообщить эту идею компилятору C# с помощью обобщенных ограничений.



Взгляните еще раз на скриптовый код, а именно на InterfaceConstrainedGenericType. Этот обобщенный тип использует выражение where, чтобы тип T был производным от интерфейса AnswerFinderInterface, тем самым разрешая вызов метода ComputeAnswer. В предыдущей статье мы говорили о том, что для вызова методов интерфейса требуется поиск в таблице vtable. А поскольку метод FindTheAnswer совершает прямой вызов функции для экземпляра ограниченного типа T (представленного через Object_t*), в коде C++ может использоваться полностью обобщенная реализация.



Перейдя от реализации функции HelloWorld_DemonstrateGenericSharing_m4 к определению функции InterfaceConstrainedGenericType_1__ctor_m11, мы можем увидеть, что и этот метод является оператором #define, связанным с функцией InterfaceConstrainedGenericType_1__ctor_m10456_gshared. Чуть ниже находится реализация функции InterfaceConstrainedGenericType_1_FindTheAnswer_m10458_gshared, которая принимает аргумент Object_t* и также является полностью обобщенной. Вызов функции InterfaceFuncInvoker0::Invoke позволяет совершить вызов управляемого метода ComputeAnswer.



extern "C" int32_t InterfaceConstrainedGenericType_1_FindTheAnswer_m10458_gshared (InterfaceConstrainedGenericType_1_t2160 * __this, Object_t * ___experiment, MethodInfo* method)
{
static bool s_Il2CppMethodIntialized;
if (!s_Il2CppMethodIntialized)
{
AnswerFinderInterface_t11_il2cpp_TypeInfo_var = il2cpp_codegen_class_from_type(&AnswerFinderInterface_t11_0_0_0);
s_Il2CppMethodIntialized = true;
}
{
int32_t L_0 = (int32_t)InterfaceFuncInvoker0<int32_t>::Invoke(0 /* System.Int32 HelloWorld/AnswerFinderInterface::ComputeAnswer() */, AnswerFinderInterface_t11_il2cpp_TypeInfo_var, (Object_t *)(*(&___experiment)));
return L_0;
}
}


Важно помнить, что IL2CPP рассматривает любой управляемый интерфейс как System.Object. Это правило подходит для любого кода, генерируемого утилитой il2cpp.exe.



Ограничения базового класса



Кроме ограничений интерфейса C# допускает наличие ограничений базового класса. Но если IL2CPP не рассматривает базовые классы как System.Object, как в таком случае работает обобщенная реализация?



Так как базовые классы всегда являются ссылочными типами, IL2CPP использует для них полностью обобщенные методы. В любом коде, использующем поле или вызывающем метод для ограниченного типа, происходит приведение типа в C++. Опять-таки компилятор C# обеспечивает корректную реализацию обобщенного ограничения, и мы обманываем компилятор C++ касательно типа.



Обобщенная реализация типов значений



Давайте вернемся к функции HelloWorld_DemonstrateGenericSharing_m4 и взглянем на реализацию GenericType. Тип DateTime – ссылочный, поэтому GenericType не является обобщенным. Перейдем к объявлению конструктора этого типа, GenericType_1__ctor_m10. Здесь, как и в других случаях, мы видим #define, но он связан с функцией GenericType_1__ctor_m10_gshared, используемой только одним классом – GenericType.



Концептуальное осмысление обобщенной реализации



Концепция обобщенной реализации может быть достаточно сложной для понимания. Предметная область полна патологических случаев (тех же рекурсивных шаблонов). Поэтому здесь нужно выделить несколько основных принципов:




  • Реализация любого метода для обобщенного типа является обобщенной.

  • В некоторых случаях реализация методов является обобщенной только для определенного типа (например, вышеупомянутый тип с обобщенным параметром типа значения GenericType).

  • Типы с обобщенным параметром ссылочного типа используют полностью обобщенную реализацию, рассматривая параметры всех типов как System.Object.

  • Типы с параметрами двух и более типов могут быть частично обобщенными, если как минимум один из типов параметров является ссылочным.



Для любого обобщенного типа утилита il2cpp.exe всегда генерирует полностью обобщенные реализации методов. Другие реализации генерируются, только если это необходимо.



Обобщенные методы



Обобщенная реализация используется не только для обобщенных типов, но и для обобщенных методов. Обратите внимание, что в исходном скриптовом коде метод UsesDifferentGenericParameter использует параметр другого типа, нежели класс GenericType. Но при рассмотрении обобщенной реализации для класса GenericType мы не видели этого метода. Введя в поиск «UsesDifferentGenericParameter», мы видим, что реализация этого метода находится в файле GenericMethods0.cpp:



extern "C" Object_t * GenericType_1_UsesDifferentGenericParameter_TisObject_t_m15243_gshared (GenericType_1_t2159 * __this, Object_t * ___value, MethodInfo* method)
{
{
Object_t * L_0 = ___value;
return L_0;
}
}


Это полностью обобщенная реализация, принимающая тип Object_t*. И хотя этот метод обобщенного типа, поведение было бы таким же для необобщенного. Можно утверждать, что il2cpp.exe всегда пытается генерировать минимальное количество кода для реализации методов с обобщенными параметрами.



Заключение



Обобщенная реализация – одно из самых важных улучшений в IL2CPP с момента ее выхода, позволяющее значительно уменьшить размер кода C++ для реализаций методов с одинаковым поведением. Мы продолжаем искать решения для уменьшения размера бинарных файлов и пытаемся использовать больше преимуществ и возможностей обобщенной реализации.



В следующей статье мы поговорим о генерации оберток p/invoke, а также о маршалинге типов между управляемым и неуправляемым кодом.

Источник: Хабрахабр

Категория: Операционные системы » Windows

Уважаемый посетитель, Вы зашли на сайт как незарегистрированный пользователь.
Мы рекомендуем Вам зарегистрироваться либо войти на сайт под своим именем.

Добавление комментария

Имя:*
E-Mail:
Комментарий:
Полужирный Наклонный текст Подчеркнутый текст Зачеркнутый текст | Выравнивание по левому краю По центру Выравнивание по правому краю | Вставка смайликов Выбор цвета | Скрытый текст Вставка цитаты Преобразовать выбранный текст из транслитерации в кириллицу Вставка спойлера
Введите два слова, показанных на изображении: *