P/Invoke平台调用技术

时间:2022-08-28 08:00:20

C#里调用非托管的Dll

    今天花了一些精力来调查了一下C#里调用非托管的Dll,C#里调用非托管的Dll要使用P/Invoke平台调用技术, 这里先简单介绍一下P/Invoke平台调用技术。
    由于开发程序转到托管代码,所以开发过程中会经常研究底层的一些关键功能,通过 P/Invoke(平台调用)即 公共语言运行库 (CLR) 的 interop 功能,
来进行底层或者其他平台dll的调用。

C#语言声明外部方法,基本形式是:
[DLLImport(“DLL文件”,……)]
修饰符 extern 返回变量类型 方法名称 (参数列表)

DLL文件:包含定义外部方法的库文件。

修饰符: 访问修饰符,除了abstract以外在声明方法时可以使用的修饰符。

extern:extern 修饰符用于声明在外部实现的方法,常见用法是在使用 Interop 服务调入非托管代码时与 DllImport 属性一起使用

返回变量类型:在DLL文件中你需调用方法的返回变量类型。

方法名称:在DLL文件中你需调用方法的名称。

参数列表:在DLL文件中你需调用方法的列表。

C# 的规则之一是
它的调用语法只能访问 CLR 数据类型,例如 System.UInt32 和 System.Boolean。
C# 显然不识别 Windows API 中使用的基于 C 的数据类型(例如 UINT 和 BOOL),这些类型只是 C 语言类型的类型定义而已。所以当 Windows API 函数如果按以下方式编写时
BOOL MessageBeep( UINT uType )
外部方法就必须使用 CLR 类型来定义,如前面的代码片段中所看到的。
需要使用与基础 API 函数类型不同但与之兼容的 CLR 类型是 P/Invoke 较难使用的一个方面。(数据封送处理)

可选的 DllImportAttribute 属性
除了指出宿主 DLL 外,DllImportAttribute 还包含了一些可选属性,
包括:EntryPointCharSetSetLastError 和 CallingConvention

EntryPoint 
在不希望外部托管方法具有与 DLL 导出相同的名称的情况下,可以设置该属性来指示导出的 DLL 函数的入口点名称。
当定义两个调用相同非托管函数的外部方法时,这特别有用。
另外,在 Windows 中还可以通过它们的序号值绑定到导出的 DLL 函数。
如果需要这样做,则诸如“#1”或“#129”的 EntryPoint 值指示 DLL 中非托管函数的序号值而不是函数名,
( 通常这个很少有相同名称)

ExactSpelling

指示 EntryPoint 是否必须与指示的入口点的拼写完全匹配,如:ExactSpelling=false;

CharSet

指示用在入口点中的字符集,如:CharSet=CharSet.Ansi;
如果没有显式地设置 CharSet 属性,则其默认值为 CharSet.Ansi。这个默认值是有缺点的,
因为对于在 Windows 2000、Windows XP 和 Windows NT 上进行的 interop 调用,它会消极地影响文本参数封送处理的性能。
如果CharSet 属性设置为 CharSet.Auto。这样可以使 CLR 根据宿主 OS 使用适当的字符集。
这个同时根据操作系统具体设置 例如:基于 Windows NT 的操作系统中,并且只支持 Unicode ,所以我们就要设置为 CharSet.Unicode。

SetLastError

指示方法是否保留 Win32"上一错误",如:SetLastError=true;
如果使用 GetLastError 来查找扩展的错误信息,
则应该在外部方法的 DllImportAttribute 中将 SetLastError 属性设置为 true
这将导致 CLR 在每次调用外部方法之后缓存由 API 函数设置的错误
在包装方法中,
可以通过调用类库的 System.Runtime.InteropServices.Marshal 类型中定义的 Marshal.GetLastWin32Error 方法来获取缓存的错误值

PreserveSig

指示方法的签名应当被保留还是被转换, 如:PreserveSig=true;

