Search code examples
c#genericsreflection.emit

Cannot get interface signature correct when implementing via TypeBuilder and Emit


I have not worked much with dynamic classes and emit and I've been hitting my head against a brick wall trying to work this out. What I want is a helper to use when I am moving structs and other value types between managed and native memory.

I thought I generally got it, but I just can't seem to get my generating type to properly support the interface I defined. Whenever I finally call TypeBuilder.CreateType(), it throws an exception saying the type does not implement the first of the two methods in the interface, I'm sure it thinks I am not supporting the second one either, but I don't get that far. I've done it with and without DefineGenericParameters (passing the actual type I care about instead). I've tried lots of variations on, for example, the byte * parameter. I tried specifying the generic parameters telling it is a non-nullable value type. I have looked at the other questions here about emit and interfaces, and I think I have taken into account what those questions suggested.

I don't know if this needs a simple tweak, or if I'm fundamentally missing something.

using System;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;

namespace Testing
{
    class Program
    {
        static unsafe void Main(string[] args)
        {
            NativeValueAccessFactory factory = new NativeValueAccessFactory();
            INativeValueAccessor<XTestStruct> caller = (INativeValueAccessor<XTestStruct>)factory.GetNativeValueAccessor<XTestStruct>();
            byte[] buffer = new byte[1024];
            XTestStruct item1 = new XTestStruct(1000, 2000);

            fixed (byte* pBuffer = &buffer[0])
            {
                caller.WriteData(item1, pBuffer);
            }

            XTestStruct item2 = new XTestStruct(1, 1);
            fixed (byte* pBuffer = &buffer[0])
            {
                item2 = caller.ReadData(pBuffer);
            }

            // Compare item1 and item2
        }
    }

    public struct XTestStruct
    {
        public int i;
        public int j;
        public XTestStruct(int i, int j) { this.i = i; this.j = j; }
    }

    public unsafe interface INativeValueAccessor<T> where T : struct
    {
        T ReadData(byte* data);
        void WriteData(T item, byte* data);
    }

    public class NativeValueAccessFactory
    {
        public NativeValueAccessFactory() { }

        public object GetNativeValueAccessor<T>() where T : struct
        {
            Type itemType = typeof(T);

            var rand = new Random();
            var name = string.Format("{0}_{1}", itemType.Name, rand.Next());
            var assemblyName = new AssemblyName(name);
            var assemblyBuilder = Thread.GetDomain().DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule(name + ".dll");

            TypeBuilder typeBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public);
            typeBuilder.AddInterfaceImplementation(typeof(INativeValueAccessor<T>));

            // ReadData method
            MethodBuilder methodBuilder1 = typeBuilder.DefineMethod("ReadData", MethodAttributes.Public | MethodAttributes.Virtual);

            GenericTypeParameterBuilder[] generics1 = methodBuilder1.DefineGenericParameters("T");
            generics1[0].SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);
            methodBuilder1.SetReturnType(generics1[0]);
            methodBuilder1.SetParameters(typeof(byte*)); // also tried typeof(byte).MakeByRefType()

            ILGenerator codeGen1 = methodBuilder1.GetILGenerator();
            codeGen1.Emit(OpCodes.Ldarg_1);
            codeGen1.Emit(OpCodes.Ldobj, itemType);
            codeGen1.Emit(OpCodes.Ret);

            // WriteData method
            MethodBuilder methodBuilder2 = typeBuilder.DefineMethod("WriteData", MethodAttributes.Public | MethodAttributes.Virtual);

            GenericTypeParameterBuilder[] generics2 = methodBuilder2.DefineGenericParameters("T");
            generics2[0].SetGenericParameterAttributes(GenericParameterAttributes.NotNullableValueTypeConstraint);
            methodBuilder2.SetReturnType(null);
            methodBuilder2.SetParameters(generics2[0], typeof(byte*));

            ILGenerator codeGen2 = methodBuilder2.GetILGenerator();
            codeGen2.Emit(OpCodes.Ldarg_2);
            codeGen2.Emit(OpCodes.Ldarg_1);
            codeGen2.Emit(OpCodes.Stobj, itemType);
            codeGen2.Emit(OpCodes.Ret);

            Type generatedType = typeBuilder.CreateType(); // Throws exception here saying interface not implemented
            return Activator.CreateInstance(generatedType); // not at all confident this will work, but not even getting here
        }
    }
}

I do know lots of other ways to copy a struct between managed and native memory, but most of them are either massively slower (e.g. BinaryReader, Marshal.PtrToStructure) or inconvenient to the user of the class (needing to provide delegates that carry out the copies, etc.). I really wanted to see if this could be done with dynamic methods, and it seems it can, if I can just figure out what I am doing wrong.


Solution

  • My mistake was in having the dynamic methods set the parameters that were generic in the interface to generic in the dynamic method. That's wrong. At this point, since we are dealing with a specific type T, XTestStruct, the correct signatures for the methods are "XTestStruct ReadData(byte*)" and "void WriteData(XTestStruct, byte*)". By still treating those params as generic in the dynamic methods, I was basically setting the signature to "T ReadData(byte*)", which is not correct. This is the code I ended up with:

        TypeBuilder typeBuilder = moduleBuilder.DefineType(name, TypeAttributes.Public);
        typeBuilder.AddInterfaceImplementation(typeof(INativeValueAccessor<T>));
    
        // ReadData method
        MethodBuilder methodBuilder1 = typeBuilder.DefineMethod("ReadData", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig);
        methodBuilder1.SetReturnType(itemType);
        methodBuilder1.SetParameters(typeof(byte*));
    
        ILGenerator codeGen1 = methodBuilder1.GetILGenerator();
        codeGen1.Emit(OpCodes.Ldarg_1);
        codeGen1.Emit(OpCodes.Ldobj, itemType);
        codeGen1.Emit(OpCodes.Ret);
    
        // WriteData method
        MethodBuilder methodBuilder2 = typeBuilder.DefineMethod("WriteData", MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.HideBySig);
        methodBuilder2.SetReturnType(null);
        methodBuilder2.SetParameters(itemType, typeof(byte*));
    
        ILGenerator codeGen2 = methodBuilder2.GetILGenerator();
        codeGen2.Emit(OpCodes.Ldarg_2);
        codeGen2.Emit(OpCodes.Ldarg_1);
        codeGen2.Emit(OpCodes.Stobj, itemType);
        codeGen2.Emit(OpCodes.Ret);
    
        Type generatedType = typeBuilder.CreateType();
        return Activator.CreateInstance(generatedType);
    

    This, inserted into the full example above, ran without error, copied the data into memory and read it back out.