【问题标题】:How can I implement my own type of extern?如何实现我自己的外部类型?
【发布时间】:2012-11-24 01:00:03
【问题描述】:

在我们的产品中,我们有一些称为“服务”的东西,它们是产品不同部分之间(尤其是语言之间——一种内部语言、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 似乎是一个定义相当模糊的问题)。我发现的最接近的是一个有点相关的问题How to provide custom implementation for extern methods in .NET?,它并没有真正回答我的问题,而且无论如何都有些不同。 C# 语言规范(尤其是在 4.0 版,第 10.6.7 节,外部方法)没有帮助。

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

【问题讨论】:

标签: c# .net-3.5 refactoring extern automated-refactoring


【解决方案1】:

C# extern 关键字的作用很小,它只是告诉编译器方法声明没有主体。编译器会进行最低限度的检查,它坚持你也提供一个属性,任何事情都会发生。所以这个示例代码可以编译得很好:

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

        class FooBar : Attribute { }

        [FooBar]
        static extern void foo();
    }

但它当然不会运行,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 生成的互操作库。此属性还需要 [Guid] 属性来提供接口所需的 guid。这在其他方面类似于 [DllImport] 属性,它生成一种对非托管代码的 pinvoke 调用,但使用 COM 调用约定。当然,这只有在您的机器上确实有所需的 COM 服务器时才能正常工作,否则它可以无限扩展。

在此函数中识别出更多属性,但它们与调用在别处定义的代码无关。

因此,除非您编写自己的 jitter,否则使用 extern 不是获得所需内容的可行方法。如果你想继续这个,你可以考虑 Mono 项目。

纯托管的常见可扩展性解决方案是已被广泛遗忘的 System.AddIn 命名空间、非常流行的 MEF 框架和 Postsharp 等 AOP 解决方案。

【讨论】:

  • 感谢您的解释和确认它不会做我想要的。
  • 我已经确认你可以创建一个不带任何属性的extern方法,并且类会编译成功;但在运行时它会抛出类负载。 (“无法从程序集 加载类型 ,因为方法 没有实现(无 RVA)。”)(我想您可以创建一些构建后进程来打开 DLL,修改程序集,然后重新保存。)
【解决方案2】:

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

对于您的用例,实现看起来像这样。首先创建一个描述您的服务的接口。无论您想在代码中调用什么服务,都可以使用此接口。

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# 代码然后编译它,这将减少运行时性能命中。)谢谢!
【解决方案3】:

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

【讨论】:

  • 服务实际上是在运行时注册的,并且不能查询可接受的参数——每个都只需要一个参数列表,并且可以对它们做自己喜欢的事情(通常调用“验证”函数,指定类型它想要,但并非总是如此)。但我希望我最终可能会进行代码生成。谢谢!
猜你喜欢
  • 2021-12-27
  • 2017-09-19
  • 1970-01-01
  • 2017-09-08
  • 2014-01-27
  • 1970-01-01
  • 1970-01-01
  • 2010-12-15
  • 1970-01-01
相关资源
最近更新 更多