数字类型
基本类型没有什么特别的地方,直接传递即可:
1 | // void __stdcall Foo1(char v1, short v2, int v3, float v4, double v5) |
对于指针或引用类型需要加入ref
关键字:
1 | // void __stdcall Foo2(int* v1, int& v2) |
引用类型是 C++ 特有的,从规范上讲不建议在接口中使用引用类型,而应该用指针。
数字类型对照表
下面是一些常见 C 数字类型及其在 C# 中的对应类型的对照表:
C 数据类型 | C# 数据类型 |
---|---|
char | char |
unsigned char | byte |
signed char | sbyte |
short | short |
unsigned short | ushort |
int | int |
unsigned int | uint |
long | long |
unsigned long | ulong |
long long | long (or) System.Int64 |
unsigned long long | ulong (or) System.UInt64 |
float | float |
double | double |
long double | decimal |
size_t | UIntPtr |
布尔类型
在 DLL 接口中,一般能见到bool
和BOOL
两种类型:
1 | bool Foo1(); // 不推荐 |
bool
和BOOL
是 C++ 和 Windows API 中使用的两种不同的布尔类型,它们之间存在一些区别。
bool
类型 (C++ 中):bool
是 C++ 标准库中引入的布尔类型。bool
类型的大小在标准规范中没有特别指定,但通常情况下,它占用一个字节(8位)。bool
类型的值可以是true
或false
。
BOOL
类型 (Windows API 中):BOOL
是 Windows API 中广泛使用的布尔类型。BOOL
类型在 Windows API 中通常被定义为一个 4 字节整数(32位),其中FALSE
被定义为0
,其他任何值被定义为TRUE
。BOOL
类型的使用是为了与 Windows 操作系统和其 API 的兼容性。
C# 中的bool
类型可以很好地与 C++ 的bool
类型兼容,因为它们都表示布尔值,即true
或false
。
如果要与使用BOOL
的 C++ 代码进行交互,需要进行一些额外的处理。
例如,对于一个返回BOOL
的 Windows API 函数:
1 | BOOL MyFunction(); |
可以在 C# 中这样声明:
1 | [ ] |
使用MarshalAs
特性来确保BOOL
被正确映射到bool
。
其实不使用 [return: MarshalAs(UnmanagedType.Bool)]
特性也可能得到正确的结果,因为 .NET 框架通常会自动处理返回值的映射。然而,使用MarshalAs
是一个良好的做法,因为它提供了更清晰的代码和更可靠的结果。
关于布尔类型的一些知识:What is the size of a boolean In C#? Does it really take 4-bytes?
数组
数组本质上就是指针。
1 | // void __stdcall Foo5(int* arr1, int arr2[]) |
注意:函数内对数组的修改会影响原数组,因为数组是引用类型。
字符串
默认是窄字符,使用本机编码:
1 | // void __stdcall Foo3(const char* str) |
如果是宽字符,则需要通过 CharSet 参数指定为Unicode
:
1 | // void __stdcall Foo4(const wchar_t* str) |
如果同时出现窄字符和宽字符(不好的设计):
1 | // void __stdcall Foo7(const char* str1, const wchar_t* str2) |
通过 MarshalAsAttribute 指示封送类型。而CharSet
将不再生效。
传出字符串
使用 StringBuilder 类型接受字符串。
1 | // void __stdcall Foo6(char* buf, const int bufLen) |
返回字符串
某些情况下可能会直接返回一个char*
作为字符串:
1 | char* __stdcall Foo8() |
那么此时就不能用string
接受了,而需要使用IntPtr
作为返回类型,再用 Marshal.PtrToStringAnsi 转为字符串:
1 | [ ] |
宽字符版:Marshal.PtrToStringUni
UTF-8版:Marshal.PtrToStringUTF8
另外还要注意,当 DLL 在堆上分配了内存后,还应该提供释放内存的接口方法,否则会产生内存泄露。
结构类型
结构是值类型,所以和基础类型的用法一样:
下面是一个示例:
1 |
|
注意,结构需要通过 StructLayout 指定内存布局。
1 | [// 顺序布局,也就是按1字节对齐 ] |
如果需要在函数内对结构进行修改,那么需要使用ref
来定义参数:
1 | [ ] |
返回结构
与字符串不同,结构体可以直接返回:
1 | MyStruct __stdcall Foo10() |
1 | [ ] |
返回结构指针
可以用ref
关键字定义结构体指针:
1 | // MyStruct* __stdcall Foo11() |
但是这样无法得到结构的指针,也就不能释放它了,导致内存泄露。
所以建议使用IntPtr
作为返回类型,然后用 Marshal.PtrToStructure 转换:
1 | IntPtr p = MyLib.Foo11(); |
回调函数
需要使用委托来实现。
1 | // 假设我们要定义一个回调函数原型,它接受一个整数参数并返回一个整数。 |
在C#中使用DLL中的回调函数:
1 | // 委托的签名与回调函数相匹配 |
这样,C#中的委托将在DLL内部的函数中被调用,并且C#代码可以处理回调函数的结果。
调用 Win32 API
微软推出了 Microsoft.Windows.CsWin32 包,使得调用 Win32 API 异常简单。
安装包后,在工程目录下新建一个NativeMethods.txt
文件,在其中写入要用到的符号,包括函数、常量等名称即可,每行一个。
然后以PInvoke.XXX
方式使用即可。
参考
https://www.mono-project.com/docs/advanced/pinvoke/
https://manski.net/2012/05/pinvoke-tutorial-basics-part-1/
https://manski.net/2012/06/pinvoke-tutorial-passing-strings-part-2/
https://manski.net/2012/06/pinvoke-tutorial-passing-parameters-part-3/
https://manski.net/2012/06/pinvoke-tutorial-pinning-part-4/
UnmanagedType 枚举