Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

When implementing an interface that has a method with 'in' parameter by TypeBuilder.CreateType, TypeLoadException is thrown

Tags:

Before beginning, this is my first question on SO. So there might be faults or lack of information about the problem. Please let me know if there's something that I need to correct. Thanks.


Using TypeBuilder, I'm building a class that implements an interface that contains a method. After implementing that method with ILGenerator, then I call TypeBuilder.CreateType() and everything goes well in the normal case. But if the method contains any parameter with the in modifier, also known as readonly reference for value types, TypeBuilder.CreateType() throws TypeLoadException("Method 'SomeMethod' ... does not have an implementation.").

Unlike the usual case of TypeLoadException that implemented method with the same signature as the one declared in the interface(s) doesn't exist, this problem is raised only when the method contains in parameter(s) even signatures are the same. When I remove or change the in modifier to ref or out, TypeBuilder.CreateType() successfully recognizes the generated method as an implementation of one declared in the interface, and the type is built normally.

Here's a fully compilable example:

using System;
using System.Collections.Generic;
using System.Globalization;
using System.Linq;
using System.Reflection;
using System.Reflection.Emit;
using System.Threading;
 
namespace EmitMethodWithInParamTest
{
    public struct StructParam
    {
        public String Data;
    }
 
    public interface ISomeInterface
    {
        Int32 SomeMethod(in StructParam param);
    }
 
    static class EmitExtension
    {
        public static void ReplicateCustomAttributes(this ParameterBuilder paramBuilder, ParameterInfo paramInfo)
        {
            foreach (var attrData in paramInfo.GetCustomAttributesData())
            {
                var ctorArgs = attrData.ConstructorArguments.Select(arg => arg.Value).ToArray();
 
                // Handling variable arguments
                var ctorParamInfos = attrData.Constructor.GetParameters();
                if (ctorParamInfos.Length > 0 &&
                    ctorParamInfos.Last().IsDefined(typeof(ParamArrayAttribute)) &&
                    ctorArgs.Last() is IReadOnlyCollection<CustomAttributeTypedArgument> variableArgs)
                {
                    ctorArgs[ctorArgs.Length - 1] = variableArgs.Select(arg => arg.Value).ToArray();
                }
 
                var namedPropArgs = attrData.NamedArguments.Where(arg => !arg.IsField);
                var namedPropInfos = namedPropArgs.Select(arg => (PropertyInfo)arg.MemberInfo).ToArray();
                var namedPropValues = namedPropArgs.Select(arg => arg.TypedValue.Value).ToArray();
 
                var namedFieldArgs = attrData.NamedArguments.Where(arg => arg.IsField);
                var namedFieldInfos = namedFieldArgs.Select(arg => (FieldInfo)arg.MemberInfo).ToArray();
                var namedFieldValues = namedFieldArgs.Select(arg => arg.TypedValue.Value).ToArray();
 
                var attrBuilder = new CustomAttributeBuilder(attrData.Constructor,
                    ctorArgs, namedPropInfos, namedPropValues, namedFieldInfos, namedFieldValues);
                paramBuilder.SetCustomAttribute(attrBuilder);
            }
        }
    }
 
    class Program
    {
        static Program()
        {
            Thread.CurrentThread.CurrentUICulture = CultureInfo.GetCultureInfo("en-us");
        }
 
