1

For the following interface and struct:

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

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

I would like get the following lambda expression via reflection:

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

Unfortunately this doesn't compile: cs8153: an expression tree lambda may not contain a call to a method, property, or indexer that returns by reference. It seems get any member of ref struct is not supported via reflection.

So I have to go for System.Reflection.Emit to generate the following accessor class:

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;
    ...
}

and get the following lambda expression via reflection:

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

Here is my code to generate the Accessor class using 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);
}

When consuming the generated Accessor class, I get InvalidProgramException: Common Language Runtime detected an invalid program. when invoking the generated getter; and get System.MethodAccessException: Attempt by method 'Accessor.SetField0(IRecord`1<Entity>, Int32)' to access IRecord`1<Entity>.get_FieldValues()' failed. when invoking the generated setter.

What am I doing wrong? I've spent whole day for this and is pretty frustrated. Any help will be very much appreciated!

Weifen Luo
  • 603
  • 5
  • 13
  • 2
    Have you done the standard dance for getting generated code correct, that is, inspect the IL you get if you compile the code the obvious way (through `ildasm` or something like https://sharplab.io), then look at what you need to generate? – Jeroen Mostert Oct 15 '20 at 12:53
  • 1
    Your code will not compile as-is, incidentally, because you're using `FieldValues` in your generated code but `Values` in your definition. Making the example self-contained simplifies following along for the people at home. For troubleshooting on your end (I can't see any obvious mistakes either, but `ref` doubtlessly is a tricky thing), you can emit to an assembly instead and save it, then compare decompilation of that with the "correct" IL. Some special flag or modifier byte or other might be missing. – Jeroen Mostert Oct 15 '20 at 13:21
  • @JeroenMostert Thanks. I've edited the question and eliminated the compile-error. I'm making a standalone console app to save the generated assembly so that I can use `ildasm` to inspect it. – Weifen Luo Oct 15 '20 at 13:41
  • Changing `IRecord` interface from `internal` to `public` makes the setter working correctly. There must be something wrong with the emitted getter code. Still investigating... – Weifen Luo Oct 15 '20 at 14:07
  • 1
    Oh, I completely missed that. :-P Yeah, keep in mind that generated code needs to respect visibility rules when it's generated as a dynamic module/assembly (not when generated as a dynamic method). It can't access `internal` types/members because it's not living in the assembly that generated the code. – Jeroen Mostert Oct 15 '20 at 14:09
  • @JeroenMostert The problem has been solved. Thank you very much! – Weifen Luo Oct 15 '20 at 14:20

1 Answers1

0

Fixed.

  1. The IRecord<T> interface must be public;

  2. Remove two lines of opcode emitting:

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);
}
Weifen Luo
  • 603
  • 5
  • 13