Привет, Хабр!
Мне пришлось столкнуться с генерацией кода, в процессе поисков я наткнулся на специфический код-генератор под .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 с таким исходным файлом сильно замедлялась, это выглядело плохим решением.

Поэтому я искал способ сгенерировать только те типы, которые используются и воспользоваться ими "здесь и сейчас". Т.е. получить результат компиляции сразу после генерации, без смены контекста выполнения (напоминаю в случае с 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
далее создадим, для примера набор из 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 способен на то же самое, но мне не пришлось это выяснить
Спасибо за внимание, всем успехов!