12

在我们的产品中,我们有称为“服务”的东西,它们是产品不同部分之间(尤其是语言之间——一种内部语言、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, ...);
}

但这相当冗长,并且在为大量服务执行此操作时并不那么容易管理,正如我打算做的那样。

看看如何externDllImportAttribute工作,我希望应该可以通过以下方式将其连接起来:

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

但我根本不知道如何实现这一点,似乎也找不到任何文档(extern似乎是一个定义相当模糊的问题)。我发现的最接近的是一个有点相关的问题,如何在 .NET 中为外部方法提供自定义实现?无论如何,这并没有真正回答我的问题,并且有所不同。C# 语言规范(特别是在 4.0 版中,第 10.6.7 节,外部方法)没有帮助。

所以,我想提供外部方法的自定义实现;这可以实现吗?如果是这样,怎么办?

4

3 回答 3

5

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 等 AOP 解决方案。

于 2012-12-06T14:57:46.233 回答
1

我最近需要做一些非常相似的事情(中继方法调用)。我最终生成了一种在运行时动态转发方法调用的类型。

对于您的用例,实现看起来像这样。首先创建一个描述您的服务的接口。您将在您想要在代码中调用服务的任何地方使用此接口。

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,它允许您在编译时生成代码。然而,这是一个付费的商业解决方案。

于 2012-12-06T13:59:58.193 回答
1

即使这不是您所要求的,我还是建议您创建自己的T4 模板来生成这些辅助方法。如果您有一些编程 API 来获取服务名称列表及其适用的参数类型,这将特别有用。

于 2012-12-06T15:20:17.547 回答