Генерация типов в Runtime

Страницы:  1

Ответить
 

Professor Seleznov


Иногда в разработке возникают задачи, требующие создания типов в рантайме. Чаще всего это необходимо при написании декларативных сервисов, высокопроизводительных мапперов или систем с динамическим проксированием.
Допустим, мы хотим сгенерировать тип с таким интерфейсом:
public interface IStudent
{
string Name { get; set; }
int Some(string value);
}
Логика метода Some (просто для примера):
public int Some(string value)
{
string str = Name + value;
Console.WriteLine(str);
return str.Length;
}
Reflection.Emit
Можно использовать System.Reflection.Emit.
// 1. Создаем сборку, модуль и тип
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("StudentReflectionEmitAssembly"), AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("StudentReflectionEmitModule");
TypeBuilder typeBuilder = moduleBuilder.DefineType("Student", TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable, typeof(object), new[] { typeof(IStudent) });
// 2. Объявляем backing-поле и свойство
FieldBuilder nameField = typeBuilder.DefineField("_name", typeof(string), FieldAttributes.Private);
PropertyBuilder nameProperty = typeBuilder.DefineProperty("Name", PropertyAttributes.None, typeof(string), Type.EmptyTypes);
// 3. Создаем сеттеры, геттеры и метод
MethodBuilder getter = typeBuilder.DefineMethod("get_Name", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.NewSlot | MethodAttributes.Final, typeof(string), Type.EmptyTypes);
MethodBuilder setter = typeBuilder.DefineMethod("set_Name", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.SpecialName | MethodAttributes.NewSlot | MethodAttributes.Final, typeof(void), new[] { typeof(string) });
MethodBuilder someMethod = typeBuilder.DefineMethod("Some", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Final, typeof(int), new[] { typeof(string) });
// 4. Пишем логику свойств
ILGenerator getterIl = getter.GetILGenerator();
getterIl.Emit(OpCodes.Ldarg_0);
getterIl.Emit(OpCodes.Ldfld, nameField);
getterIl.Emit(OpCodes.Ret);
nameProperty.SetGetMethod(getter);
ILGenerator setterIl = setter.GetILGenerator();
setterIl.Emit(OpCodes.Ldarg_0);
setterIl.Emit(OpCodes.Ldarg_1);
setterIl.Emit(OpCodes.Stfld, nameField);
setterIl.Emit(OpCodes.Ret);
nameProperty.SetSetMethod(setter);
// 5. Пишем логику метода Some
ILGenerator someIl = someMethod.GetILGenerator();
LocalBuilder str = someIl.DeclareLocal(typeof(string));
someIl.Emit(OpCodes.Ldarg_0);
someIl.Emit(OpCodes.Call, getter);
someIl.Emit(OpCodes.Ldarg_1);
someIl.Emit(OpCodes.Call, typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) }));
someIl.Emit(OpCodes.Stloc, str);
someIl.Emit(OpCodes.Ldloc, str);
someIl.Emit(OpCodes.Call, typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) }));
someIl.Emit(OpCodes.Ldloc, str);
someIl.Emit(OpCodes.Call, typeof(string).GetProperty(nameof(string.Length)).GetGetMethod());
someIl.Emit(OpCodes.Ret);
// 6. Финализация типа
typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);
Type studentType = typeBuilder.CreateTypeInfo().AsType();
Получился многословный код. Хотя он очень шаблонный, можно написать небольшую обертку и получить:
AssemblyFactory
Это позволит описывать типы в декларативном стиле:
Type studentType = AssemblyFactory.CreateAssembly("StudentReflectionEmitAssembly")
.CreateClass("Student", typeof(object), new[] { typeof(IStudent) })
.AddProperty(typeof(string), nameof(IStudent.Name))
.AddMethod(typeof(int), nameof(IStudent.Some), new[] { typeof(string) }, (ilGenerator, typeBuilder) =>
{
LocalBuilder str = ilGenerator.DeclareLocal(typeof(string));
ilGenerator.Emit(OpCodes.Ldarg_0);
ilGenerator.Emit(OpCodes.Call, typeBuilder.GetProperty(nameof(IStudent.Name)).GetGetMethod());
ilGenerator.Emit(OpCodes.Ldarg_1);
ilGenerator.Emit(OpCodes.Call, typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) }));
ilGenerator.Emit(OpCodes.Stloc, str);
ilGenerator.Emit(OpCodes.Ldloc, str);
ilGenerator.Emit(OpCodes.Call, typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) }));
ilGenerator.Emit(OpCodes.Ldloc, str);
ilGenerator.Emit(OpCodes.Call, typeof(string).GetProperty(nameof(string.Length)).GetGetMethod());
ilGenerator.Emit(OpCodes.Ret);
})
.Build();
Для этого напишем фабрику:
private class AssemblyFactory
{
// Создает сборку
public static AssemblyFactory CreateAssembly(string name)
{
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(name), AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(name);
return new AssemblyFactory(moduleBuilder);
}
private readonly ModuleBuilder moduleBuilder;
private AssemblyFactory(ModuleBuilder moduleBuilder)
{
this.moduleBuilder = moduleBuilder;
}
// Создает класс в сборке
public DynamicTypeBuilder CreateClass(string name, Type baseType, Type[] interfaces)
{
TypeBuilder typeBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable, baseType, interfaces);
return new DynamicTypeBuilder(typeBuilder);
}
}
И билдер классов:
private sealed class DynamicTypeBuilder(TypeBuilder typeBuilder)
{
// те же атрибуты, что и раньше
private const MethodAttributes DefaultMethodAttributes = MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Final;
private const MethodAttributes PropertyMethodAttributes = DefaultMethodAttributes | MethodAttributes.SpecialName;
// сохраняем поля и свойства для доступа
private readonly IDictionary<string, FieldBuilder> fields = new Dictionary<string, FieldBuilder>(StringComparer.Ordinal);
private readonly IDictionary<string, PropertyBuilder> properties = new Dictionary<string, PropertyBuilder>(StringComparer.Ordinal);
// создание полей очень простое
public DynamicTypeBuilder AddField(Type type, string name)
{
fields[name] = typeBuilder.DefineField(name, type, FieldAttributes.Public);
return this;
}
// свойство чуть сложнее, но так же взято из кода выше
public DynamicTypeBuilder AddProperty(Type type, string name)
{
FieldBuilder field = typeBuilder.DefineField($"_{name}", type, FieldAttributes.Private);
fields[name] = field;
PropertyBuilder property = typeBuilder.DefineProperty(name, PropertyAttributes.None, type, Type.EmptyTypes);
properties[name] = property;
MethodBuilder getter = typeBuilder.DefineMethod($"get_{name}", PropertyMethodAttributes, type, Type.EmptyTypes);
ILGenerator getterIl = getter.GetILGenerator();
getterIl.Emit(OpCodes.Ldarg_0);
getterIl.Emit(OpCodes.Ldfld, field);
getterIl.Emit(OpCodes.Ret);
property.SetGetMethod(getter);
MethodBuilder setter = typeBuilder.DefineMethod($"set_{name}", PropertyMethodAttributes, typeof(void), new[] { type });
ILGenerator setterIl = setter.GetILGenerator();
setterIl.Emit(OpCodes.Ldarg_0);
setterIl.Emit(OpCodes.Ldarg_1);
setterIl.Emit(OpCodes.Stfld, field);
setterIl.Emit(OpCodes.Ret);
property.SetSetMethod(setter);
return this;
}
public FieldBuilder GetField(string name)
{
return fields[name];
}
public PropertyBuilder GetProperty(string name)
{
return properties[name];
}
// создание методов делегируется вызывающей стороне
public DynamicTypeBuilder AddMethod(Type returnType, string name, Type[] parameterTypes, Action<ILGenerator, DynamicTypeBuilder> emit)
{
MethodBuilder method = typeBuilder.DefineMethod(name, DefaultMethodAttributes, returnType, parameterTypes);
emit(method.GetILGenerator(), this);
return this;
}
// и само создание типа
public Type Build()
{
typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);
return typeBuilder.CreateTypeInfo().AsType();
}
}
Этого уже достаточно, чтобы создавать DTO и писать простые методы, но писать IL — не самое приятное занятие. На этом моменте нужно подумать: а что может генерировать логику в IL?
Expression Trees
Сначала нужно вкратце разобраться, как оно работает. Работа с ним идет через статические методы System.Linq.Expressions.Expression.
Допустим, мы хотим построить дерево для (User u) => u.Age >= 18.
Для построения дерева вызывается метод Expression.Lambda, он получает дженерик делегата Func<>, Action<>, тело и параметры. Нужно сначала создать параметры через Expression.Parameter и передать в Lambda. Если типы параметров и делегатов не совпадают — будет выброшено исключение.
// входящие параметры описываются какой тип и какое имя (имена могут повторяться или их может не быть, они нужны в основном для отладки)
ParameterExpression paramUser = Expression.Parameter(typeof(User), "u");
Expression body = ...;
Expression<Predicate<User>> lambda = Expression.Lambda<Predicate<User>>(
body, // тело выражения
paramUser // и входящие параметры передаются здесь
);
Теперь нужно получить поле Age и сравнить его с 18.
// получаем поле, с которым хотим работать
MemberExpression propAge = Expression.PropertyOrField(paramUser, "Age");
// создаем константу, так как можно работать только с Expression
ConstantExpression const18 = Expression.Constant(18, typeof(int));
// и делаем проверку «больше или равно». Любое Expression может быть телом, в нашем случае это будет greaterOrEqual
BinaryExpression greaterOrEqual = Expression.GreaterThanOrEqual(propAge, const18);
Полный код выглядит так:
ParameterExpression paramUser = Expression.Parameter(typeof(User), "u");
MemberExpression propAge = Expression.PropertyOrField(paramUser, "Age");
ConstantExpression const18 = Expression.Constant(18, typeof(int));
BinaryExpression greaterOrEqual = Expression.GreaterThanOrEqual(propAge, const18);
Expression<Predicate<User>> lambda = Expression.Lambda<Predicate<User>>(
greaterOrEqual,
paramUser
);
// и когда дерево собрано его можно скомпилировать
Predicate<User> compiled = lambda.Compile();
// и использовать
if (compiled(new User(name: "Anton", age: 20)))
{
Console.WriteLine("Hello");
}
Объеденяем
И тут приходит мысль: что если попробовать писать методы, используя Expression Tree?
Сам по себе IL не работает в ООП, и все методы по своей сути — это статические функции. А когда функция используется как метод класса, нулевым аргументом подставляется экземпляр типа. Тогда теоретически можно написать делегат с первым параметром “self” и написать метод на Expression Tree.
Легальных способов подсунуть IL нет, поэтому прибегнем к грязному свинству и черной магии — к рефлексии.
Если сильно покопаться в Expression Tree, а точнее в том, как и где компилируется IL, можно найти тип System.Linq.Expressions.Compiler.LambdaCompiler - он записывает IL. В конструктор принимает LambdaExpression и AnalyzedTree. AnalyzedTree — это проанализированное дерево, оно создает scope генерации и создается через System.Linq.Expressions.Compiler.VariableBinder.Bind. Естественно, всё это internal-классы.
Найдем типы:
Assembly expressionsAssembly = typeof(Expression).Assembly;
Type variableBinderType = expressionsAssembly.GetType("System.Linq.Expressions.Compiler.VariableBinder", throwOnError: true);
Type lambdaCompilerType = expressionsAssembly.GetType("System.Linq.Expressions.Compiler.LambdaCompiler", throwOnError: true);
Создадим LambdaCompiler:
object analyzedTree = variableBinderType.GetMethod("Bind", PrivateStatic).Invoke(null, new object[] { expression });
object compiler = lambdaCompilerType.GetConstructor(
PrivateInstance,
null,
new[] { analyzedTree.GetType(), typeof(LambdaExpression) },
null).Invoke(new[] { analyzedTree, expression });
Создаем метод в билдере. Не забываем, что в делегате первым аргументом указывается объект, которому должен принадлежать метод, но в самом методе он, естественно, не виден. И подсунем ILGenerator в компилятор в поле _ilg:
MethodBuilder method = typeBuilder.DefineMethod(name, DefaultMethodAttributes, expression.ReturnType, delegateParameters.Skip(1).ToArray());
lambdaCompilerType.GetField("_ilg", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, method.GetILGenerator());
К сожалению для нас, это не всё. По умолчанию лямбда имеет первым аргументом контекст замыкания. Нужно сказать компилятору, что замыкания нет (_hasClosureArgument = false), и подменить структуру метода в _method:
DynamicMethod signatureMethod = new DynamicMethod(method.Name + "_ExpressionSignature", expression.ReturnType, delegateParameters, method.Module, skipVisibility: true);
lambdaCompilerType.GetField("_hasClosureArgument", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, false);
lambdaCompilerType.GetField("_method", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, signatureMethod);
Осталось вызвать метод EmitLambdaBody, который запишет IL в наш ILGenerator:
lambdaCompilerType.GetMethod("EmitLambdaBody", BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null).Invoke(compiler, null);
И вуаля! Теперь методы можно писать на Expression Tree. Но есть нюансы работы с self. Перепишем наш метод Some на Expression Tree:
Type studentType = AssemblyFactory.CreateAssembly("StudentWithoutInterfaceExpressionTreeAssembly")
.CreateClass("Student", typeof(object), new[] { typeof(IStudent) })
.AddProperty(typeof(string), "Name")
.AddMethod("Some", typeBuilder =>
{
// Так как тип ещё не создан, нужно принимать object
ParameterExpression self = Expression.Parameter(typeof(object), "self");
ParameterExpression value = Expression.Parameter(typeof(string), "value");
// И конвертировать в ожидаемый тип. Повезло, что TypeBuilder — наследник Type.
UnaryExpression typedSelf = Expression.Convert(self, typeBuilder.Type);
// Мы создавали свойство, но при попытке доступа к нему будет ошибка. Опять же из-за нескомпилированного типа.
// Поэтому можно работать только с полями, зато можно работать с любыми полями.
MemberExpression name = Expression.Field(typedSelf, typeBuilder.GetField("_Name"));
// находим методы string.Concat и Console.WriteLine
MethodInfo concatMethod = typeof(string).GetMethod(nameof(string.Concat), new[] { typeof(string), typeof(string) });
MethodInfo writeLineMethod = typeof(Console).GetMethod(nameof(Console.WriteLine), new[] { typeof(string) });
// Описываем переменную, в которую сохраним конкатенацию
ParameterExpression str = Expression.Variable(typeof(string), "str");
ParameterExpression[] variables = new[] { str };
// string.Concat(_Name, value)
MethodCallExpression concatExpression = Expression.Call(concatMethod, name, value);
// str = string.Concat(_Name, value)
BinaryExpression assign = Expression.Assign(str, concatExpression);
// Console.WriteLine(str);
MethodCallExpression callWriteLine = Expression.Call(writeLineMethod, str);
// str.Length
MemberExpression returnValue = Expression.Property(str, nameof(string.Length));
// создаем блок, первым аргументом всегда идут переменные которые используются в этом блоке
// а последним должен быть return
BlockExpression body = Expression.Block(
variables,
assign,
callWriteLine,
returnValue
);
return Expression.Lambda<Func<object, string, int>>(
body,
self, value
);
})
.Build();

