Система.Reflection.Emit: поле или свойство ссылочной структуры

#c# #reflection #emit #ref-struct

#c# #отражение #испускать #ссылка-структура

Вопрос:

Для следующего интерфейса и структуры:

 internal interface IRecord<T> where T : struct
{
    ref T Values { get; }
}

public struct Entity
{
    public int Field1;
    ...
}
  

Я хотел бы получить следующее лямбда-выражение через отражение:

 Expression<Func<IRecord<Entity>, int>> getter = x => x.Values.Field1;
Expression<Action<IRecord<Entity>, int>> setter = (x, value) => x.Values.Field1 = value;
  

К сожалению, это не компилируется : cs8153: an expression tree lambda may not contain a call to a method, property, or indexer that returns by reference . Кажется, получить какой-либо член ref struct не поддерживается через отражение.

Итак, я должен пойти на System.Reflection.Emit создание следующего класса доступа:

 public static class Accessor
{
    public static int GetField1(IRecord<Entity> record) => record.Values.Field1;
    public static void SetField1(IRecord<Entity> record, int value) => record.Values.Field1 = value;
    ...
}
  

и получаем следующее лямбда-выражение через отражение:

 Expression<Func<IRecord<Entity>, int>> getter = x => Accessor.GetField1(x);
Expression<Action<IRecord<Entity>, int>> setter = (x, value) => Accessor.SetField1(x, value);
  

Вот мой код для создания Accessor класса с использованием System.Reflection.Emit :

 private static readonly ModuleBuilder ModuleBuilder = GetModuleBuilder();

private static ModuleBuilder GetModuleBuilder()
{
    AssemblyName assemblyName = new AssemblyName("AccessTypeBuilder");
    AssemblyBuilder assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
    ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("Module");
    return moduleBuilder;
}

public static Type BuildAccessorType(Type fieldValuesType)
{
    TypeBuilder typeBuilder = ModuleBuilder.DefineType("Accessor", TypeAttributes.Public | TypeAttributes.Abstract | TypeAttributes.Sealed, typeof(object));
    BuildAccessor(typeBuilder, typeof(Entity), "Field1", typeof(int));
    return typeBuilder.CreateType();
}

private static void BuildAccessor(TypeBuilder typeBuilder, Type fieldValuesType, string fieldName, Type dataType)
{
    typeBuilder.DefineGetter(fieldValuesType, $"Get{fieldName}", dataType, fieldName);
    typeBuilder.DefineSetter(fieldValuesType, $"Set{fieldName}", dataType, fieldName);
}

private static void DefineField(this TypeBuilder typeBuilder, Type dataType, string fieldName)
{
    typeBuilder.DefineField(fieldName, dataType, FieldAttributes.Public);
}

private static Type RecordType(this Type fieldValuesType)
{
    return typeof(IRecord<>).MakeGenericType(fieldValuesType);
}

private static MethodInfo FieldValues(this Type fieldValuesType)
{
    var recordType = fieldValuesType.RecordType();
    var property = recordType.GetProperty(nameof(IRecord<int>.Values));
    return property.GetMethod;
}

private static FieldInfo Field(this Type fieldValuesType, string fieldName) => fieldValuesType.GetField(fieldName, BindingFlags.Public | BindingFlags.Instance);

private static void DefineGetter(this TypeBuilder typeBuilder, Type fieldValuesType, string methodName, Type dataType, string fieldName)
{
    var method = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Static, dataType, new Type[] { fieldValuesType.RecordType() });
    var methodBody = method.GetILGenerator();
    methodBody.EmitGetter(fieldValuesType, fieldName);
}

private static void EmitGetter(this ILGenerator methodBody, Type fieldValuesType, string fieldName)
{
    methodBody.Emit(OpCodes.Ldarg_0);
    methodBody.Emit(OpCodes.Callvirt, fieldValuesType.FieldValues());
    methodBody.Emit(OpCodes.Ldfld, fieldValuesType.Field(fieldName));
    methodBody.Emit(OpCodes.Stloc_0);
    methodBody.Emit(OpCodes.Ldloc_0);
    methodBody.Emit(OpCodes.Ret);
}

