C#调用C++动态库:从基础字符串到指针结构的实现
C#开发中常需调用C++编写的原生DLL以获取高性能或复用现有逻辑。本文深入解析C#通过Platform Invocation Services (P/Invoke)调用C++方法的两种核心场景:基础类型参数传递,以及复杂结构体与指针数据的交互。内容包含完整的声明定义、内存分配策略及数据类型转换代码示例,帮助开发者安全高效地桥接托管与非托管代码。
基础调用:简单类型参数处理
当C++函数仅接受基本类型(如int, string)且不返回复杂结构时,调用最为简单。关键在于正确使用string视为不可变的输入参数,这通常符合大多数C++函数的期望。
第1步:声明静态外部方法
在C#类中引入System.Runtime.InteropServices命名空间,并使用[DllImport]标记方法。以下示例演示如何调用一个验证用户名的函数:
[DllImport("um_web_client.dll")]
private static extern int CheckUserName(string UserName);
// 封装调用逻辑
public static int AuthUserName(string username)
{
// 假设这是检查连接的逻辑
if(!IsConnection())
{
StartWebClient(UDBClientIP, UDBClientPort, UDBClientTimeout);
}
return CheckUserName(username);
}
注意:此处CheckUserName直接接收string。若C++端修改了该字符串,C#端的不可变性可能导致问题,但在只读场景中这是最便捷的方式。
高级调用:处理结构体与指针
当C++函数返回结构体数组、指针或需要双向数据传输时,必须显式控制内存布局和数据封送(Marshaling)。这是因为.NET运行时无法自动将托管对象映射到非托管的内存块。
第2步:定义兼容的结构体布局
在C#中定义与C++结构体字段一致的类型。使用StructLayout(LayoutKind.Sequential)确保字段在内存中的排列顺序与C++一致。对于字符指针等类型,需注意编码(Ansi/Unicode)和数据类型(IntPtr vs string)的选择。
// 用于Marshal.PtrToStructure的普通类(堆上分配)
[StructLayout(LayoutKind.Sequential, CharSet=CharSet.Ansi )]
public class tagPlayerInfoOut
{
public int id;
public String PlayerName; // 注意:作为字符串字段时, Marshal可能不处理指针
public int Country;
public int OfficerRace;
public int Level;
public int MilitaryRank;
public int Money;
public int ResourceCount;
public int CityCount;
public int GeneralCount;
public int Credit;
}
// 用于指针运算的Struct(值类型),IntPtr更准确对应 char* 等非托管内存
[StructLayout(LayoutKind.Sequential)]
public struct tagPlayerInfoOutEx
{
public int id;
public IntPtr PlayerName; // 对应 C++ 中的 char*,由 Marshal.PtrToStringAnsi 读取
public int Country;
public int OfficerRace;
public int Level;
public int MilitaryRank;
public int Money;
public int ResourceCount;
public int CityCount;
public int GeneralCount;
public int Credit;
}
风险提醒:使用Class配合PtrToStructure时,必须在循环内不断创建新实例或使用固定逻辑,因为每次操作都是基于同一地址复制数据。使用Struct配合unsafe指针访问则更为直接且高效,但需启用编译器的不安全代码支持。
第3步:声明接收指针或输出参数的API
根据C++函数的实际签名,C#声明有两种常见写法:一种使用out IntPtr接收原始内存地址,另一种使用双重指针语法(虽不推荐在C#中使用**,通常用ref或out IntPtr模拟)。以下展示标准的out IntPtr方式:
[DllImport("um_web_client.dll")]
public static extern int GetPlayerListCs(int maxrows, out int rows, out IntPtr playerlist);
第4步:执行第一种解法——使用Marshal手动转换
此方法不使用不安全代码,通过Marshal类在IntPtr和非托管结构之间进行数据拷贝。适合对安全性要求高、不允许启用不安全代码的项目。
- 调用API获取
IntPtr指向的内存块和记录总数。 - 计算每个结构体的大小,并通过指针算术遍历内存块。
- 使用
Marshal.PtrToStructure将数据复制到新的C#对象中。 - 处理完成后,务必释放非托管内存,防止泄漏。
public static DataTable UsingMarshal()
{
if(!IsConnection())
{
StartWebClient(UDBClientIP, UDBClientPort, UDBClientTimeout);
}
DataTable dt = new DataTable();
// 添加列...
dt.Columns.Add("id");
dt.Columns.Add("PlayerName");
// 其他列...
int size = 0;
IntPtr outArray;
// 调用获取数据
GetPlayerListCs(20, out size, out outArray);
IntPtr current = outArray;
for(int i = 0; i < size; i++)
{
// 每次迭代创建新实例以存储当前切片的数据
tagPlayerInfoOut entry = new tagPlayerInfoOut();
// 从非托管内存复制到托管结构
Marshal.PtrToStructure(current, entry);
// 可选:如果结构体中包含复杂的自定义非托管资源,可能需要 DestroyStructure
// Marshal.DestroyStructure(current, typeof(tagPlayerInfoOut));
// 移动指针到下一个结构体位置
current = (IntPtr)((long)current + Marshal.SizeOf(entry));
// 添加到DataTable
dt.Rows.Add(new object[] {
entry.id,
entry.PlayerName,
entry.Country,
// 其他字段...
entry.Credit
});
dt.AcceptChanges();
}
// 【关键】释放C++分配的内存
Marshal.FreeCoTaskMem(outArray);
return dt;
}
第5步:执行第二种解法——使用Unsafe指针优化性能
如果项目允许unsafe上下文,直接操作指针可以显著减少内存拷贝开销,提高大数据量下的性能。此方法需要编译器选项允许不安全代码。
- 声明
unsafe方法。 - 使用
out T*类型的参数直接接收指针。 - 通过指针算术直接访问内存中的数据。
- 读取字符串时使用
Marshal.PtrToStringAnsi或Utf8相关方法。
public static unsafe DataTable UsingUnsafe()
{
int size;
if(!IsConnection())
{
StartWebClient(UDBClientIP, UDBClientPort, UDBClientTimeout);
}
DataTable dt = new DataTable();
dt.Columns.Add("id");
dt.Columns.Add("PlayerName");
// 其他列...
tagPlayerInfoOutEx* pResult;
// 直接接收指针
GetPlayerListCs(20, out size, out pResult);
tagPlayerInfoOutEx* pCurrent = pResult;
for(int i = 0; i < size; i++, pCurrent++)
{
// 直接访问成员
int id = pCurrent->id;
// 将IntPtr转换为字符串
string name = Marshal.PtrToStringAnsi(pCurrent->PlayerName);
dt.Rows.Add(new object[] {
id,
name,
pCurrent->Country,
// 其他字段...
pCurrent->Credit
});
dt.AcceptChanges();
}
// 释放内存
Marshal.FreeCoTaskMem((IntPtr)pResult);
return dt;
}
注意事项与最佳实践
- 内存泄漏:所有从C++返回的非托管内存(如
malloc或new分配的数组)必须由C#端负责释放,通常使用Marshal.FreeCoTaskMem。切勿遗漏释放步骤。 - 编码一致性:C++字符串可能是ANSI、UTF-8或Unicode。定义
StructLayout时需设置正确的CharSet,并在读取IntPtr时使用对应的Marshal字符串转换方法。 - 平台架构:确保C#进程的平台目标(x86/x64)与C++ DLL的编译架构完全一致,否则会导致
DllNotFoundException或栈不平衡错误。
简要总结
C#调用C++核心在于理解内存边界。DllImport适用于简单交互;涉及数组或复杂结构时,推荐使用out IntPtr结合Marshal进行数据搬运,或在允许的情况下使用unsafe指针以提升效率。无论哪种方式,务必牢记非托管资源的生命周期管理,避免内存泄漏。
免责声明:本文内容仅供参考,不代表本站观点。转载或引用请注明出处。









