page contents

c#调用c动态库

本文讲述了c#调用c动态库!具有很好的参考价值,希望对大家有所帮助。一起跟随六星小编过来看看吧,具体如下:

attachments-2022-10-ECsOHR5763461e7fdaeb5.png

本文讲述了c#调用c动态库!具有很好的参考价值,希望对大家有所帮助。一起跟随六星小编过来看看吧,具体如下:

一、引言

“为什么我们需要掌握互操作技术的呢?” 对于这个问题的解释就是——掌握了.NET平台下的互操作性技术可以帮助我们在.NET中调用非托管的dll和COM组件。

。.NET 平台下提供了3种互操作性的技术:

  • Platform Invoke(P/Invoke),即平台调用,主要用于调用C库函数和Windows API
  • C++ Introp, 主要用于Managed C++(托管C++)中调用C++类库
  • COM Interop, 主要用于在.NET中调用COM组件和在COM中使用.NET程序集。

二、平台调用

使用平台调用的技术可以在托管代码中调用动态链接库(Dll)中实现的非托管函数,如Win32 Dll和C/C++ 创建的dll。

2.1 在托管代码中通过平台调用来调用非托管代码的步骤

(1).  获得非托管函数的信息,即dll的名称,需要调用的非托管函数名等信息

(2). 在托管代码中对非托管函数进行声明,并且附加平台调用所需要属性

(3). 在托管代码中直接调用第二步中声明的托管函数

平台调用的过程可以通过下图更好地理解:

2.2、如何使用平台调用Win32 函数——从实例开始

第一步就需要知道非托管函数声明,为了找到需要需要调用的非托管函数,可以借助两个工具——Visual Studio自带的dumpbin.exe和depends.exe.

  • dumpbin.exe 是一个命令行工具,可以用于查看从非托管DLL中导出的函数等信息,可以通过打开Visual Studio 2010 Command Prompt(中文版为Visual Studio 命令提示(2010)),然后切换到DLL所在的目录,输入 dummbin.exe/exports dllName, 如 dummbin.exe/exports User32.dll 来查看User32.dll中的函数声明,关于更多命令的参数可以参看MSDN;
  • 然而 depends.exe是一个可视化界面工具,大家可以从 “VS安装目录\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Tools\Bin\”  这个路径找到,然后双击  depends.exe 就可以出来一个可视化界面(如果某些人安装的VS没有附带这个工具,也可以从官方网站下载:http://www.dependencywalker.com/),如下图:

上图中 用红色标示出 MessageBox 有两个版本,而MessageBoxA 代表的就是ANSI版本,而MessageBoxW 代笔的就是Unicode版本,这也是上面所说的依据。下面就看看 MessageBox的C++声明的(更多的函数的定义大家可以从MSDN中找到,这里提供MessageBox的定义在MSDN中的链接:

1
2
3
4
5
6
int WINAPI MessageBox(
  _In_opt_  HWND hWnd,
  _In_opt_  LPCTSTR lpText,
  _In_opt_  LPCTSTR lpCaption,
  _In_      UINT uType
);

现在已经知道了需要调用的Win32 API 函数的定义声明,下面就依据平台调用的步骤,在.NET 中实现对该非托管函数的调用,下面就看看.NET中的代码的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
using System;
 
// 使用平台调用技术进行互操作性之前,首先需要添加这个命名空间
using System.Runtime.InteropServices;
 
namespace 平台调用Demo
{
    class Program
    {
        // 在托管代码中对非托管函数进行声明,并且附加平台调用所需要属性 在默认情况下,CharSet为CharSet.Ansi
        // 指定调用哪个版本的方法有两种——通过DllImport属性的CharSet字段和通过EntryPoint字段指定 在托管函数中声明注意一定要加上 static 和extern 这两个关键字
 
        //第一种指定方式,通过CharSet字段指定,在默认情况下CharSet为CharSet.Ansi
        [DllImport("user32.dll")]
        public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);
 
        [DllImport("user32.dll")]
        public static extern int MessageBoxA(IntPtr hWnd, String text, String caption, uint type);
 
        // [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int
        // MessageBox(IntPtr hWnd, String text, String caption, uint type);
 
        [DllImport("user32.dll", CharSet = CharSet.Unicode)]
        public static extern int MessageBoxW(IntPtr hWnd, String text, String caption, uint type);
 
        // 通过EntryPoint字段指定
        [DllImport("user32.dll", EntryPoint = "MessageBoxA")]
        public static extern int MessageBox3(IntPtr hWnd, String text, String caption, uint type);
 
        [DllImport("user32.dll", EntryPoint = "MessageBoxW", CharSet = CharSet.Unicode)]
        public static extern int MessageBox4(IntPtr hWnd, String text, String caption, uint type);
 
        static void Main(string[] args)
        {
            // 在托管代码中直接调用声明的托管函数 使用CharSet字段指定的方式,要求在托管代码中声明的函数名必须与非托管函数名一样 否则就会出现找不到入口点的运行时错误
 
            // 下面的调用都可以运行正确
            MessageBox(new IntPtr(0), "Learning Hard", "欢迎", 0);
            MessageBoxA(new IntPtr(0), "Learning Hard", "欢迎", 0);
            MessageBoxW(new IntPtr(0), "Learning Hard", "欢迎", 0);
 
            //使用指定函数入口点的方式调用,OK
            MessageBox3(new IntPtr(0), "Learning Hard", "欢迎", 0);
            MessageBox4(new IntPtr(0), "Learning Hard", "欢迎", 0);
        }
    }
}

2.3使用平台调用技术中,还需要注意下面4点

(1). DllImport属性的ExactSpelling字段如果设置为true时,则在托管代码中声明的函数名必须与要调用的非托管函数名完全一致,因为从ExactSpelling字面意思可以看出为 "准确拼写"的意思,当ExactSpelling设置为true时,此时会改变平台调用的行为,此时平台调用只会根据根函数名进行搜索,而找不到的时候不会添加 A或者W来进行再搜索,.

1
2
[DllImport("user32.dll", ExactSpelling=true)]
       public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type);

