C# 调用非托管 DLL 参数传递总结
2024-03-17 17:33:41

数字类型

基本类型没有什么特别的地方,直接传递即可:

1
2
3
// void __stdcall Foo1(char v1, short v2, int v3, float v4, double v5)

internal static extern void Foo1(char v1, short v2, int v3, float v4, double v5);

对于指针或引用类型需要加入ref关键字:

1
2
3
// void __stdcall Foo2(int* v1, int& v2)

internal static extern void Foo2(ref int v1, ref 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 接口中,一般能见到boolBOOL两种类型:

1
2
bool Foo1(); // 不推荐
BOOL Foo2(); // 推荐

boolBOOL是 C++ 和 Windows API 中使用的两种不同的布尔类型,它们之间存在一些区别。

  1. bool类型 (C++ 中):

    • bool是 C++ 标准库中引入的布尔类型。
    • bool类型的大小在标准规范中没有特别指定,但通常情况下,它占用一个字节(8位)。
    • bool类型的值可以是 truefalse
  2. BOOL类型 (Windows API 中):

    • BOOL是 Windows API 中广泛使用的布尔类型。
    • BOOL类型在 Windows API 中通常被定义为一个 4 字节整数(32位),其中FALSE被定义为0,其他任何值被定义为TRUE
    • BOOL类型的使用是为了与 Windows 操作系统和其 API 的兼容性。


C# 中的bool类型可以很好地与 C++ 的bool类型兼容,因为它们都表示布尔值,即truefalse
如果要与使用BOOL的 C++ 代码进行交互,需要进行一些额外的处理。
例如,对于一个返回BOOL的 Windows API 函数:

1
BOOL MyFunction();

可以在 C# 中这样声明:

1
2
3
[DllImport("YourLibrary.dll")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool MyFunction();

使用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
2
3
4
// void __stdcall Foo5(int* arr1, int arr2[])

[DllImport(DLL_NAME)]
internal static extern void Foo5(int[] arr1, int[] arr2);

注意:函数内对数组的修改会影响原数组,因为数组是引用类型。

字符串

默认是窄字符,使用本机编码:

1
2
3
4
// void __stdcall Foo3(const char* str)

[DllImport(DLL_NAME)]
internal static extern void Foo3(string v1);

如果是宽字符,则需要通过 CharSet 参数指定为Unicode

1
2
3
4
// void __stdcall Foo4(const wchar_t* str)

[DllImport(DLL_NAME, CharSet = CharSet.Unicode)]
internal static extern void Foo4(string v1);

如果同时出现窄字符和宽字符(不好的设计):

1
2
3
4
// void __stdcall Foo7(const char* str1, const wchar_t* str2)

[DllImport(DLL_NAME, CharSet = CharSet.Unicode)]
internal static extern void Foo7([MarshalAs(UnmanagedType.LPStr)]string v1, [MarshalAs(UnmanagedType.LPWStr)]string v2);

通过 MarshalAsAttribute 指示封送类型。而CharSet将不再生效。

传出字符串

使用 StringBuilder 类型接受字符串。

1
2
3
4
5
6
7
// void __stdcall Foo6(char* buf, const int bufLen)

[DllImport(DLL_NAME)]
internal static extern void Foo6(StringBuilder str, int len);

var sb = new StringBuilder(10);
MyLib.Foo6(sb, sb.Capacity);

返回字符串

某些情况下可能会直接返回一个char*作为字符串:

1
2
3
4
5
6
7
char* __stdcall Foo8()
{
const char* myString = "hello";
char* dynamicString = new char[strlen(myString) + 1];
strcpy(dynamicString, myString);
return dynamicString;
}

那么此时就不能用string接受了,而需要使用IntPtr作为返回类型,再用 Marshal.PtrToStringAnsi 转为字符串:

1
2
3
4
5
[DllImport(DLL_NAME)]
internal static extern IntPtr Foo8();

IntPtr p = MyLib.Foo8();
string s = Marshal.PtrToStringAnsi(p);

宽字符版:Marshal.PtrToStringUni
UTF-8版:Marshal.PtrToStringUTF8
另外还要注意,当 DLL 在堆上分配了内存后,还应该提供释放内存的接口方法,否则会产生内存泄露。

结构类型

结构是值类型,所以和基础类型的用法一样:
下面是一个示例:

1
2
3
4
5
6
7
8
9
10
11
12
13
#pragma pack(push, 1)  // 内存按1字节对齐
struct MyStruct
{
int x;
int y;
};
#pragma pack(pop)

void __stdcall Foo9(MyStruct s)
{
s.x = 5;
s.y = 10;
}

注意,结构需要通过 StructLayout 指定内存布局。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
[StructLayout(LayoutKind.Sequential)]  // 顺序布局,也就是按1字节对齐
internal struct MyStruct
{
public int x;
public int y;
}

[DllImport(DLL_NAME)]
internal static extern void Foo9(MyStruct ms);

var ms = new MyStruct();
ms.x = 1;
ms.y = 2;
MyLib.Foo9(ms);
MessageBox.Show($"{ms.x}, {ms.y}"); // 1, 2

如果需要在函数内对结构进行修改,那么需要使用ref来定义参数:

1
2
[DllImport(DLL_NAME)]
internal static extern void Foo9(ref MyStruct ms);

返回结构

与字符串不同,结构体可以直接返回:

1
2
3
4
5
6
7
MyStruct __stdcall Foo10()
{
MyStruct ms;
ms.x = 5;
ms.y = 10;
return ms;
}
1
2
[DllImport(DLL_NAME)]
internal static extern MyStruct Foo10();

返回结构指针

可以用ref关键字定义结构体指针:

1
2
3
4
// MyStruct* __stdcall Foo11()

[DllImport(DLL_NAME)]
internal static extern ref MyStruct Foo11();

但是这样无法得到结构的指针,也就不能释放它了,导致内存泄露。
所以建议使用IntPtr作为返回类型,然后用 Marshal.PtrToStructure 转换:

1
2
3
4
IntPtr p = MyLib.Foo11();
var ms = Marshal.PtrToStructure<MyStruct>(p);
MessageBox.Show($"{ms.x}, {ms.y}");
MyLib.Free(p); // DLL提供释放接口

回调函数

需要使用委托来实现。

1
2
3
4
5
6
// 假设我们要定义一个回调函数原型,它接受一个整数参数并返回一个整数。
typedef int (*CallbackFunction)(int);

int __stdcall SomeFunction(CallbackFunction callback) {
return callback(42);
}

在C#中使用DLL中的回调函数:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// 委托的签名与回调函数相匹配
public delegate int CallbackDelegate(int value);

[DllImport("YourDLL.dll")]
public static extern int SomeFunction([MarshalAs(UnmanagedType.FunctionPtr)] CallbackDelegate callback);

// 创建委托实例,并将其传递给DLL中的函数
CallbackDelegate myCallback = (value) => {
Console.WriteLine($"Callback called with value: {value}");
return value * 2; // 做一些处理并返回结果
};

// 调用DLL函数,并传递委托
int result = SomeFunction(myCallback);
Console.WriteLine($"Result from DLL: {result}");

这样,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 枚举

上一页
2024-03-17 17:33:41
下一页