Pull to refresh

Unity3d CodeDOM. Генерация кода на лету

Level of difficultyMedium
Reading time4 min
Views1.1K

Привет, Хабр!

Мне пришлось столкнуться с генерацией кода, в процессе поисков я наткнулся на специфический код-генератор под .NET (CodeDOM), который заработал у меня в среде Unity3d.

Для начала какие методы и инструменты были мне знакомы на момент решения задачи:

I. Генерация кода в Unity3d у большинства будет ассоциироваться с Roslyn Source Generator. К сожалению мне не довелось воспользоваться этой системой генерации на собственном опыте

II. Так же мне в руки попадалась библиотека Roslyn C# - Runtime Compiler. С её помощью можно из текста скомпилировать исполняемый код даже внутри билда, на большинстве платформ, и сложности с безопасностью библиотека берёт на себя - и это прекрасно!

III. Так же вам ничто не помешает сгенерировать текст файла скрипта вручную, сложить в виде .cs-файла внутри проекта, дождаться рекомпиляции и воспользоваться сгенерированным кодом

Всё описанное выше - прекрасные инструменты, подходящие для решения определенных задач.

Вот какая задача была у нас:

  • Editor-time т.е. в редакторе (НЕ в билде)

  • Во время импорта 3d-модели происходит её специальная обработка, и вызывается generic-метод Mesh.SetVertexBufferData()

  • тип передаваемый в этот метод зависит от набора аттрибутов в 3d-модели (position, normal, tangent uv0 и т.п.)

  • тип неизвестен = тип нужно сгенерировать и вызвать метод с помощью рефлексии

