C#调用C++动态库:从基础字符串到指针结构的实现

林舟
林舟
发布时间:2026-10-14 | 2人浏览过
原创

C#开发中常需调用C++编写的原生DLL以获取高性能或复用现有逻辑。本文深入解析C#通过Platform Invocation Services (P/Invoke)调用C++方法的两种核心场景:基础类型参数传递,以及复杂结构体与指针数据的交互。内容包含完整的声明定义、内存分配策略及数据类型转换代码示例,帮助开发者安全高效地桥接托管与非托管代码。

基础调用:简单类型参数处理

当C++函数仅接受基本类型(如int, string)且不返回复杂结构时,调用最为简单。关键在于正确使用DllImport特性指定DLL名称,并确保C#中的参数类型与C++签名匹配。对于字符串参数,默认情况下P/Invoke会将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和非托管结构之间进行数据拷贝。适合对安全性要求高、不允许启用不安全代码的项目。

  1. 调用API获取IntPtr指向的内存块和记录总数。
  2. 计算每个结构体的大小,并通过指针算术遍历内存块。
  3. 使用Marshal.PtrToStructure将数据复制到新的C#对象中。
  4. 处理完成后,务必释放非托管内存,防止泄漏。
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上下文,直接操作指针可以显著减少内存拷贝开销,提高大数据量下的性能。此方法需要编译器选项允许不安全代码。

  1. 声明unsafe方法。
  2. 使用out T*类型的参数直接接收指针。
  3. 通过指针算术直接访问内存中的数据。
  4. 读取字符串时使用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指针以提升效率。无论哪种方式,务必牢记非托管资源的生命周期管理,避免内存泄漏。

以上就是C#调用C++动态库处理指针与结构体的详细内容,更多关于C#非托管互操作技术的相关资料请关注本站其它相关文章!
相关标签:

本站声明

本文内容仅供参考,不代表本站观点。转载或引用请注明出处。如发现内容问题,请通过联系我们页面反馈。

Python 相关产品和应用

全部下载

专题列表