参数化 DllImport 以在 C# 应用程序中使用

Posted

技术标签:

【中文标题】参数化 DllImport 以在 C# 应用程序中使用【英文标题】:Parameterising DllImport for use in a C# application 【发布时间】:2010-12-12 06:11:29 【问题描述】:

我们有一个供应商提供一个库来访问他们的硬件。不幸的是,如果您有多个设备,则需要使用不同的 dll 名称多次导入它们的库。因此,我们有大量重复的代码,我担心它很快会成为维护的噩梦。

我们目前拥有的东西是这样的:

namespace MyNamespace 
    public static class Device01 
        public const string DLL_NAME = @"Device01.dll";

        [DllImport(DLL_NAME, EntryPoint = "_function1")]
        public static extern int Function1(byte[] param);

...

        [DllImport(DLL_NAME, EntryPoint = "_function99")]
        public static extern int Function99(int param);
    

....

    public static class Device16 
        public const string DLL_NAME = @"Device16.dll";

        [DllImport(DLL_NAME, EntryPoint = "_function1")]
        public static extern int Function1(byte[] param);

...

        [DllImport(DLL_NAME, EntryPoint = "_function99")]
        public static extern int Function99(int param);
    

如果我使用 C 或 C++,我只会在一个文件中定义函数,并在静态类中多次#include 它们,虽然不漂亮但比替代方案更好,但在 C# 中我没有这个选项。

如果有人对如何有效地定义一个允许我们生成所需数量的静态设备类的工厂有任何聪明的想法,我会非常感兴趣。

谢谢,

编辑:函数原型变化很大,因此任何依赖它们相同的方法都不合适。感谢到目前为止的建议,我没有这么快就提出这么多想法。

【问题讨论】:

关于您最近的编辑:我的“第三种方法”可以调整为您喜欢的任何内容。为每个方法创建一个委托类型(无论如何您都需要它,并且您当前已经拥有它作为DllImports),为参数添加一个 param 数组并使用泛型使调用类型安全。稍等,我会尝试调整示例。 通过添加“委托反射”(发送给 Eric Lippert)和泛型,它可以解决您可能遇到的几乎任何类型的 P/Invoke 调用的问题 ;-) 【参考方案1】:

只是一些注意事项:

替代#one

编辑:这种方法需要更改编译方法,这很困难,并且需要注入、修改程序集或其他 AOP 领域常用的方法。考虑下面的方法二,这更容易。

    删除所有具有相同签名的函数,各保留一个 使用GetIlAsByteArray 为您的DllImport 方法创建一个动态方法 使用the technique described here来操作函数的IL,这里可以更改DllImport属性等 创建这些函数的委托并缓存您的调用 返回委托

备选方案#二:

编辑:这种替代方法起初似乎有点涉及,但有人已经为您完成了这项工作。查找this excellent CodeProject article 并简单地下载并使用它的代码来动态创建 DllImport 风格的方法。基本上,它归结为:

    删除所有 DllImport 创建您自己的 DllImport 包装器:采用 dll 名称和函数名称(假设所有签名都相同) 包装器使用 dllimport API 函数通过 LoadLibraryLoadLibraryEx 执行“手动” DllImport 包装器使用MethodBuilder 为您创建一个方法。 将委托返回给您可以用作函数的方法。

另类#three

编辑:进一步看,有一种更简单的方法:只需使用 DefinePInvokeMethod 即可满足您的所有需求。 MSDN 链接已经给出了一个很好的例子,但是this CodeProject article 提供了一个可以基于 DLL 和函数名称创建任何本机 DLL 的完整包装器。

    删除所有 DllImport 样式签名 围绕DefinePInvokeMethod 创建一个简单的包装方法 确保添加简单的缓存(字典?)以防止在每次调用时构建整个方法 从包装器返回一个委托。

这是这种方法在代码中的样子,您可以随意重用返回的委托,动态方法的昂贵构建应该每个方法只完成一次。

编辑:更新了代码示例以使用任何委托并自动反映委托签名中的正确返回类型和参数类型。这样,我们将实现与签名完全解耦,也就是说,鉴于您目前的情况,我们可以做到最好。优点:您具有类型安全性和单点更改,这意味着:非常易于管理。

// expand this list to contain all your variants
// this is basically all you need to adjust (!!!)
public delegate int Function01(byte[] b);
public delegate int Function02();
public delegate void Function03();
public delegate double Function04(int p, byte b, short s);

