Как я могу реализовать свой собственный тип extern?

В нашем продукте есть вещи, называемые «сервисами», которые являются основными средствами связи между различными частями продукта (и особенно между языками — внутренним языком C, Python и .NET).

В настоящее время код выглядит так (Services.Execute с использованием params object[] args):

myString = (string)Services.Execute("service_name", arg1, arg2, ...);

Я бы предпочел иметь возможность писать такой код и пользоваться преимуществами проверки типов и менее подробным кодом:

myString = ServiceName(arg1, arg2, ...);

Это может быть достигнуто с помощью простой функции,

public static string ServiceName(int arg1, Entity arg2, ...)
{
    return (string)Services.Execute("service_name", arg1, arg2, ...);
}

Но это довольно многословно, и не так просто справиться с этим для множества сервисов, как я собираюсь сделать.

Глядя на то, как работают extern и DllImportAttribute, я надеюсь, что можно будет подключить это каким-то образом:

[ServiceImport("service_name")]
public static extern string ServiceName(int arg1, Entity arg2, ...);

Но я вообще не знаю, как этого добиться, и не могу найти для этого никакой документации (extern кажется довольно расплывчато определенным вопросом). Самый близкий, который я нашел, - это несколько связанный вопрос, Как предоставить пользовательскую реализацию для внешних методов в .NET? что на самом деле не ответило на мой вопрос и в любом случае несколько отличается. Спецификация языка C# (особенно в версии 4.0, раздел 10.6.7, Внешние методы) не помогает.

Итак, я хочу предоставить собственную реализацию внешних методов; можно ли этого добиться? И если да, то как?


person Chris Morgan    schedule 06.12.2012    source источник
comment
Чаще всего это делается с помощью интерфейсов и удаленных прокси-серверов.   -  person leppie    schedule 06.12.2012
comment
См. stackoverflow.com/questions/7245507/   -  person Paul Zahra    schedule 06.12.2012
comment
@PaulZahra: я видел этот вопрос - я упомянул его в своем вопросе   -  person Chris Morgan    schedule 07.12.2012


Ответы (3)


Ключевое слово C# extern делает очень мало, оно просто сообщает компилятору, что объявление метода не будет иметь тела. Компилятор делает минимальную проверку, он настаивает на том, чтобы вы также предоставили атрибут, все идет. Таким образом, этот пример кода отлично скомпилируется:

   class Program {
        static void Main(string[] args) {
            foo();
        }

        class FooBar : Attribute { }

        [FooBar]
        static extern void foo();
    }

Но конечно не запустится, джиттер разводит руками на декларацию. Это то, что требуется для фактического запуска этого кода, это работа jitter для создания правильного исполняемого кода для этого. Что требуется, так это то, что дрожание распознает атрибут.

Вы можете увидеть это в исходном коде джиттера в Дистрибутив SSCLI20, файл исходного кода clr/src/md/compiler/custattr.cpp, функция RegMeta::_HandleKnownCustomAttribute(). Это код, который подходит для .NET 2.0, я не знаю о дополнениях к нему, влияющих на вызов метода. Вы увидите, что он обрабатывает следующие атрибуты, относящиеся к генерации кода для вызовов методов, которые будут использовать ключевое слово extern:

  • [DllImport], ты, без сомнения, это знаешь

  • [MethodImpl(MethodImplOptions.InternalCall)], атрибут, который используется в методах, реализованных в среде CLR, а не в платформе. Они написаны на C++, CLR имеет внутреннюю таблицу, которая ссылается на функцию C++. Каноническим примером является метод Math.Pow(), подробности реализации которого я описал в этом ответе. В противном случае таблица не расширяема, она встроена в исходный код CLR.

  • [ComImport] — атрибут, помечающий интерфейс как реализованный в другом месте, обязательно на COM-сервере. Вы редко программируете этот атрибут напрямую, вместо этого вы будете использовать библиотеку взаимодействия, созданную Tlbimp.exe. Этот атрибут также требует, чтобы атрибут [Guid] задавал требуемый guid интерфейса. В остальном он похож на атрибут [DllImport], он генерирует вызов типа pinvoke для неуправляемого кода, но с использованием соглашений о вызовах COM. Конечно, это может работать должным образом только в том случае, если у вас действительно есть требуемый COM-сервер на вашей машине, в противном случае он бесконечно расширяем.

В этой функции распознается гораздо больше атрибутов, но они никак не связаны с вызывающим кодом, который определен в другом месте.

Так что, если вы не пишете свой собственный джиттер, использование extern не является жизнеспособным способом получить то, что вы хотите. Вы могли бы рассмотреть проект Mono, если вы все равно хотите этим заниматься.

Распространенными решениями по расширению, которые полностью управляемы, являются почти забытое пространство имен System.AddIn, очень популярная платформа MEF и решения АОП, такие как Postsharp.

person Hans Passant    schedule 06.12.2012
comment
Спасибо за объяснение и подтверждение того, что это не будет делать то, что я хочу. - person Chris Morgan; 07.12.2012

Недавно мне нужно было сделать что-то очень похожее (ретрансляция вызовов методов). В итоге я создал тип, который динамически перенаправлял вызовы методов во время выполнения.

Для вашего варианта использования реализация будет выглядеть примерно так. Сначала создайте интерфейс, описывающий ваш сервис. Вы будете использовать этот интерфейс везде, где хотите вызвать службу в своем коде.