CallingConvention

指示入口点的调用约定, 如:CallingConvention=CallingConvention.Winapi;
通过此属性,可以给 CLR 指示应该将哪种函数调用约定用于堆栈中的参数。
CallingConvention.Winapi 的默认值是最好的选择,它在大多数情况下都可行
在 c,c++运行时 DLL 函数和少数函数中,可能需要将约定更改为 CallingConvention.Cdecl。

“数据封送处理”及“封送数字和逻辑标量”

这个我在学习当中,如果有兴趣的话,可以更加深入。

下面举了一个简单的 P/Invoke 示例

【c# 调用 c++  .dll 】
首先需要添加using System.Runtime.InteropServices; //交互服务的命名空间
静态加载.dll
   1. 首先把被加载.dll拷贝到运行目标bin/debug目录下(如果路径要求不被限制,就可以选择动态加载)
   2.  声明调用方法名
   [DllImport("xxxx.dll", EntryPoint = "xxx方法名xxx", ExactSpelling = false, CallingConvention = CallingConvention.Cdecl)]
   public static extern int OperatePlus(int a, int b); 
   3.执行调用
       static void Main(string[] args)
       {
           Console.WriteLine(OperatePlus(100, 102));
           Console.Read();
       }
   4.静态调用成功
动态加载.dll
    1. 
        声明调用kernel32.dll中调用方法
        /// <summary>
        /// 装载动态库
        /// </summary>
        /// <param name="lpLibFileName">DLL 文件名</param>
        /// <returns>函数库模块的句柄 </returns>
        [DllImport("kernel32.dll", EntryPoint = "LoadLibrary")]
        public static extern int LoadLibrary(
            [MarshalAs(UnmanagedType.LPStr)] string lpLibFileName);

        /// <summary>
        /// 获取要引入的函数,将符号名或标识号转换为DLL内部地址。
        /// </summary>
        /// <param name="hModule">包含需调用函数的函数库模块的句柄</param>
        /// <param name="lpProcName">调用函数的名称</param>
        /// <returns>函数指针</returns>
        [DllImport("kernel32.dll", EntryPoint = "GetProcAddress")]
        public static extern IntPtr GetProcAddress(int hModule,
            [MarshalAs(UnmanagedType.LPStr)] string lpProcName);

        /// <summary>
        /// 释放动态链接库。
        /// </summary>
        /// <param name="hModule">需释放的函数库模块的句柄</param>
        /// <returns>是否已释放指定的 Dll</returns>
        [DllImport("kernel32.dll", EntryPoint = "FreeLibrary")]
        public static extern bool FreeLibrary(int hModule);
      
    2. 声明委托        

    /// <summary>
        ///函数指针封装成委托
        /// </summary>
        /// <param name="a"></param>
        /// <param name="b"></param>
        /// <returns></returns>
        [UnmanagedFunctionPointerAttribute(CallingConvention.Cdecl)] //控制作为非托管函数指针传入或传出非托管代码的委托签名的封送行为
        delegate int OperatePlus(int a, int b);
        
   3. 调用声明的方法
        static void Main(string[] args)
        {
            int hModule = LoadLibrary(@"path\xxx.dll");
            if (hModule == 0) 
                return;
            IntPtr intPtr = GetProcAddress(hModule, "xxx方法名xxx");
            //xxx委托名xxx OperatePlusFunction = (xxx委托名xxx )Marshal.GetDelegateForFunctionPointer(intPtr, typeof(xxx委托名xxx ));//函数指针封装成委托
            OperatePlus OperatePlusFunction = (OperatePlus)Marshal.GetDelegateForFunctionPointer(intPtr, typeof(OperatePlus));//函数指针封装成委托
            Console.WriteLine(OperatePlusFunction(2, 2));
            Console.Read();
        }
        
  同样C#中调用Delphi.dll c.dll 等这些操作方式应该是一样。