利用PInvoke实现C++和C#联合编程

C#和C++联合编程有以下两种常用的方式:

  1. 利用PInvoke实现直接调用,该方法仅支持函数调用。
  2. 利用C++/CLI作为代理中间层。

基于PInvoke调用C++

数据类型大小

虽然不同语言中可能对于不同类型的名字不同,但其根本上都是在操作对应数据的指针。至少能够确定好C#和C++之间的类型对应关系(字节数相同),那么我们就可以通过指针传递数据地址实现跨语言的通信。下面给出两种语言之间的基础类型对应关系。

C/C++ C# 长度
short short 2 Bytes
int int 4 Bytes
long(该类型在传递的时候常常会弄混) int 4 Bytes
bool bool 1 Byte
char(ASCII 字符) byte 1 Byte
wchar_t(Unicode 字符,与 C# 的 Char 兼容) char 2 Bytes
float float 4 Bytes
double double 8 Bytes

调用流程

在VS中创建一个C++动态链接库。然后分别写头文件和源文件。对于头文件的编写我们可以直接使用MSDN中提供的头文件模板进行修改。另外对于需要预编译的文件,我们还需要引用pch.h头文件才能编译成功。

头文件模板
// MathLibrary.h - Contains declarations of math functions
//防止重复编译
#pragma once

//用于指定函数用于导出还是导入
#ifdef MATHLIBRARY_EXPORTS
#define MATHLIBRARY_API __declspec(dllexport)
#else
#define MATHLIBRARY_API __declspec(dllimport)
#endif

//以C的命名规范去导出函数名(同名),如果用C++的名字会很乱
extern "C" MATHLIBRARY_API void fibonacci_init(
    const unsigned long long a, const unsigned long long b);

extern "C" MATHLIBRARY_API bool fibonacci_next();

extern "C" MATHLIBRARY_API unsigned long long fibonacci_current();

extern "C" MATHLIBRARY_API unsigned fibonacci_index();

参数传递

我们简单定义一个函数来实现基本数据类型的传参和返回值。然后在一个C#控制台应用中调用该函数。

cpp
//test.h
#pragma once
#include"pch.h"

#ifdef MATHLIBRARY_EXPORTS
#define MATHLIBRARY_API __declspec(dllexport)
#else
#define MATHLIBRARY_API __declspec(dllimport)
#endif
extern "C" MATHLIBRARY_API int sayHello(int a);

//test.cpp
#include "test.h"
int sayHello(int a) 
{
	return a + 3;
}
cs
internal class Program
{

    [DllImport("./TestDll.dll", EntryPoint = "sayHello",CharSet =CharSet.Ansi)]
    extern static int mySayHello(int a);
    private static void Main(string[] args)
    {

        var b = mySayHello(3);
        Console.WriteLine(b);
    }
}

需要注意的是,我们在C#中要使用特性标识该函数为指定Dll中的指定方法(EntryPoint)。
image
介绍几个比较重要的参数。

  1. dllName:所需调用的DLL文件的路径。
  2. EntryPoint:调用函数的名称(同名时可以不写)。
  3. CharSet:指定字符编码规则,仅在传递字符或字符串时生效。因为C++和C#的默认编码规则不同。如果不指定CharSet说明一个字符编码的字节数量,可能会导致C++那边读取错位。
  4. CallingConvention:双方约定谁对调用堆栈负责。函数返回后会有一个出栈的操作,如果双方都对栈进行清理,那么肯定会出错。
CallingConvention C/C++ 中常见对应 参数压栈顺序 谁清理栈 是否支持变参 典型使用场景 备注
Winapi Windows API(自动选择) 右 → 左 被调用者 调用 Windows API 默认值,在 Windows 下等价于 StdCall
Cdecl __cdecl 右 → 左 调用者 C 库函数(如 printf 唯一支持可变参数
StdCall __stdcall 右 → 左 被调用者 Win32 DLL 导出函数 栈更安全,Windows API 常用
ThisCall __thiscall 右 → 左(this 在寄存器) 被调用者 C++ 成员函数 P/Invoke 几乎不用
FastCall __fastcall 寄存器 + 栈 被调用者 高性能场景 跨语言不推荐

调用成功结果如下:
image

指针的传递

由于C#是托管代码,传递指针时。我们必须防止GC因为优化内存布局导致C#这边的指针发生移动。所以需要使用fixed关键字固定住指针,直到C++那边处理结束。
针对一下类型的指针,我们需要进行固定。

  1. 托管数组(如 int[]、byte[]、char[])。
  2. 包含非托管类型字段的结构体(struct)(需用 fixed 固定结构体的字段指针)。
  3. string 类型,实际上是char[](固定后可获取其字符的指针)。

复杂类型传递

这里的复杂类型主要指的是结构体。传递结构体主要注意以下几点。

  1. 要传递的成员为公有的值类型字段
  2. C#中结构体字段类型与C++结构体中的字段类型相兼容
  3. C#结构中的字段顺序与C++结构体中的字段顺序相同,要保证该功能,需要将C#结构体标记为[StructLayout(LayoutKind.Sequential)],如果有字符串类型数据还需要指定CharSet

我们首先在C#中定义如下的结构体。
image

结构体特性中的CharSet属性指明当前结构体中的字符数据以何种方式内放在内存中。
ANSI===char*
Unicode===wchar_t*


这里起作用的是结构体特性中指定的CharSet,因为调用的API没有字符串类型的参数,所以DllImport特性中的CharSet没影响。

然后在CPP中声明一个同样结构的结构体。
image
调用方法相同.这里可能还会是乱码,但数据是正确传过去了,只需要在CPP那边修改一下控制台的编码规则,即可正常显示中文。
image

传递C#函数指针实现C++回调

C++本身就支持函数指针,C#这边的函数指针实际上就是委托。因此完全可以实现这种回调。
参数列表的参数要保持一致。在C#那边用委托声明函数指针,然后CPP就用正常的函数指针声明。
CS中声明如下
image
C++中声明如下
image
image

这个回调会把C++那边传入的参数加3再返回。最后结果打印一个6说明调用成功,并且你在C#开调试断点也能够进来。
image

跨语言的类传递

由于PInvoke本身不支持Class的传递,这里的类传递也是通过指针实现的,并且传递的类没办法在两种语言之间自动转换。建议的用法就是C#这边就保留一个对应类的指针(IntPtr),有关类的操作全部(包括创建、销毁)等交给C++处理,然后返回对应的结果。 此时C++那边相当于一个工厂。C#这边通过判断指针监视其生命周期。由于方法的限制,我们只能通过静态方法去获取类的数据。

posted @ 2025-12-15 20:28  Ytytyty  阅读(2)  评论(0)    收藏  举报