(2). 如果采用设置CharSet的值来控制调用函数的版本时,则需要在托管代码中声明的函数名必须与根函数名一致,否则也会调用出错

1
2
[DllImport("user32.dll")]
        public static extern int MessageBox1(IntPtr hWnd, String text, String caption, uint type);

(3). 如果通过指定DllImport属性的EntryPoint字段的方式来调用函数版本时,此时必须相应地指定与之匹配的CharSet设置,意思就是——如果指定EntryPoint为 MessageBoxW,那么必须将CharSet指定为CharSet.Unicode,如果指定EntryPoint为 MessageBoxA,那么必须将CharSet指定为CharSet.Ansi或者不指定,因为 CharSet默认值就是Ansi。

 (4). CharSet还有一个可选字段为——CharSet.Auto, 如果把CharSet字段设置为CharSet.Auto,则平台调用会针对目标操作系统适当地自动封送字符串。在 Windows NT、Windows 2000、Windows XP 和 Windows Server 2003 系列上,默认值为 Unicode;在 Windows 98 和 Windows Me 上,默认值为 Ansi。

2.3、获得Win32函数的错误信息

捕捉由托管定义导致的异常演示代码:

1
2
3
4
5
6
7
8
9
10
11
12
try
{
    MessageBox1(new IntPtr(0), "Learning Hard", "欢迎", 0);
}
catch (DllNotFoundException dllNotFoundExc)
{
    Console.WriteLine("DllNotFoundException 异常发生,异常信息为: " + dllNotFoundExc.Message);
}
catch (EntryPointNotFoundException entryPointExc)
{
    Console.WriteLine("EntryPointNotFoundException 异常发生,异常信息为: " + entryPointExc.Message);
}