Полный листинг фабрики&#58;

private class AssemblyFactory
{
public static AssemblyFactory CreateAssembly(string name)
{
AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName(name), AssemblyBuilderAccess.Run);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule(name);
return new AssemblyFactory(moduleBuilder);
}
private readonly ModuleBuilder moduleBuilder;
private AssemblyFactory(ModuleBuilder moduleBuilder)
{
this.moduleBuilder = moduleBuilder;
}
public DynamicTypeBuilder CreateClass(string name, Type baseType, Type[] interfaces)
{
TypeBuilder typeBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.Serializable, baseType, interfaces);
return new DynamicTypeBuilder(typeBuilder);
}
}
private sealed class DynamicTypeBuilder(TypeBuilder typeBuilder)
{
private const MethodAttributes DefaultMethodAttributes =
MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig | MethodAttributes.NewSlot | MethodAttributes.Final;
private const MethodAttributes PropertyMethodAttributes = DefaultMethodAttributes | MethodAttributes.SpecialName;
private readonly IDictionary<string, FieldBuilder> fields = new Dictionary<string, FieldBuilder>(StringComparer.Ordinal);
private readonly IDictionary<string, PropertyBuilder> properties = new Dictionary<string, PropertyBuilder>(StringComparer.Ordinal);
public Type Type => typeBuilder;
public DynamicTypeBuilder AddField(Type type, string name)
{
fields[name] = typeBuilder.DefineField(name, type, FieldAttributes.Public);
return this;
}
public DynamicTypeBuilder AddProperty(Type type, string name)
{
FieldBuilder field = typeBuilder.DefineField($"_{name}", type, FieldAttributes.Private);
fields[field.Name] = field;
PropertyBuilder property = typeBuilder.DefineProperty(name, PropertyAttributes.None, type, Type.EmptyTypes);
properties[name] = property;
MethodBuilder getter = typeBuilder.DefineMethod($"get_{name}", PropertyMethodAttributes, type, Type.EmptyTypes);
ILGenerator getterIl = getter.GetILGenerator();
getterIl.Emit(OpCodes.Ldarg_0);
getterIl.Emit(OpCodes.Ldfld, field);
getterIl.Emit(OpCodes.Ret);
property.SetGetMethod(getter);
MethodBuilder setter = typeBuilder.DefineMethod($"set_{name}", PropertyMethodAttributes, typeof(void), new[] { type });
ILGenerator setterIl = setter.GetILGenerator();
setterIl.Emit(OpCodes.Ldarg_0);
setterIl.Emit(OpCodes.Ldarg_1);
setterIl.Emit(OpCodes.Stfld, field);
setterIl.Emit(OpCodes.Ret);
property.SetSetMethod(setter);
return this;
}
public FieldBuilder GetField(string name)
{
return fields[name];
}
public PropertyBuilder GetProperty(string name)
{
return properties[name];
}
public DynamicTypeBuilder AddMethod(Type returnType, string name, Type[] parameterTypes, Action<ILGenerator, DynamicTypeBuilder> emit)
{
MethodBuilder method = typeBuilder.DefineMethod(name, DefaultMethodAttributes, returnType, parameterTypes);
emit(method.GetILGenerator(), this);
return this;
}
public DynamicTypeBuilder AddMethod<TDelegate>(string name, Func<DynamicTypeBuilder, Expression<TDelegate>> expressionFactory)
where TDelegate : Delegate
{
Expression<TDelegate> expression = expressionFactory(this);
Type[] delegateParameters = expression.Parameters.Select(x => x.Type).ToArray();
if (delegateParameters.Length == 0)
{
throw new ArgumentException("Expression must have the instance as its first parameter.", nameof(expression));
}
Assembly expressionsAssembly = typeof(Expression).Assembly;
Type variableBinderType = expressionsAssembly.GetType("System.Linq.Expressions.Compiler.VariableBinder", throwOnError: true);
Type lambdaCompilerType = expressionsAssembly.GetType("System.Linq.Expressions.Compiler.LambdaCompiler", throwOnError: true);
object analyzedTree = variableBinderType.GetMethod("Bind", BindingFlags.NonPublic | BindingFlags.Static).Invoke(null, new object[] { expression });
object compiler = lambdaCompilerType.GetConstructor(
BindingFlags.NonPublic | BindingFlags.Instance,
null,
new[] { analyzedTree.GetType(), typeof(LambdaExpression) },
null).Invoke(new[] { analyzedTree, expression });
MethodBuilder method = typeBuilder.DefineMethod(name, DefaultMethodAttributes, expression.ReturnType, delegateParameters.Skip(1).ToArray());
DynamicMethod signatureMethod = new DynamicMethod(method.Name + "_ExpressionSignature", expression.ReturnType, delegateParameters, method.Module, skipVisibility: true);
lambdaCompilerType.GetField("_ilg", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, method.GetILGenerator());
lambdaCompilerType.GetField("_hasClosureArgument", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, false);
lambdaCompilerType.GetField("_method", BindingFlags.NonPublic | BindingFlags.Instance).SetValue(compiler, signatureMethod);
lambdaCompilerType.GetMethod("EmitLambdaBody", BindingFlags.NonPublic | BindingFlags.Instance, null, Type.EmptyTypes, null).Invoke(compiler, null);
return this;
}
public Type Build()
{
typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);
return typeBuilder.CreateTypeInfo().AsType();
}
}
Заключение
Мы прошли путь от низкоуровневых IL до высокоуровневых Expression Trees. Такой подход позволяет создавать динамические типы, не жертвуя при этом читаемостью.
Статья и так получилась большой, может позже разберу Roslyn как альтернативный способ.-Источник
 
Loading...
Error