// TODO: add some typical error handling
public T CreateDynamicDllInvoke<T>(string functionName, string library)

    // create in-memory assembly, module and type
    AssemblyBuilder assemblyBuilder = AppDomain.CurrentDomain.DefineDynamicAssembly(
        new AssemblyName("DynamicDllInvoke"),
        AssemblyBuilderAccess.Run);

    ModuleBuilder modBuilder = assemblyBuilder.DefineDynamicModule("DynamicDllModule");

    // note: without TypeBuilder, you can create global functions
    // on the module level, but you cannot create delegates to them
    TypeBuilder typeBuilder = modBuilder.DefineType(
        "DynamicDllInvokeType",
        TypeAttributes.Public | TypeAttributes.UnicodeClass);

    // get params from delegate dynamically (!), trick from Eric Lippert
    MethodInfo delegateMI = typeof(T).GetMethod("Invoke");
    Type[] delegateParams = (from param in delegateMI.GetParameters()
                            select param.ParameterType).ToArray();

    // automatically create the correct signagure for PInvoke
    MethodBuilder methodBuilder = typeBuilder.DefinePInvokeMethod(
        functionName,
        library,
        MethodAttributes.Public |
        MethodAttributes.Static |
        MethodAttributes.PinvokeImpl,
        CallingConventions.Standard,
        delegateMI.ReturnType,        /* the return type */
        delegateParams,               /* array of parameters from delegate T */
        CallingConvention.Winapi,
        CharSet.Ansi);

    // needed according to MSDN
    methodBuilder.SetImplementationFlags(
        methodBuilder.GetMethodImplementationFlags() |
        MethodImplAttributes.PreserveSig);

    Type dynamicType = typeBuilder.CreateType();

    MethodInfo methodInfo = dynamicType.GetMethod(functionName);

    // create the delegate of type T, double casting is necessary
    return (T) (object) Delegate.CreateDelegate(
        typeof(T),
        methodInfo, true);



// call it as follows, simply use the appropriate delegate and the
// the rest "just works":
Function02 getTickCount = CreateDynamicDllInvoke<Function02>
    ("GetTickCount", "kernel32.dll");

Debug.WriteLine(getTickCount());

我猜其他方法也是可能的(就像这个线程中其他人提到的模板方法)。

更新:添加了指向excellent codeproject article的链接。更新:添加了第三种更简单的方法。更新: 添加了代码示例更新: 更新了代码示例以与任何函数原型无缝协作更新: 修复了严重错误:typeof(Function02) 应为 typeof(T)当然

【讨论】:

你不能在运行时修改属性,如果你让它们可变的话你可以,但这意味着未定义的行为。 实际上,你可以,但前提是你使用MethodBuilder 类在运行时创建它们,但你是对的,你不能为编译的方法更改它们(除非你使用其中一种优秀的注入/AOP 库)。【参考方案2】:

如何使用T4(文本模板转换工具包)。创建一个包含以下内容的 .tt 文件:

<#@ template language="C#" #>
using System.Runtime.InteropServices;
namespace MyNamespace 
    <# foreach(string deviceName in DeviceNames)  #>
    public static class <#= deviceName #>
    
        public const string DLL_NAME = @"<#= deviceName #>.dll";
        <# foreach(string functionName in FunctionNames)  #>
        [DllImport(DLL_NAME, EntryPoint = "<#= functionName #>")]
        public static extern int <#= functionName.Substring(1) #>(byte[] param);
        <#  #>        
    
    <#  #>

<#+
string[] DeviceNames = new string[]  "Device01", "Device02", "Device03" ;
string[] FunctionNames = new string[]  "_function1", "_function2", "_function3" ;
#>

Visual Studio 会将其转换为:

using System.Runtime.InteropServices;
namespace MyNamespace 

    public static class Device01
    
        public const string DLL_NAME = @"Device01.dll";

        [DllImport(DLL_NAME, EntryPoint = "_function1")]
        public static extern int function1(byte[] param);
        [DllImport(DLL_NAME, EntryPoint = "_function2")]
        public static extern int function2(byte[] param);
        [DllImport(DLL_NAME, EntryPoint = "_function3")]
        public static extern int function3(byte[] param);

    

    public static class Device02
    
        public const string DLL_NAME = @"Device02.dll";

        [DllImport(DLL_NAME, EntryPoint = "_function1")]
        public static extern int function1(byte[] param);
        [DllImport(DLL_NAME, EntryPoint = "_function2")]
        public static extern int function2(byte[] param);
        [DllImport(DLL_NAME, EntryPoint = "_function3")]
        public static extern int function3(byte[] param);

    

    public static class Device03
    
        public const string DLL_NAME = @"Device03.dll";

        [DllImport(DLL_NAME, EntryPoint = "_function1")]
        public static extern int function1(byte[] param);
        [DllImport(DLL_NAME, EntryPoint = "_function2")]
        public static extern int function2(byte[] param);
        [DllImport(DLL_NAME, EntryPoint = "_function3")]
        public static extern int function3(byte[] param);

    

【讨论】:

看其他方法所涉及的复杂度,我认为这确实是解决这个问题的最佳方案。【参考方案3】:

我还建议使用原生的LoadLibraryGetProcAddress

对于后者,您只需使用与 pinvoke 方法签名匹配的委托类型调用 Marshal.GetDelegateForFunctionPointer

【讨论】:

+1 for GetDelegateForFunctionPointer,当我查看第一个解决方案时找不到它。很难让这三个 api 调用处理原型的可变参数和返回类型并仍然保持类型安全。 @abel: 不是真的“太”难:)

以上是关于参数化 DllImport 以在 C# 应用程序中使用的主要内容,如果未能解决你的问题,请参考以下文章

如何在 C# 中将 DLLImport 与结构一起用作参数?

调用以 std::list 作为参数的 C++ 函数

如何避免从 C# 构建的 Sql Server 2005 参数化查询变慢

c#使用DllImport调用c++dll的函数

通过 DllImport 在 C# 中调用 C 方法 - 尝试读取或写入受保护的内存

C# DllImport“调用导致堆栈不对称。原因可能是托管的 PInvoke 签名与非托管的目标签名不匹配。请检查 PInvoke 签名的调用约定和参数与非托管的目标签名是否匹配 ”