捕获由Win32函数本身返回异常的演示代码如下:要想获得在调用Win32函数过程中出现的错误信息,首先必须将DllImport属性的SetLastError字段设置为true,只有这样,平台调用才会将最后一次调用Win32产生的错误码保存起来,然后会在托管代码调用Win32失败后,通过Marshal类的静态方法GetLastWin32Error获得由平台调用保存的错误码,从而对错误进行相应的分析和处理。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
class Program
{
    // Win32 API
    //  DWORD WINAPI GetFileAttributes(
    //  _In_  LPCTSTR lpFileName
    //);
 
    // 在托管代码中对非托管函数进行声明
    [DllImport("Kernel32.dll",SetLastError=true,CharSet=CharSet.Unicode)]
    public static extern uint GetFileAttributes(string filename);
 
    static void Main(string[] args)
    {
        // 试图获得一个不存在文件的属性
        // 此时调用Win32函数会发生错误
        GetFileAttributes("FileNotexist.txt");
 
        // 在应用程序的Bin目录下存在一个test.txt文件,此时调用会成功
        //GetFileAttributes("test.txt");
 
        // 获得最后一次获得的错误
        int lastErrorCode = Marshal.GetLastWin32Error();
 
        // 将Win32的错误码转换为托管异常
        //Win32Exception win32exception = new Win32Exception();
        Win32Exception win32exception = new Win32Exception(lastErrorCode);
        if (lastErrorCode != 0)
        {
            Console.WriteLine("调用Win32函数发生错误,错误信息为 : {0}", win32exception.Message);
        }
        else
        {
            Console.WriteLine("调用Win32函数成功,返回的信息为: {0}", win32exception.Message);
        }
 
        Console.Read();
    }
}

2.4 数据封送

数据封送是——在托管代码中对非托管函数进行互操作时,需要通过方法的参数和返回值在托管内存和非托管内存之间传递数据的过程,数据封送处理的过程是由CLR(公共语言运行时)的封送处理服务(即封送拆送器)完成的。

封送时需要处理的数据类型分为两种——可直接复制到本机结构中的类型(blittable)和非直接复制到本机结构中的类型(non-bittable)。

2.4.1 可直接复制到本机结构中的类型

把在托管内存和非托管内存中有相同表现形式的数据类型称为——可直接复制到本机结构中的类型,这些数据类型不需要封送拆送器进行任何特殊的处理就可以在托管和非托管代码之间传递,

下面列出一些课直接复制到本机结构中的简单数据类型:

 Windows 数据类型

非托管数据类型

托管数据类型

托管数据类型解释

 BYTE/Uchar/UInt8

unsigned char

System.Byte

无符号8位整型

 Sbyte/Char/Int8

char

System.SByte

有符号8位整型

 Short/Int16

short

System.Int16

有符号16位整型

 USHORT/WORD/UInt16/WCHAR

unsigned short

System.UInt16

无符号16位整型

 Bool/HResult/Int/Long

long/int

System.Int32

有符号32位整型

 DWORD/ULONG/UINT

unsigned long/unsigned int

System.UInt32

无符号32位整型

 INT64/LONGLONG

_int64

System.Int64

有符号64位整型

 UINT64/DWORDLONG/ULONGLONG

_uint64

System.UInt64

无符号64位整型

 INT_PTR/hANDLE/wPARAM

void*/int或_int64

System.IntPtr

有符号指针类型

 HANDLE

void*

System.UIntPtr

无符号指针类型

 FLOAT

float

System.Single

单精度浮点数

 DOUBLE

double

System.Double

双精度浮点数

除了上表列出来的简单类型之外,还有一些复制类型也属于可直接复制到本机结构中的数据类型:

(1) 数据元素都是可直接复制到本机结构中的一元数组,如整数数组,浮点数组等

(2)只包含可直接复制到本机结构中的格式化值类型

(3)成员变量全部都是可复制到本机结构中的类型且作为格式化类型封送的类

上面提到的格式化指的是——在类型定义时,成员的内存布局在声明时就明确指定的类型。在代码中用StructLayout属性修饰被指定的类型,并将StructLayout的LayoutKind属性设置为Sequential或Explicit,例如:

1
2
3
4
5
6
7
8
using System.Runtime.InteropServices;
 
