Mirco Gericke
Mirco Gericke

Reputation: 331

Reflection.Emit implementing generic method constraints of interface

I'm trying to dynamically generate implementations of interfaces using System.Reflection.Emit. To be able to generate implementations of generic methods I have to correctly apply all generic parameter constraints of the interface method to method in the generated class implementing it, but I can't figure out what I'm doing wrong with base class constraints.

When trying to build the type I receive the following error (translated):

System.TypeLoadException: 'The method "Error" in type "TestImplementation" of Assembly "TestAsm, Version=0.0.0.0, Culture=neutral, PublicKeyToken=null" tried to implicitly implement an interface method with weaker constraints for a type parameter.'

Here is a simple sample interface:

public interface ITest
{
    //Base type constraint seems to cause issues, also reproduces with other classes
    void Error<T>() where T : Encoding;

    // these all work as expected when code is generated for them
    //Task<T> A<T>(T input) where T : struct;
    //Task<T> B<T>(T input) where T : class, new();
    //Task<T> C<T>(T input) where T : IComparable<string>, IComparable, ICloneable;
}

And here is the type generator:

internal class Program
{
    private static Type Build()
    {
        // quite a lot of boilerplate is necessary for this, sorry!
        var asm = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("TestAsm"), AssemblyBuilderAccess.Run);
        var module = asm.DefineDynamicModule(asm.GetName().Name);


        var type = module.DefineType(
            "TestImplementation",
            TypeAttributes.Class | TypeAttributes.Public | TypeAttributes.AutoLayout | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit,
            typeof(object),
            new[] { typeof(ITest) }
        );
        var method = typeof(ITest).GetMethod("Error");

        var m = type.DefineMethod(
            method.Name,
            MethodAttributes.Public | MethodAttributes.Final | MethodAttributes.HideBySig | MethodAttributes.Virtual | MethodAttributes.NewSlot,
            CallingConventions.Standard | CallingConventions.HasThis,
            typeof(void),
            new Type[0]
        );

        //this is where the constraints are applied, I assume something is missing here
        var constraint = method.GetGenericArguments()[0];
        var constraintBuilder = m.DefineGenericParameters("T")[0];

        constraintBuilder.SetBaseTypeConstraint(constraint.BaseType);
        constraintBuilder.SetInterfaceConstraints(constraint.GetInterfaces());
        constraintBuilder.SetGenericParameterAttributes(constraint.GenericParameterAttributes);
        foreach (var attribute in BuildCustomAttributes(constraint.GetCustomAttributesData()))
        {
            constraintBuilder.SetCustomAttribute(attribute);
        }

        // dummy method body
        var il = m.GetILGenerator();
        il.EmitWriteLine("Sucess!");
        il.Emit(OpCodes.Ret);

        //fails right here \/
        return type.CreateType();
    }

    // I don't think attributes are actually necessary, but just in case..
    private static IEnumerable<CustomAttributeBuilder> BuildCustomAttributes(IEnumerable<CustomAttributeData> customAttributes)
    {
        return customAttributes.Select(attribute =>
        {
            var attributeArgs = attribute.ConstructorArguments.Select(a => a.Value).ToArray();
            var namedPropertyInfos = attribute.NamedArguments.Select(a => a.MemberInfo).OfType<PropertyInfo>().ToArray();
            var namedPropertyValues = attribute.NamedArguments.Where(a => a.MemberInfo is PropertyInfo).Select(a => a.TypedValue.Value).ToArray();
            var namedFieldInfos = attribute.NamedArguments.Select(a => a.MemberInfo).OfType<FieldInfo>().ToArray();
            var namedFieldValues = attribute.NamedArguments.Where(a => a.MemberInfo is FieldInfo).Select(a => a.TypedValue.Value).ToArray();
            return new CustomAttributeBuilder(attribute.Constructor, attributeArgs, namedPropertyInfos, namedPropertyValues, namedFieldInfos, namedFieldValues);
        });
    }

    private static void Main(string[] args)
    {
        var t = Build();
        var instance = (ITest)Activator.CreateInstance(t);
        instance.Error<List<object>>();
    }
}

What I've tried:

Is there something missing in my constraint generation? I expected the call constraintBuilder.SetBaseTypeConstraint(constraint.BaseType); to set the constraint for base classes.

Upvotes: 2

Views: 297

Answers (1)

Mirco Gericke
Mirco Gericke

Reputation: 331

Setting any interface constraints either overrides the BaseType constraint or causes a misleading error message. This fixes the issue:

if(constraint.BaseType != null)
{
    constraintBuilder.SetBaseTypeConstraint(constraint.BaseType);
}
else
{
    constraintBuilder.SetInterfaceConstraints(constraint.GetInterfaces());
}

This seems really counter intuitive because I'm now applying FEWER constraints to fix an error saying I'm applying too few constraints. This also works for declarations like

void Test<T>() where T: Example, IComparable<Example>`

even though I thought it wouldn't because I'm no longer applying the interface constraint if a base class constraint exists. This seemed odd, so I decided to investigate further and came to the conclusion that the GetInterfaces() method returning the correct interfaces is a coincidence or an implementation detail. The documentation makes no mention of this and instead suggests using the GetGenericParameterConstraints method.

Here is the solution I finally implemented:

constraintBuilder.SetBaseTypeConstraint(constraint.BaseType);
constraintBuilder.SetInterfaceConstraints(constraint.GetInterfaces());

was replaced with

var interfaceList = new List<Type>();
foreach (var restriction in constraint.GetGenericParameterConstraints())
{
        if (restriction.IsClass)
        {
                constraintBuilder.SetBaseTypeConstraint(restriction);
        }
        else
        {
                interfaceList.Add(restriction);
        }
}

if (interfaceList.Count > 0)
{
        constraintBuilder.SetInterfaceConstraints(interfaceList.ToArray());
}

Upvotes: 2

Related Questions