        static void Main(String[] args)
        {
            var assemblyBuilder = AssemblyBuilder.DefineDynamicAssembly(new AssemblyName("DynamicAssembly"), AssemblyBuilderAccess.Run);
            var moduleBuilder = assemblyBuilder.DefineDynamicModule("MainModule");
            var typeBuilder = moduleBuilder.DefineType("SomeClass",
                TypeAttributes.Public | TypeAttributes.Class | TypeAttributes.AutoClass | TypeAttributes.AnsiClass | TypeAttributes.BeforeFieldInit | TypeAttributes.AutoLayout,
                null /*base class*/,
                new[] { typeof(ISomeInterface) });
 
            var methodInfoToImpl = typeof(ISomeInterface).GetMethod(nameof(ISomeInterface.SomeMethod));
            var paramInfos = methodInfoToImpl.GetParameters();
 
            var methodBuilder = typeBuilder.DefineMethod(methodInfoToImpl.Name,
                MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final,
                CallingConventions.HasThis,
                methodInfoToImpl.ReturnType,
                paramInfos.Select(pi => pi.ParameterType).ToArray());
 
            foreach (var paramInfo in paramInfos)
            {
                // paramInfo.Position is zero-based but DefineParameter requires 1-based index.
                var paramBuilder = methodBuilder.DefineParameter(paramInfo.Position + 1, paramInfo.Attributes, paramInfo.Name);
                if (paramInfo.Attributes.HasFlag(ParameterAttributes.HasDefault))
                {
                    paramBuilder.SetConstant(paramInfo.DefaultValue);
                }
                paramBuilder.ReplicateCustomAttributes(paramInfo);
            }
 
            // Dummy implementation for example. Always throws NotImplementedException.
            var ilGen = methodBuilder.GetILGenerator();
            ilGen.Emit(OpCodes.Newobj, typeof(NotImplementedException).GetConstructor(Type.EmptyTypes));
            ilGen.Emit(OpCodes.Throw);
 
            var builtType = typeBuilder.CreateType();               // <- TypeLoadException("Method 'SomeMethod' in type 'SomeClass' from assembly 'DynamicAssembly, ...' does not have an implementation.") is thrown.
            var generatedObj = (ISomeInterface)Activator.CreateInstance(builtType);
 
            var someParam = new StructParam() { Data = "SomeData" };
            var result = generatedObj.SomeMethod(in someParam);     // <- NotImplementedException expected by dummy implementation if executed.
 
            Console.WriteLine($"Result: {result}");
        }
    }
}

This code is also uploaded to Pastebin.

While digging down this problem, I found that the in parameter has two custom attributes, InteropServices.InAttribute and CompilerServices.IsReadOnlyAttribute. But when I generate a method without implementing the interface (this succeeds normally because no signature matching required), in parameter of generated method has only one custom attribute, InAttribute. So I replicated all custom attributes of parameters from the interface, but still TypeLoadException is being raised.

I've tested this on .NET Framework 4.6.1 and .NET Core 2.2 with C# 7.2 and 7.3. And all environments gave me the same exception. I'm using Visual Studio 2017 on Windows.

Is there anything that I have missed or are there any workarounds?

Thank you for any help in advance.

like image 253
Codeption Avatar asked Jun 12 '19 14:06

Codeption


1 Answers

After writing the question above, I've been investigated built binary of sample code in IL and source code of CoreCLR for a few days, and now I found the problem and solution.

In short, required and optional custom modifiers of return type and each parameter type take a part of method signature like each types do, and it had to be replicated manually. I thought that it will be done by passing ParameterAttributes.In to MethodBuilder.DefineParameter and replicating the custom attribute InAttribute, but it was wrong.

And, among in, ref and out modifiers, only in emits a required custom modifier to specified parameter. In contrast, ref and out are represented only with their type itself. This is the reason why only in didn't work as expected.

To replicate custom modifiers, call to TypeBuilder.DefineMethod need be modified like this:

var methodBuilder = typeBuilder.DefineMethod(methodInfoToImpl.Name,
    MethodAttributes.Public | MethodAttributes.Virtual | MethodAttributes.Final,
    CallingConventions.HasThis,
    methodInfoToImpl.ReturnType,
    methodInfoToImpl.ReturnParameter.GetRequiredCustomModifiers(),      // *
    methodInfoToImpl.ReturnParameter.GetOptionalCustomModifiers(),      // *
    paramInfos.Select(pi => pi.ParameterType).ToArray(),
    paramInfos.Select(pi => pi.GetRequiredCustomModifiers()).ToArray(), // *
    paramInfos.Select(pi => pi.GetOptionalCustomModifiers()).ToArray()  // *
    );

Marked lines with // * are newly added to replicate custom modifiers of return/parameter types.

Or, we can do this by calling MethodBuilder.SetSignature method after calling DefineMethod without any type and custom modifiers arguments. If we decided to call SetSignature separately, we need to call it before any DefineParameter, SetCustomAttribute, Equals(Object), SetImplementationFlags, getter of property Signature and many other methods that call the internal method MethodBuilder.GetMethodSignature() that cache bytes representing method signature.

Thank you for reading and giving me advice. :)

like image 154
Codeption Avatar answered Sep 21 '22 22:09

Codeption