При реализации интерфейса, который имеет метод с параметром «in» от TypeBuilder.CreateType, исключение TypeLoadException - PullRequest
2 голосов
/ 12 июня 2019

Прежде чем начать, это мой первый вопрос о SO.Таким образом, могут быть какие-либо ошибки или недостаток информации о проблеме.Пожалуйста, дайте мне знать, если мне нужно что-то исправить.Спасибо.


Я создаю класс, который реализует интерфейс, содержащий метод с TypeBuilder.После реализации этого метода с ILGenerator, затем я вызываю TypeBuilder.CreateType(), и в обычном случае все идет хорошо.Но если метод содержит какой-либо параметр с модификатором in, также известным как ссылка только для чтения для типов значений, TypeBuilder.CreateType() выдает TypeLoadException("Method 'SomeMethod' ... does not have an implementation.").

В отличие от обычногоВ случае TypeLoadException, когда реализованный метод с такой же сигнатурой, объявленной в интерфейсе (интерфейсах), не существует, эта проблема возникает только тогда, когда метод содержит in параметров, даже если сигнатуры одинаковы.Когда я удаляю или изменяю модификатор in на ref или out, TypeBuilder.CreateType() успешно распознает сгенерированный метод как реализацию одного из объявленных в интерфейсе, и тип создается нормально.

Вотполностью компилируемый пример:

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 = namedPropArgs.Select(arg => (FieldInfo)arg.MemberInfo).ToArray();
                var namedFieldValues = namedPropArgs.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}");
        }
    }
}

Этот код также загружен в Pastebin .

Пока я копал эту проблему, я обнаружил, что параметр in имеет дванастраиваемые атрибуты InteropServices.InAttribute и CompilerServices.IsReadOnlyAttribute.Но когда я генерирую метод без реализации интерфейса (это обычно происходит успешно, так как не требуется сопоставление подписи) , in параметр сгенерированного метода имеет только один пользовательский атрибут, InAttribute.Поэтому я скопировал все пользовательские атрибуты параметров из интерфейса, но все еще вызывается TypeLoadException.

Я проверял это на .NET Framework 4.6.1 и .NET Core 2.2 с C# 7.2 and 7.3.И все среды дали мне одно и то же исключение.Я использую Visual Studio 2017 для Windows.

Есть что-то, что я пропустил, или какой-нибудь обходной путь?

Спасибо за любую помощь заранее.

1 Ответ

1 голос
/ 17 июня 2019

После написания вопроса выше, я исследовал встроенный двоичный код примера в IL и исходный код CoreCLR в течение нескольких дней, и теперь я нашел проблему и решение.

Короче говоря, обязательные и необязательные пользовательские модификаторы возвращаемого типа и каждого типа параметра принимают на себя часть сигнатуры метода, как и каждый тип, и ее пришлось реплицировать вручную. Я думал, что это будет сделано путем передачи ParameterAttributes.In в MethodBuilder.DefineParameter и репликации пользовательского атрибута InAttribute, но это было неправильно.

И среди модификаторов in, ref и out только in выдает требуемый пользовательский модификатор для указанного параметра. Напротив, ref и out представлены только самим их типом. По этой причине только in не сработал, как ожидалось.

Для репликации пользовательских модификаторов вызов TypeBuilder.DefineMethod необходимо изменить следующим образом:

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()  // *
    );

Помеченные строки с // * добавляются для репликации пользовательских модификаторов типов возврата / параметров.

Или мы можем сделать это, вызвав метод MethodBuilder.SetSignature после вызова DefineMethod без аргументов типа и пользовательских модификаторов. Если мы решили вызвать SetSignature отдельно, нам нужно вызвать его перед любым DefineParameter, SetCustomAttribute, Equals(Object), SetImplementationFlags, получателем свойства Signature и многими другими методами, которые вызывают внутренний метод MethodBuilder.GetMethodSignature() эти байты кэша, представляющие сигнатуру метода.

Спасибо, что прочитали и дали мне совет. :)

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...