【在 C# 中引用 C++ 程序集】

发布于:2025-07-03 ⋅ 阅读:(18) ⋅ 点赞:(0)

在 C# 中引用 C++ 程序集(DLL)主要通过 P/Invoke(平台调用)C++/CLI 包装器 两种方式实现。由于 C# 和 C++ 的底层机制不同(如内存管理、异常处理),直接调用 C++ DLL 需要处理类型转换、调用约定等细节。以下是详细指南:


一、P/Invoke(平台调用)

适用场景:调用 C++ 导出的 纯函数(非类成员函数),通常通过 extern "C"__declspec(dllexport) 暴露接口。
特点

  • 无需修改 C++ 代码,但需手动处理数据类型映射。
  • 适合简单函数调用,不支持 C++ 类、对象或复杂数据结构。

步骤 1:C++ 端准备

// MathLibrary.h
#pragma once

#ifdef MATHLIBRARY_EXPORTS
#define MATHLIBRARY_API __declspec(dllexport)
#else
#define MATHLIBRARY_API __declspec(dllimport)
#endif

extern "C" {
    // 导出纯函数(避免名称修饰)
    MATHLIBRARY_API int Add(int a, int b);
    MATHLIBRARY_API double Multiply(double a, double b);
}
// MathLibrary.cpp
#include "MathLibrary.h"

extern "C" {
    MATHLIBRARY_API int Add(int a, int b) { return a + b; }
    MATHLIBRARY_API double Multiply(double a, double b) { return a * b; }
}

步骤 2:C# 端调用

using System;
using System.Runtime.InteropServices;

class Program
{
    // 声明 DLL 导入(需指定路径或放在输出目录)
    [DllImport("MathLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern int Add(int a, int b);

    [DllImport("MathLibrary.dll", CallingConvention = CallingConvention.Cdecl)]
    public static extern double Multiply(double a, double b);

    static void Main()
    {
        int sum = Add(3, 5);
        double product = Multiply(2.5, 4.0);
        Console.WriteLine($"Sum: {sum}, Product: {product}");
    }
}

关键注意事项

  1. 调用约定

    • C++ 默认使用 __cdecl__stdcall,需在 DllImport 中通过 CallingConvention 显式指定(如 CallingConvention.Cdecl)。
    • 若约定不匹配,可能导致栈破坏或崩溃。
  2. 数据类型映射

    • 基本类型:intintdoubledouble
    • 字符串:C++ 的 char* 需用 [MarshalAs(UnmanagedType.LPStr)] 转换。
    • 结构体:需在 C# 中定义 [StructLayout(LayoutKind.Sequential)] 的对应结构。
  3. DLL 路径

    • 将 C++ DLL 放在 C# 程序的输出目录(如 bin\Debug),或指定绝对路径。
    • 调试时可用 SetDllDirectory 动态加载路径。

二、C++/CLI 包装器(推荐复杂场景)

适用场景:需要调用 C++ 类、STL 容器或复杂对象时,通过 C++/CLI 创建托管-非托管桥接层。
特点

  • 支持面向对象调用,自动处理内存管理和类型转换。
  • 需在 Visual Studio 中创建 C++/CLI 类库项目

步骤 1:创建 C++/CLI 包装器

// MathWrapper.h
#pragma once

#include "../MathLibrary/MathLibrary.h" // 原始 C++ 头文件

namespace MathWrapper {
    public ref class Calculator // 托管类
    {
    public:
        int Add(int a, int b) { return ::Add(a, b); } // 调用原生 C++ 函数
        double Multiply(double a, double b) { return ::Multiply(a, b); }
    };
}

步骤 2:C# 端引用

  1. 在 C# 项目中添加对 C++/CLI 程序集 的引用(非原生 DLL)。
  2. 直接调用托管类:
using MathWrapper;

class Program
{
    static void Main()
    {
        Calculator calc = new Calculator();
        int sum = calc.Add(3, 5);
        double product = calc.Multiply(2.5, 4.0);
        Console.WriteLine($"Sum: {sum}, Product: {product}");
    }
}

优势对比

方式 P/Invoke C++/CLI 包装器
适用性 简单函数 复杂类、STL、面向对象调用
性能 较高(直接调用) 较低(需托管/非托管转换)
开发复杂度 需手动处理类型和调用约定 自动类型转换,代码更简洁
内存管理 需手动处理(如 IntPtr 自动托管内存

三、高级场景处理

1. 处理复杂数据结构

C++ 结构体

// C++ 端
struct Point { int x; int y; };
extern "C" MATHLIBRARY_API int GetDistance(Point p1, Point p2);

C# 端

[StructLayout(LayoutKind.Sequential)]
struct Point { public int x; public int y; }

[DllImport("MathLibrary.dll")]
public static extern int GetDistance(Point p1, Point p2);

2. 回调函数

C++ 端

typedef void (*Callback)(int result);
extern "C" MATHLIBRARY_API void RunAsync(Callback cb);

C# 端

delegate void CallbackDelegate(int result);

[DllImport("MathLibrary.dll")]
public static extern void RunAsync(CallbackDelegate cb);

// 调用时需保持委托不被垃圾回收
static void Main()
{
    CallbackDelegate callback = result => Console.WriteLine($"Result: {result}");
    RunAsync(callback);
    Console.ReadLine(); // 防止主线程退出
}

3. 内存管理

  • C++ 分配的内存(如 char*)需在 C++ 端提供释放函数,或在 C# 中用 Marshal.FreeHGlobal 释放。
  • 避免内存泄漏:确保每块分配的内存都有明确的释放路径。

四、常见问题排查

  1. DllNotFoundException

    • 检查 DLL 路径是否在输出目录或系统路径(如 PATH 环境变量)中。
    • 使用 Dependency Walker 确认 C++ DLL 的依赖项是否完整。
  2. EntryPointNotFoundException

    • 确保 C++ 函数名未被修饰(用 extern "C" 禁用名称修饰)。
    • 检查调用约定是否匹配。
  3. 数据损坏

    • 结构体对齐问题:在 C# 中用 [StructLayout(LayoutKind.Sequential, Pack=1)] 指定对齐方式。
    • 字符串编码:明确使用 LPStr(ANSI)或 LPWStr(Unicode)。

五、总结

  • 简单函数:优先用 P/Invoke,但需处理类型和调用约定。
  • 复杂对象/类:使用 C++/CLI 包装器,简化调用并自动管理内存。
  • 调试技巧
    • 在 C++ 端添加日志,确认函数是否被正确调用。
    • Marshal.PtrToStringAnsi 等工具检查内存数据。

通过合理选择方式,可以高效地在 C# 中复用 C++ 代码,平衡性能与开发效率。

注:内容由AI生成


网站公告

今日签到

点亮在社区的每一天
去签到