// 下面的结构体也属于可直接复制到本机结构中的类型
[StructLayout(LayoutKind.Sequential)]
public struct Point {
   public int x;
   public int y;
}

2.4.2 非直接复制到本机结构中的类型

对于这种类型,封送器需要对它们进行相应的类型转换之后再复制到被调用的函数中,下面列出一些非直接复制到本机结构中的数据类型:

 Windows 数据类型

非托管数据类型

托管数据类型

托管数据类型解释

 Bool

bool

System.Boolean

布尔类型

 WCHAR/TCHAR

char/ wchar_t

System.Char

ANSI字符/Unicode字符

 LPCSTR/LPCWSTR/LPCTSTR/LPSTR/LPWSTR/LPTSTR

const char*/const wchar_t*/char*/wchar_t*

System.String

ANSI字符串/Unicode字符串,如果非托管代码不需要更新此字符串时,此时用String类型在托管代码中声明字符串类型

 LPSTR/LPWSTR/LPTSTR

Char*/wchar_t*

System.StringBuilder

ANSI字符串/Unicode字符串,如果非托管代码需要更新此字符串,然后把更新的字符串传回托管代码中,此时用StringBuilder类型在托管代码中声明字符串

除了上表中列出的类型之外,还有很多其他类型属于非直接复制到本机结构中的类型,例如其他指针类型和句柄类型等。

2.4.3、封送字符串的处理

封送作为返回值的字符串,下面是一段演示代码,代码中主要是调用Win32 GetTempPath函数来获得返回临时路径,此时拆送器就需要把返回的字符串封送回托管代码中。使用System.StringBuilder托管数据类型。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
// 托管函数中的返回值封送回托管函数的例子
class Program
{
 
    // Win32 GetTempPath函数的定义如下:  
    //DWORD WINAPI GetTempPath(
    //  _In_   DWORD nBufferLength,
    //  _Out_  LPTSTR lpBuffer
    //); 
 
    // 主要是注意如何在托管代码中定义该函数原型      
    [DllImport("Kernel32.dll", CharSet = CharSet.Unicode, SetLastError=true)]
    public static extern uint GetTempPath(int bufferLength, StringBuilder buffer);
    static void Main(string[] args)
    {
        StringBuilder buffer = new StringBuilder(300);
        uint tempPath=GetTempPath(300, buffer);
        string path = buffer.ToString();
        if (tempPath == 0)
        {
            int errorcode =Marshal.GetLastWin32Error();
            Win32Exception win32expection = new Win32Exception(errorcode);
            Console.WriteLine("调用非托管函数发生异常,异常信息为:" +win32expection.Message);
        }
 
        Console.WriteLine("调用非托管函数成功。");
        Console.WriteLine("Temp 路径为:" + buffer);
        Console.Read();
    }
}

2.4.4、封送结构体的处理

在我们实际调用Win32 API函数时,经常需要封送结构体和类等复制类型,下面就以Win32 函数GetVersionEx为例子来演示如何对作为参数的结构体进行封送处理。

下面是GetVersionEx非托管定义

1
2
3
BOOL GetVersionEx(
  LPOSVERSIONINFO lpVersionInformation
);

参数lpVersionInformation是一个指向 OSVERSIONINFO结构体的指针类型,所以我们在托管代码中为函数GetVersionEx函数之前,必须知道 OSVERSIONINFO结构体的非托管定义,然后再在托管代码中定义一个等价的结构体类型作为参数。以下是OSVERSIONINFO结构体的非托管定义:

1
2
3
4
5
6
7
8
9
10
11
12
13
typedef struct  _OSVERSIONINFO{
    DWORD dwOSVersionInfoSize;       //在使用GetVersionEx之前要将此初始化为结构的大小
    DWORD dwMajorVersion;               //系统主版本号
    DWORD dwMinorVersion;               //系统次版本号
    DWORD dwBuildNumber;               //系统构建号
    DWORD dwPlatformId;                  //系统支持的平台
    TCHAR szCSDVersion[128];          //系统补丁包的名称
    WORD wServicePackMajor;            //系统补丁包的主版本
    WORD wServicePackMinor;            //系统补丁包的次版本
    WORD wSuiteMask;                      //标识系统上的程序组
    BYTE wProductType;                    //标识系统类型
    BYTE wReserved;                         //保留,未使用
} OSVERSIONINFO;