Т.к. мы пишем собственную библиотеку, мы старались минимизировать зависимости и оставили платный II (Roslyn C# - Runtime Compiler) в качестве последнего, запасного варианта. I (Roslyn Source Generator) в свою очередь, если верить документации, требует подключения NuGet-пакетов

Первым делом я посчитал сколько всего возможно различных типов, и их было действительно много. более 8 000 вариантов

Я сгенерировал их самым простым способом III. полученный файл был очень тяжелым, и IDE мне об этом говорила, 100 000 строк. При условии что большая часть этих типов никогда не будет использована, и рекомпиляция Assembly с таким исходным файлом сильно замедлялась, это выглядело плохим решением.

3 Mb исходного кода 100 000 строк для 8 000 типов структур
3 Mb исходного кода 100 000 строк для 8 000 типов структур

Поэтому я искал способ сгенерировать только те типы, которые используются и воспользоваться ими "здесь и сейчас". Т.е. получить результат компиляции сразу после генерации, без смены контекста выполнения (напоминаю в случае с III нужно ждать рекомпиляцию, а значит контекст выполнения сменится). В процессе поисков я и наткнулся на CodeDOM-генерацию

https://learn.microsoft.com/ru-ru/dotnet/framework/reflection-and-codedom/using-the-codedom

Генерация осуществляются с помощью создания специальных объектов-дескрипторов. на каждый элемент языка:

  • неймспейс

  • поле

  • тип

  • аттрибут

  • метод

  • параметр

Создаётся отдельный instance объекта-дескриптора. потом всё это складывается в CompileUnit, вызывается метод CodeDomProvider.CompileAssemblyFromDom(CompileUnit) и на выходе получаем Assembly с описанными нами типами, полями и т.п.

Рассмотрим на примере. Допустим нам нужно сгенерировать следующий тип:

[StructLayout(LayoutKind.Sequential)]
public struct CustomTypeName
{
    private int TesField;
}

Сначала создаём параметры компиляции, и укажем наши зависимости:

var codeProvider = CodeDomProvider.CreateProvider("c#");
var compilerParams = new CompilerParameters();
compilerParams.GenerateInMemory = true;
compilerParams.GenerateExecutable = false;
compilerParams.TreatWarningsAsErrors = false;
compilerParams.ReferencedAssemblies.Add(typeof(Vector2).Assembly.Location);
compilerParams.ReferencedAssemblies.Add(typeof(half4).Assembly.Location);
compilerParams.ReferencedAssemblies.Add(Assembly.Load("netstandard, Version=2.1.0.0, Culture=neutral, PublicKeyToken=cc7b13ffcd2ddd51").Location); 

Последняя строчка интересна, т.к. Unity3d использует свою специфическую сборку библиотек .NET. и получение сборки через typeof(System.Object).Assembly давало ссылку на другую сборку, и в результате генерации давало ошибку дубликатов с mscorlib.dll, подробнее об этой проблеме и её решении можно почитать здесь: https://discussions.unity.com/t/c-codedom-generation-mscorlib-dll-unityengine-dll-conflict/1608849

ошибка дубликата при добавлении ссылки на System через typeof(System.Object).Assembly
ошибка дубликата при добавлении ссылки на System через typeof(System.Object).Assembly

далее создадим, для примера набор из 1 аттрибута, который будет подписан к нашему типу

var customAttributes = new CodeAttributeDeclarationCollection(new[]{
   new CodeAttributeDeclaration(
       new CodeTypeReference(typeof(StructLayoutAttribute)), 
       new CodeAttributeArgument(
           new CodePrimitiveExpression(LayoutKind.Sequential)))
}); 

код выше аналогичен следующему исходному коду:

[StructLayout(LayoutKind.Sequential)]

далее создадим дескриптор типа. сделаем наш тип структурой, и укажем ему наш набор из 1 аттрибута

var typeDeclaration = new CodeTypeDeclaration("CustomTypeName")
{
   IsStruct = true, 
   CustomAttributes = customAttributes, 
   TypeAttributes = TypeAttributes.Public | TypeAttributes.SequentialLayout,
};

добавим поле

typeDeclaration.Members.Add(new CodeMemberField(typeof(int), "TestField"));

создадим namespace, и добавим туда наш тип

var codeNamespace = new CodeNamespace();
codeNamespace.Types.Add(typeDeclaration);

Создадим финальный CodeCompileUnit , и добавим в него наш namespace

var unit = new CodeCompileUnit();
unit.Namespaces.Add(codeNamespace)

Вызовем последнюю функцию CodeDomProvider.CompileAssemblyFromDom

CompilerResults result = codeProvider.CompileAssemblyFromDom(compilerParams, compileUnit);

и из CompilerResults.CompiledAssembly получим наш готовый Type, который тут же можно использовать! К слову: dll-файл библиотеки в этот момент тоже создаётся и кладётся в User/AppData/Temp

Type type = result.CompiledAssembly.GetType("CustomTypeName"); 

Теперь у нас есть тип, с которым мы можем работать через рефлексию, давайте для примера создадим значение такого типа и выставим на нем поле в значение 15

var instance = Activator.CreateInstance(type);
var fieldInfo = type.GetField("TesField", BindingFlags.Instance | BindingFlags.NonPublic);
fieldInfo.SetValue(instance, 15);

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

Array array = Array.CreateInstance(type, length);

for (int i = length - 1; i >= 0; i--)
{
  var element = array.GetValue(i);
  fieldInfo[0].SetValue(element, InputArray1[i]);
  fieldInfo[1].SetValue(element, InputArray2[i]);
  array.SetValue(element, i);
}

полный код генерации на Github gists:

https://gist.github.com/mitay-walle/12e06e2b525709c98c1dc1fb47f38279

Повторюсь, что данный тип генерации подошел нам больше из-за возможности получить результат компиляции в том же контексте выполнения, что и сама генерация. Возможно Roslyn Code Generator способен на то же самое, но мне не пришлось это выяснить

Спасибо за внимание, всем успехов!

Tags:
Hubs:
Total votes 1: ↑1 and ↓0+1
Comments0

Articles