public interface IMyService
{
    [ServiceImport("service_name")]
    string ServiceName(int arg1, string arg2);
}

Затем запустите код для создания класса, динамически реализующего этот интерфейс.

// Get handle to the method that is going to be called.
MethodInfo executeMethod = typeof(Services).GetMethod("Execute");

// Create assembly, module and a type (class) in it.
AssemblyName assemblyName = new AssemblyName("MyAssembly");
AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(assemblyName, AssemblyBuilderAccess.Run, (IEnumerable<CustomAttributeBuilder>)null);
ModuleBuilder moduleBuilder = assemblyBuilder.DefineDynamicModule("MyModule");
TypeBuilder typeBuilder = moduleBuilder.DefineType("MyClass", TypeAttributes.Class | TypeAttributes.Public, typeof(object), new Type[] { typeof(IMyService) });
typeBuilder.DefineDefaultConstructor(MethodAttributes.Public);

// Implement each interface method.
foreach (MethodInfo method in typeof(IMyService).GetMethods())
{
    ServiceImportAttribute attr = method
        .GetCustomAttributes(typeof(ServiceImportAttribute), false)
        .Cast<ServiceImportAttribute>()
        .SingleOrDefault();

    var parameters = method.GetParameters();

    if (attr == null)
    {
        throw new ArgumentException(string.Format("Method {0} on interface IMyService does not define ServiceImport attribute."));
    }
    else
    {
        // There is ServiceImport attribute defined on the method.
        // Implement the method.
        MethodBuilder methodBuilder = typeBuilder.DefineMethod(
            method.Name,
            MethodAttributes.Public | MethodAttributes.Virtual,
            CallingConventions.HasThis,
            method.ReturnType,
            parameters.Select(p => p.ParameterType).ToArray());

        // Generate the method body.
        ILGenerator methodGenerator = methodBuilder.GetILGenerator();

        LocalBuilder paramsLocal = methodGenerator.DeclareLocal(typeof(object[])); // Create the local variable for the params array.
        methodGenerator.Emit(OpCodes.Ldc_I4, parameters.Length); // Amount of elements in the params array.
        methodGenerator.Emit(OpCodes.Newarr, typeof(object)); // Create the new array.
        methodGenerator.Emit(OpCodes.Stloc, paramsLocal); // Store the array in the local variable.

        // Copy method parameters to the params array.
        for (int i = 0; i < parameters.Length; i++)
        {
            methodGenerator.Emit(OpCodes.Ldloc, paramsLocal); // Load the params local variable.
            methodGenerator.Emit(OpCodes.Ldc_I4, i); // Value will be saved in the index i.
            methodGenerator.Emit(OpCodes.Ldarg, (short)(i + 1)); // Load value of the (i + 1) parameter. Note that parameter with index 0 is skipped, because it is "this".
            if (parameters[i].ParameterType.IsValueType)
            {
                methodGenerator.Emit(OpCodes.Box, parameters[i].ParameterType); // If the parameter is of value type, it needs to be boxed, otherwise it cannot be put into object[] array.
            }

            methodGenerator.Emit(OpCodes.Stelem, typeof(object)); // Set element in the array.
        }

        // Call the method.
        methodGenerator.Emit(OpCodes.Ldstr, attr.Name); // Load name of the service to execute.
        methodGenerator.Emit(OpCodes.Ldloc, paramsLocal); // Load the params array.
        methodGenerator.Emit(OpCodes.Call, executeMethod); // Invoke the "Execute" method.
        methodGenerator.Emit(OpCodes.Ret); // Return the returned value.
    }
}

Type generatedType = typeBuilder.CreateType();

// Create an instance of the type and test it.
IMyService service = (IMyService)generatedType.GetConstructor(new Type[] { }).Invoke(new object[] { });
service.ServiceName(1, "aaa");

Это решение, вероятно, немного запутано, но если вы хотите избавить себя от необходимости создавать код самостоятельно, оно работает достаточно хорошо. Обратите внимание, что создание динамического типа снижает производительность. Однако это обычно делается во время инициализации и не должно сильно влиять на время выполнения.

В качестве альтернативы я советую вам взглянуть на PostSharp, который позволяет генерировать код во время компиляции. Однако это платное коммерческое решение.

person Omikr0n    schedule 06.12.2012
comment
Хм. Этот метод будет работать, но если это делается, это может быть сделано во время компиляции в другом коде. (Наша система сборки вполне способна делать такие вещи, как генерация кода C# из скрипта Python и его последующая компиляция, что уменьшит производительность во время выполнения.) Спасибо! - person Chris Morgan; 07.12.2012

Хотя это не совсем то, о чем вы просили, я бы рекомендовал создать собственный шаблон T4, который будет генерировать эти вспомогательные методы. Это особенно полезно, если у вас есть программный API для получения списка имен служб и применимых типов параметров.

person Earlz    schedule 06.12.2012
comment
Службы фактически регистрируются во время выполнения, и допустимые аргументы не могут быть запрошены — каждая просто берет список аргументов и может делать с ними все, что хочет (обычно вызывает функцию проверки, указывая нужные типы, но не всегда). Но я ожидаю, что, вероятно, закончу генерацией кода. Спасибо! - person Chris Morgan; 07.12.2012