知道了OSVERSIONINFO结构体在非托管代码中的定义之后, 现在我们就需要在托管代码中定义一个等价的结构,并且要保证两个结构体在内存中的布局相同。托管代码中的结构体定义如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
// 因为Win32 GetVersionEx函数参数lpVersionInformation是一个指向 OSVERSIONINFO的数据结构
// 所以托管代码中定义个结构体,把结构体对象作为非托管函数参数
[StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
public struct OSVersionInfo
{
    public UInt32 OSVersionInfoSize; // 结构的大小,在调用方法前要初始化该字段
    public UInt32 MajorVersion; // 系统主版本号
    public UInt32 MinorVersion; // 系统此版本号
    public UInt32 BuildNumber;  // 系统构建号
    public UInt32 PlatformId;  // 系统支持的平台
 
    // 此属性用于表示将其封送成内联数组
    [MarshalAs(UnmanagedType.ByValTStr,SizeConst=128)]
    public string CSDVersion; // 系统补丁包的名称
    public UInt16 ServicePackMajor; // 系统补丁包的主版本
    public UInt16 ServicePackMinor;  // 系统补丁包的次版本
    public UInt16 SuiteMask;   //标识系统上的程序组
    public Byte ProductType;    //标识系统类型
    public Byte Reserved;  //保留,未使用
}

从上面的定义可以看出, 托管代码中定义的结构体有以下三个方面与非托管代码中的结构体是相同的:

  • 字段声明的顺序
  • 字段的类型
  • 字段在内存中的大小

并且在上面结构体的定义中,我们使用到了 StructLayout 属性,该属性属于System.Runtime.InteropServices命名空间(所以在使用平台调用技术必须添加这个额外的命名空间)。这个类的作用就是允许开发人员显式指定结构体或类中数据字段的内存布局,为了保证结构体中的数据字段在内存中的顺序与定义时一致,所以指定为 LayoutKind.Sequential(该枚举也是默认值)。

下面就具体看看在托管代码中调用的代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
using System;
using System.ComponentModel;
using System.Runtime.InteropServices;
namespace 封送结构体的处理
{
    class Program
    {
        // 对GetVersionEx进行托管定义
        // 为了传递指向结构体的指针并将初始化的信息传递给非托管代码,需要用ref关键字修饰参数
        // 这里不能使用out关键字,如果使用了out关键字,CLR就不会对参数进行初始化操作,这样就会导致调用失败
        [DllImport("Kernel32",CharSet=CharSet.Unicode,EntryPoint="GetVersionEx")]
        private static extern Boolean GetVersionEx_Struct(ref  OSVersionInfo osVersionInfo);
 
        // 因为Win32 GetVersionEx函数参数lpVersionInformation是一个指向 OSVERSIONINFO的数据结构
        // 所以托管代码中定义个结构体,把结构体对象作为非托管函数参数
        [StructLayout(LayoutKind.Sequential,CharSet=CharSet.Unicode)]
        public struct OSVersionInfo
        {
            public UInt32 OSVersionInfoSize; // 结构的大小,在调用方法前要初始化该字段
            public UInt32 MajorVersion; // 系统主版本号
            public UInt32 MinorVersion; // 系统此版本号
            public UInt32 BuildNumber;  // 系统构建号
            public
  • 发表于 2022-10-12 09:57
  • 阅读 ( 246 )
  • 分类:C/C++开发

你可能感兴趣的文章

相关问题

0 条评论

请先 登录 后评论
轩辕小不懂
轩辕小不懂

2403 篇文章

作家榜 »

  1. 轩辕小不懂 2403 文章
  2. Pack 1131 文章
  3. 小柒 1046 文章
  4. Nen 576 文章
  5. 王昭君 209 文章
  6. 文双 71 文章
  7. 小威 64 文章
  8. Cara 36 文章