private static void DefineSetter(this TypeBuilder typeBuilder, Type fieldValuesType, string methodName, Type dataType, string fieldName)
{
    var method = typeBuilder.DefineMethod(methodName, MethodAttributes.Public | MethodAttributes.Static, typeof(void), new Type[] { fieldValuesType.RecordType(), dataType });
    var methodBody = method.GetILGenerator();
    methodBody.EmitSetter(fieldValuesType, fieldName);
}

private static void EmitSetter(this ILGenerator methodBody, Type fieldValuesType, string fieldName)
{
    methodBody.Emit(OpCodes.Nop);
    methodBody.Emit(OpCodes.Ldarg_0);
    methodBody.Emit(OpCodes.Callvirt, fieldValuesType.FieldValues());
    methodBody.Emit(OpCodes.Ldarg_1);
    methodBody.Emit(OpCodes.Stfld, fieldValuesType.Field(fieldName));
    methodBody.Emit(OpCodes.Ret);
}
  

При использовании сгенерированного Accessor класса я получаю InvalidProgramException: Common Language Runtime detected an invalid program. при вызове сгенерированного средства получения; и получаю System.MethodAccessException: Attempt by method 'Accessor.SetField0(IRecord`1<Entity>, Int32)' to access IRecord`1<Entity>.get_FieldValues()' failed. при вызове сгенерированного средства установки.

Что я делаю не так? Я потратил на это целый день и очень расстроен. Любая помощь будет очень признательна!

Комментарии:

1. Выполнили ли вы стандартный танец для корректного получения сгенерированного кода, то есть проверили IL, который вы получите, если скомпилируете код очевидным способом (через ildasm или что-то вроде sharplab.io ), затем посмотрите, что вам нужно сгенерировать?

2. Ваш код не будет компилироваться как есть, между прочим, потому что вы используете FieldValues в своем сгенерированном коде, но Values в своем определении. Создание автономного примера упрощает отслеживание для людей дома. Для устранения неполадок с вашей стороны (я тоже не вижу никаких очевидных ошибок, но ref , несомненно, это сложная вещь), вы можете вместо этого передать в сборку и сохранить ее, а затем сравнить декомпиляцию этого с «правильным» IL. Может отсутствовать какой-то специальный флаг или байт-модификатор или другой.

3. @JeroenMostert Спасибо. Я отредактировал вопрос и устранил ошибку компиляции. Я создаю отдельное консольное приложение для сохранения сгенерированной сборки, чтобы я мог использовать ildasm ее для проверки.

4. Изменение IRecord<T> интерфейса с internal на public делает сеттер корректно работающим. Должно быть, что-то не так с отправленным кодом получения. Все еще исследуется…

5. О, я полностью пропустил это. 😛 Да, имейте в виду, что сгенерированный код должен соблюдать правила видимости, когда он генерируется как динамический модуль / сборка (а не когда генерируется как динамический метод). Он не может получить доступ internal к типам / элементам, потому что он не находится в сборке, которая сгенерировала код.

Ответ №1:

Исправлено.

  1. IRecord<T> Интерфейс должен быть public ;

  2. Удалите две строки кода операции, испускающие:

 private static void EmitGetter(this ILGenerator methodBody, Type fieldValuesType, string fieldName)
{
    methodBody.Emit(OpCodes.Ldarg_0);
    methodBody.Emit(OpCodes.Callvirt, fieldValuesType.FieldValues());
    methodBody.Emit(OpCodes.Ldfld, fieldValuesType.Field(fieldName));
    //methodBody.Emit(OpCodes.Stloc_0); --removed
    //methodBody.Emit(OpCodes.Ldloc_0); --removed
    methodBody.Emit(OpCodes.Ret);
}