如何实现自己的 extern 类型?

12
在我们的产品中,我们有所谓的“服务”(services),它们是产品中不同部分之间(尤其是内部语言、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中为extern方法提供自定义实现? 它没有真正回答我的问题,而且有些不同。C#语言规范(特别是版本4.0的10.6.7节 External methods)也没有帮助。

因此,我想提供外部方法的自定义实现;这可以实现吗?如果可以,怎么做?


最常见的方法是使用接口和远程代理。 - leppie
请参见 http://stackoverflow.com/questions/7245507/how-to-provide-custom-implementation-for-extern-methods-in-net。 - Paul Zahra
@PaulZahra:我看到了那个问题 - 我在我的问题中提到了它。 - Chris Morgan
3个回答

5
C# 中的 extern 关键字并没有太多作用,它只是告诉编译器方法声明不会有主体。编译器进行最少的检查,它要求你提供一个属性,任何属性都可以。因此,这个示例代码将编译得很好:
   class Program {
        static void Main(string[] args) {
            foo();
        }

        class FooBar : Attribute { }

        [FooBar]
        static extern void foo();
    }

当然它是不能运行的,声明时Jitter(JIT编译器)会放弃。实际上运行这段代码需要Jitter生成适当的可执行代码,因此Jitter需要识别该属性。
你可以在Jitter源代码中看到这个处理过程,位于SSCLI20 distribution, clr/src/md/compiler/custattr.cpp文件中的RegMeta::_HandleKnownCustomAttribute()函数。这段代码是针对.NET 2.0准确的,我不知道有没有增加对方法调用的影响。你将看到它处理与方法调用的代码生成相关的以下属性,这些属性将使用extern关键字:
  • [DllImport],你毫无疑问知道它

  • [MethodImpl(MethodImplOptions.InternalCall)],一个用于在CLR中实现而不是在框架中使用的方法的属性。它们是用C++编写的,CLR有一个内部表格与C++函数链接。典型的例子是Math.Pow()方法,我在this answer中描述了实现细节。该表格在CLR源代码中是硬编码的,不能扩展。

  • [ComImport],标记接口在其他地方实现的属性,通常是在COM服务器中实现。你很少直接编程此属性,而是使用Tlbimp.exe生成的interop库。此属性还需要[Guid]属性来给出接口所需的guid。这与[DllImport]属性类似,它生成一种pinvoke调用到非托管代码,但使用COM调用约定。当然,只有在您的机器上实际拥有所需的COM服务器时,它才能正常工作,否则它是无限可扩展的。

此函数还识别了许多其他属性,但它们与定义在其他地方的调用代码无关。

所以,除非你自己编写jitter,否则使用extern不是实现你想要的方式。如果你仍想追求这个目标,可以考虑Mono项目。
常见的纯托管的可扩展性解决方案包括被大多数人遗忘的System.AddIn名称空间、非常受欢迎的MEF框架和像Postsharp这样的AOP解决方案。

感谢您的解释和确认,它不会做我想要的事情。 - Chris Morgan
我已确认您可以创建一个没有任何属性的 extern 方法,该类将成功编译;但在运行时,它会在类加载时抛出异常。(“无法从程序集 <assembly> 中加载类型<type> ,因为方法<method> 没有实现(没有 RVA)。)(我猜您可以创建一些后构建进程来打开 DLL,修改程序集,然后重新保存它。) - Mike Rosoft

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


嗯,那个技巧可以行得通,但如果它正在被执行,最好在其他代码中在编译时完成。 (我们的构建系统非常能够执行这样的操作,例如从Python脚本生成C#代码,然后进行编译,这将减少运行时性能损失。)谢谢! - Chris Morgan

1

虽然这不完全是您要求的,但我建议创建自己的T4模板,它将生成这些辅助方法。如果您有一些编程API来获取服务名称列表及其适用的参数类型,则尤其有用。


服务实际上是在运行时注册的,无法查询可接受的参数——每个服务只需获取一系列参数并可以随意使用它们(通常调用“verify”函数,指定所需的类型,但不总是这样)。但我预计最终可能会采用代码生成。谢谢! - Chris Morgan

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接