C#与C++互操作

托管代码和非托管代码的交互技术有3种:平台调用(PInvoke)、COM Interop、利用C++/CLI作为代理中间层
本文暂不讨论COM Interop。
本文代码测试平台:Windows7 64位,VS2015 Pro,.NET4.5

C#调用C++

C#通过PInvoke调用WIN32 API

PInvoke本质就是调用dll,dll里面包含一系列C/C++ 的API。
PInvoke最简单,但只能调用函数,不能直接调用类
但有一个折衷的办法,就是在C++里面定义一系列函数,里面调用相应的类,暴露给调用方(托管语言)的只有一系列的函数接口(API)。
使用PInvoke可以封送大部分的操作,但是对于复杂的操作处理起来就非常麻烦,同时无法处理异常(无法获取原来异常的真实信息)。

PInvoke的过程:
1. 获取非托管函数的信息(查看dll的内容或者头文件,得到API)
2. 在托管代码中声明非托管函数,设置PInvoke的属性(如函数入口点)
3. 在托管代码中直接调用上一步声明的函数

既然是调用函数,少不了的就是参数传递。但由于托管语言是基于CLR的,而非托管语言则是本机代码(Native code),两者存在很大的差别,如数据类型不一致。这时候,在托管语言这一方,需要进行数据封送处理。封送指的就是托管内存和非托管内存之间传递数据的过程。
封送是双向的,由封送拆收器完成,其主要任务是:
1. 数据类型转换。非托管数据类型到托管数据类型的相互转换(输出),或者,托管数据类型到非托管数据类型的转换(输入);
2. 内存搬运。非托管内存复制到托管内存,或者,托管内存复制到非托管内存;
3. 内存释放。

基本数据类型的异同

C++ C# 长度
short short 2Bytes
int int 4Bytes
long int 4Bytes
bool bool 1Byte
char(Ascii码字符) byte 1Byte
wchar_t(Unicode字符,该类型与C#中的Char兼容) char 2Bytes
float float 4Bytes
double double 8Bytes

API与C#的数据类型对应关系表

API数据类型 类型描述 C#类型 API数据类型 类型描述 C#类型\
WORD 16位无符号整数 ushort CHAR 字符 char
LONG 32位无符号整数 int DWORDLONG 64位长整数 long
DWORD 32位无符号整数 uint HDC 设备描述表句柄 int
HANDLE 句柄,32位整数 int HGDIOBJ GDI对象句柄 int
UINT 32位无符号整数 uint HINSTANCE 实例句柄 int
BOOL 32位布尔型整数 bool HWM 窗口句柄 int
LPSTR 指向字符的32位指针 string HPARAM 32位消息参数 int
LPCSTR 指向常字符的32位指针 String LPARAM 32位消息参数 int
BYTE 字节 byte WPARAM 32位消息参数 int

创建c++的Win32DLL项目

这里写图片描述
1. 新建c++控制台项目,Application type选择DLL,勾选”Export symbols”导出符号。
2. 添加需要导出的代码API。
- 代码中定义了一个名为TESTCPPDLL_API的宏,意思是将后面修饰的内容定义为DLL中要导出的内容。
- EXTERN_C,是在winnt.h中定义的宏,等同于在函数前面添加extern C,意思是该函数在编译和连接时使用C语言的方式,以保证函数名字不变。
3. 在编译C++DLL之前,需要做以下配置,在项目属性对话框中选择”C/C++”|”Advanced”,将Compile AS 选项的值改为”C++”。然后确定,并编译。
4. 添加C#的应用程序,如果要在C#中调用C++的DLL文件,先要在C#的类中添加一个静态方法,并且使用DllImportAttribute对该方法进行修饰,代码如下所示:

    [DllImport(@"TestCPPDLL.dll", EntryPoint = "Add")]
    extern static int Add(int a, int b);

各种类型的数据封送

基本值类型

本例Add方法中传递的是数值类型(int),其他的数据类型,如float,double,和bool类型的传递方式是一样的

[DllImport(@"TestCPPDLL.dll", EntryPoint = "Add")]
extern static int Add(int a, int b);

static void Main(string[] args)
{
    int c = Add(1,2);
    Console.WriteLine(c);
    Console.Read();
}

字符串

c++的定义:

EXTERN_C TESTCPPDLL_API void __stdcall SayHelloWorld(wchar_t*content);

//这里的参数是wchar_t类型的指针,对应着C#中的char类型。TestCPPDLL.cpp中添加如下代码:

TESTCPPDLL_API void __stdcall SayHelloWorld(wchar_t*content)
{
    cout<<content;
}

该代码的功能就是将输入的字符串通过C++在控制台上输出。下面是在C#中的声明:

[DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "SayHelloWorld")]
extern static void SayHello([MarshalAs(UnmanagedType.LPTStr)]string c); //指定了EntryPoint,可以修改函数名防止重名

//调用过程如下所示:
    SayHello("Hello");

指针

我们可以直接在方法中传递指针,但是要注意的是我们常常需要将数组的指针(数据入口地址,第一个元素的地址),数据从C/C++到C#时问题不大,但是如果从C#到C/C++时一定要将数组先固化,然后再传递处理。
c++的定义:

//传入一个整型指针,将其所指向的内容加1
EXTERN_C TESTCPPDLL_API void __stdcall AddInt(int *i);

//传入一个整型数组的指针以及数组长度,遍历每一个元素并且输出
EXTERN_C TESTCPPDLL_API void __stdcall AddIntArray(int *firstElement,int arraylength);

//在C++中生成一个整型数组,并且数组指针返回给C#
EXTERN_C TESTCPPDLL_API int* __stdcall GetArrayFromCPP();

//TestCPPDLL.cpp中,代码如下所示:

TESTCPPDLL_API void __stdcall AddInt(int *i)
{
    (*i)++;
}

TESTCPPDLL_API void __stdcall AddIntArray(int *firstElement,int arrayLength)
{
    int*currentPointer=firstElement;
    for (int i = 0; i < arrayLength; i++)
    {
        cout<<*currentPointer;
        currentPointer++;
    }
    cout<<endl;
}

int *arrPtr;
TESTCPPDLL_API int* __stdcall GetArrayFromCPP()
{
    arrPtr=new int[10];

    for (int i = 0; i < 10; i++)
    {
        arrPtr[i]=i;
    }
    return arrPtr;
}

对应调用的C#代码如下所示:

[DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "AddInt")]
extern static void AddInt(ref int i);

[DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "AddIntArray")]
extern static void AddIntArray(ref int firstElement, int arraylength);

[DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "GetArrayFromCPP")]
extern static IntPtr GetArrayFromCPP();


//调用过程如下所示:

int i = 10;
AddInt(ref i);
Console.Write("\t调用AddInt(int *i), i=10, => " + i);
Console.WriteLine("");


Console.Write("\t调用AddIntArray(int *first, int len),将C#中的数据传递到C++中,并在C++中输出 => ");
int[] CSArray = new int[10];
for (int iArr = 0; iArr < 10; iArr++)
{
    CSArray[iArr] = iArr;
}
AddIntArray(ref CSArray[0], 10);
Console.WriteLine("");


Console.Write("\t调用GetArrayFromCPP(),获取一个C++中建立的数组 => ");
IntPtr pArrayPointer = GetArrayFromCPP();
for (int iArr = 0; iArr < 10; iArr++)
{
    Console.Write(Marshal.PtrToStructure(pArrayPointer, typeof(int)).ToString());
    pArrayPointer += sizeof(int);
}

函数指针

C#中并没有函数指针的概念,但是可以使用委托(delegate)来代替函数指针。
大家可能会问,为什么要传递函数指针呢?利用PInvoke可以实现C#对C/C++函数的调用,反过来,我们能不能在C/C++程序运行的某一时刻,来调用一个C#对应的函数呢?(例如在C++中存在一个独立线程,该线程可能在任意时刻触发一个事件,并且需要通知C#)。这个时候,我们就有必要将一个C#中已经指向某一个函数的函数指针(委托)传递给C++。

想要传递函数指针,首先要在C#中定义一个委托,并且在C++中定义一个函数指针,同时要保证委托和函数指针具备相同的函数原型。
c++的定义:

//定义一个函数指针
typedef void(__stdcall *CPPCallback)(int tick);
//定义一个用于设置函数指针的方法,并在该函数中调用C#中传递过来的委托
EXTERN_C TESTCPPDLL_API void __stdcall SetCallback(CPPCallback callback);


TESTCPPDLL_API void __stdcall SetCallback(CPPCallback callback)
{
    int tick = 100;

    //下面的代码是对C#中委托进行调用
    callback(tick);
}

对应的c#代码:

//定义一个委托,返回值为空,存在一个整型参数
public delegate void CSCallback(int tick);

//定义一个用于回调的方法,与前面定义的委托的原型一样,该方法会被C++所调用
static void CSCallbackFunction(int tick)
{
    Console.WriteLine(tick.ToString());
}

//定义一个委托类型的实例,在主程序中该委托实例将指向前面定义的CSCallbackFunction方法
static CSCallback callback;

//这里使用CSCallback委托类型来兼容C++里的CPPCallback函数指针
[DllImport("TestCPPDLL.dll", EntryPoint = "SetCallback")]
extern static void SetCallback(CSCallback callback);


//调用过程如下所示:

    //让委托指向将被回调的方法
    callback = CSCallbackFunction;
    //将委托传递给C++
    SetCallback(callback);

枚举

以MessageBeep()为例。MSDN 给出了以下原型:
c++的定义:

BOOL MessageBeep(
 UINT uType // 声音类型
); 

这看起来很简单,但是从注释中可以发现两个有趣的事实。
首先,uType 参数实际上接受一组预先定义的常量。
其次,可能的参数值包括 -1,这意味着尽管它被定义为 uint 类型,但 int 会更加适合。对于 uType 参数,使用 enum 类型是合乎情理的。
对应的c#代码:

public enum BeepType
{ 
  SimpleBeep = -1, 
  IconAsterisk = 0x00000040, 
  IconExclamation = 0x00000030, 
  IconHand = 0x00000010, 
  IconQuestion = 0x00000020, 
  Ok = 0x00000000, 
} 
[DllImport("user32.dll")] 
public static extern bool MessageBeep(BeepType beepType); 


//调用过程如下所示:
    MessageBeep(BeepType.IconQuestion);   

如果常量为其他类型(非int),则需要修改枚举类型的基本类型
enum Name : Type {…}

结构体

结构体的数据封送,就是API以结构体作为输入参数或输出参数。

对于含有结构体参数的API,必须先在C++和C#中分别定义一个等价的结构体;
等价的含义是,除了字段的名称可以不一样以外,以下内容必须保持一致:
1. 字段声明顺序;要保证该功能,需要将C#结构体标记为[StructLayout( LayoutKind.Sequential)]
2. 字段的类型;
3. 字段在内存中的大小。

在定义托管结构体时,可能需要使用StructLayout属性来指定对象中的内存布局。
正确地声明托管结构体,是封送的关键。

带有传出参数的API,要使用指针传递参数,在C#里要声明ref

c++的定义:

typedef struct _DEMOSTRUCT
{
    int a;
    short b;
    float c;
    double d;
}DEMOSTRUCT, *pDEMOSTRUCT;

EXTERN_C TESTCPPDLL_API void __stdcall Func(pDEMOSTRUCT p_demoStruct);


TESTCPPDLL_API void __stdcall Func(pDEMOSTRUCT p_demoStruct)
{
    printf("got struct in cpp, int = %d, short = %d,   float = $f, double = %f \n",
        p_demoStruct->a,
        p_demoStruct->b,
        p_demoStruct->c,
        p_demoStruct->d
    );

    p_demoStruct->a += 10;
}

对应的c#代码:

private struct ManagedDemoStruct
        {
            public int a;
            public short b;
            public float c;
            public double d;
        }
        [DllImport("TestCPPDLL.dll", EntryPoint = "Func")]
        private extern static void myFunc(ref ManagedDemoStruct argStruct); //使用指针传递参数要声明ref



//调用过程如下所示:

            ManagedDemoStruct demoStruct = new ManagedDemoStruct();
            demoStruct.a = 10;
            demoStruct.b = 20;
            demoStruct.c = 3.5f;
            demoStruct.d = 6.8f;
            myFunc(ref demoStruct);

内嵌指针的结构体

c++的定义:

struct CXTest
{
    LPBYTE pData;     // 一个指向byte数组的指针
    int nLen;         // 数组的长度
}
BOOL WINAPI XFunction(const CXTest &inData_, CXTest &outData_);

对应c#代码:

struct CXTest
{
    public IntPrt pData;
    public int nLen;
};
static extern bool XFunction(ref [In] CXTest inData_, ref CXTest outData_);


//调用过程如下所示:

//设数组长度为nDataLen
CXTest stIn = new CXTest(), stOut = new CXTest();
byte[] pIn = new byte[nDataLen];
// 为数组赋值
stIn.pData = Marshal.AllocHGlobal(nDataLen);
Marshal.Copy(pIn, 0, stIn.pData, nDataLen);
stIn.nLen = nDataLen;
stOut.pData = Marshal.AllocHGlobal(nDataLen);
stOut.nLen = nDataLen;
XFunction(ref stIn, ref stOut);
byte[] pOut = new byte[nDataLen];
Marshal.Copy(stOut.pData, pOut, 0, nDataLen);
// ....
Marshal.FreeHGlobal(stIn.pData);
Marshal.FreeHGlobal(stOut.pData);

此处最重要的是要注意,pData的内存要先申请,再向里copy数据;还有最后要记得释放申请的内存。

内嵌数组与字符串的结构体

c++下的定义与实现:

struct CXTest 
{
    WCHAR wzName[64];
    int nLen;
    byte byData[100];
};
bool SetTest(const CXTest &stTest_);

在C#下,为了方便初始化byte数组,我们使用类来代替结构

[StructLayout(LayoutKind.Sequential, Pack=2, CharSet=CharSet.Unicode)] 
class CXTest
{
    public void Init()
    {
        strName = "";
        nLen = 0;
        byData = new byte[100];
    }
    [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
    public string strName;
    public int nLen;
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 100)]
    public byte[] byData;
}
extern extern bool SetTest(CXTest stTest_);

定义后,虽然为byData预留的空间,但是其指向null,不能为其复制。由于结构体不能自定义缺省参数,所以增加一个Init函数或通过类来替换来初始化byData。
从底层接口中获取数据一定要使用struct,且从底层接口中(out)获取数据后,byData就自动指向了实际的内容了。向底层接口中设定数据时,如果使用struct一定要先调用init,并且通过ref方式;如果是类,则不能使用ref修饰(C#中:类默认放在堆中,结构体默认放在栈中的)。

结构体数组

c++的定义:

struct UIM_BOOK_STRUCT   
{     
   int UimIndex;      
   char szName[15];      
   char szPhone[21];   
};

int ReadUimAllBook(UIM_BOOK_STRUCT lpUimBookItem[],int nMaxArraySize);

对应c#代码:

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]//可以指定编码类型   
public struct UIM_BOOK_STRUCT   
{     
   public int UimIndex;      
   [MarshalAs(UnmanagedType.ByValTStr, SizeConst= 15)]
   public string szName;      
   [MarshalAs(UnmanagedType.ByValTStr, SizeConst= 21)]
   public string szPhone;   
};   

[DllImport( "CdmaCard.dll",EntryPoint="ReadUimAllBook")]   
public static extern int ReadUimAllBook([Out] UIM_BOOK_STRUCT [] lpUimBookItem,int nMaxArraySize);   
UIM_BOOK_STRUCT[] p = new UIM_BOOK_STRUCT[20]; int ret = ReadUimAllBook(p,p.Length); 

字符串与字符串缓冲区

在 Win32 中还有两种不同的字符串表示:ANSI、Unicode。由于 P/Invoke 的设计者不想让您为所在的平台操心,因此他们提供了内置的支持来自动使用 A 或 W 版本。如果您调用的函数不存在,互操作层将为您查找并使用 A 或 W 版本。但是互操作的默认字符类型是 Ansi 或单字节,如果非托管代码为宽字符,则需要明确的把CharSet设为CharSet.Unicode。
.NET 中的字符串类型是不可改变的类型,这意味着它的值将永远保持不变。对于要将字符串值复制到字符串缓冲区的函数,字符串将无效。这样做至少会破坏由封送拆收器在转换字符串时创建的临时缓冲区;严重时会破坏托管堆,而这通常会导致错误的发生。无论哪种情况都不可能获得正确的返回值。
要解决此问题,我们需要使用其他类型。StringBuilder 类型就是被设计为用作缓冲区的,我们将使用它来代替字符串。下面是一个示例:
C++函数声明:

DWORD GetShortPathName(
  LPCTSTR lpszLongPath,
  LPTSTR lpszShortPath,
  DWORD cchBuffer
); 

C#中封装

[DllImport("kernel32.dll", CharSet = CharSet.Auto)] 
public static extern int GetShortPathName( 
  [MarshalAs(UnmanagedType.LPTStr)] 
  string path, 
  [MarshalAs(UnmanagedType.LPTStr)] 
  StringBuilder shortPath, 
  int shortPathLength);   


//调用过程如下所示:

StringBuilder shortPath = new StringBuilder(80); 
int result = GetShortPathName(@"d:\dest.jpg", shortPath, shortPath.Capacity); 
string s = shortPath.ToString(); 
//请注意,StringBuilder 的 Capacity 传递的是缓冲区大小。 

完整代码

TestCPPDLL.h

// 下列 ifdef 块是创建使从 DLL 导出更简单的
// 宏的标准方法。此 DLL 中的所有文件都是用命令行上定义的 TESTCPPDLL_EXPORTS
// 符号编译的。在使用此 DLL 的
// 任何其他项目上不应定义此符号。这样,源文件中包含此文件的任何其他项目都会将
// TESTCPPDLL_API 函数视为是从 DLL 导入的,而此 DLL 则将用此宏定义的
// 符号视为是被导出的。
#ifdef TESTCPPDLL_EXPORTS
#define TESTCPPDLL_API __declspec(dllexport)
#else
#define TESTCPPDLL_API __declspec(dllimport)
#endif

// 此类是从 TestCPPDLL.dll 导出的
class TESTCPPDLL_API CTestCPPDLL {
public:
    CTestCPPDLL(void);
};

EXTERN_C TESTCPPDLL_API int __stdcall Add(int a, int b);
EXTERN_C TESTCPPDLL_API void __stdcall SayHelloWorld(wchar_t*content);

//传入一个整型指针,将其所指向的内容加1
EXTERN_C TESTCPPDLL_API void __stdcall AddInt(int *i);

//传入一个整型数组的指针以及数组长度,遍历每一个元素并且输出
EXTERN_C TESTCPPDLL_API void __stdcall AddIntArray(int *firstElement, int arraylength);

//在C++中生成一个整型数组,并且数组指针返回给C#
EXTERN_C TESTCPPDLL_API int* __stdcall GetArrayFromCPP();

//定义一个函数指针
typedef void(__stdcall *CPPCallback)(int tick);
//定义一个用于设置函数指针的方法,并在该函数中调用C#中传递过来的委托
EXTERN_C TESTCPPDLL_API void __stdcall SetCallback(CPPCallback callback);

//定义一个枚举
enum mEnumType
{
    SimpleBeep = -1,
    IconAsterisk = 0x00000040,
    IconExclamation = 0x00000030,
    IconHand = 0x00000010,
    IconQuestion = 0x00000020,
    Ok = 0x00000000,
};
EXTERN_C TESTCPPDLL_API bool __stdcall SendEnumFromCSToCPP(mEnumType mtype);


//定义一个结构体
struct Vector3
{
    float X, Y, Z;
};
EXTERN_C TESTCPPDLL_API void __stdcall SendStructFromCSToCPP(Vector3 vector);

typedef struct _DEMOSTRUCT
{
    int a;
    short b;
    float c;
    double d;
}DEMOSTRUCT, *pDEMOSTRUCT;

EXTERN_C TESTCPPDLL_API void __stdcall Func(pDEMOSTRUCT p_demoStruct);

//内嵌指针的结构体
struct CXTest
{
    LPBYTE pData;     // 一个指向byte数组的指针
    int nLen;         // 数组的长度
};
EXTERN_C TESTCPPDLL_API bool XFunction(const CXTest &inData_, CXTest &outData_);

struct CXTest2
{
    WCHAR wzName[64];
    int nLen;
    BYTE byData[100];
};
EXTERN_C TESTCPPDLL_API bool SetTest(const CXTest2 &stTest_);

TestCPPDLL.cpp

// TestCPPDLL.cpp : 定义 DLL 应用程序的导出函数。
#include "stdafx.h"
#include "TestCPPDLL.h"
#include <iostream>

using namespace std;

TESTCPPDLL_API int __stdcall Add(int a, int b)
{
    return a + b;
}

TESTCPPDLL_API void __stdcall SayHelloWorld(wchar_t*content)
{
    wprintf(content);
    printf("\n");
}

TESTCPPDLL_API void __stdcall AddInt(int *i)
{
    (*i)++;
}

TESTCPPDLL_API void __stdcall AddIntArray(int *firstElement, int arrayLength)
{
    int*currentPointer = firstElement;

    for (int i = 0; i < arrayLength; i++)
    {
        cout << *currentPointer;

        currentPointer++;
    }

    cout << endl;
}

int *arrPtr;
TESTCPPDLL_API int* __stdcall GetArrayFromCPP()
{
    arrPtr = new int[10];

    for (int i = 0; i < 10; i++)
    {
        arrPtr[i] = i;
    }

    return arrPtr;
}

TESTCPPDLL_API void __stdcall SetCallback(CPPCallback callback)
{
    int tick = 100;

    //下面的代码是对C#中委托进行调用
    callback(tick);
}

TESTCPPDLL_API bool __stdcall SendEnumFromCSToCPP(mEnumType mtype) {
    cout << "got enum in cpp, " << mtype;
    return true;
}


TESTCPPDLL_API void __stdcall SendStructFromCSToCPP(Vector3 vector)
{
    cout << "got vector3 in cpp,x:" << vector.X << ",Y:" << vector.Y << ",Z:" << vector.Z;
}

TESTCPPDLL_API void __stdcall Func(pDEMOSTRUCT p_demoStruct)
{
    printf("got struct in cpp, int = %d, short = %d,   float = $f, double = %f \n",
        p_demoStruct->a,
        p_demoStruct->b,
        p_demoStruct->c,
        p_demoStruct->d
    );

    p_demoStruct->a += 10;
}

TESTCPPDLL_API bool __stdcall XFunction(const CXTest &inData_, CXTest &outData_) {
    if (outData_.nLen > 0) {
        for (int i = 0; i < outData_.nLen; i++)
        {
            outData_.pData[i] = 65 + i;
            cout << outData_.pData[i];
        }
    }
    return true;
}

TESTCPPDLL_API bool __stdcall SetTest(const CXTest2 &stTest_) {

    return true;
}


// 这是已导出类的构造函数。
// 有关类定义的信息,请参阅 TestCPPDLL.h
CTestCPPDLL::CTestCPPDLL()
{
    cout << "This is CTestCPPDLL Class.";
    return;
}

C#侧调用文件 Program.cs

using System;
using System.Text;
using System.Runtime.InteropServices;

namespace TestCSharpProgram
{
    class Program
    {

        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "Add")]
        extern static int Add(int a, int b);

        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "SayHelloWorld")]
        extern static void SayHello([MarshalAs(UnmanagedType.LPTStr)]string c); //指定了EntryPoint,可以修改函数名防止重名

        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "AddInt")]
        extern static void AddInt(ref int i);

        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "AddIntArray")]
        extern static void AddIntArray(ref int firstElement, int arraylength);

        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "GetArrayFromCPP")]
        extern static IntPtr GetArrayFromCPP();




        //定义一个委托,返回值为空,存在一个整型参数
        public delegate void CSCallback(int tick);

        //定义一个用于回调的方法,与前面定义的委托的原型一样,该方法会被C++所调用
        static void CSCallbackFunction(int tick)
        {
            Console.WriteLine(tick.ToString());
        }

        //定义一个委托类型的实例,在主程序中该委托实例将指向前面定义的CSCallbackFunction方法
        static CSCallback callback;


        //这里使用CSCallback委托类型来兼容C++里的CPPCallback函数指针
        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "SetCallback")]
        extern static void SetCallback(CSCallback callback);


        //定义一个枚举
        public enum mEnumType
        {
            SimpleBeep = -1,
            IconAsterisk = 0x00000040,
            IconExclamation = 0x00000030,
            IconHand = 0x00000010,
            IconQuestion = 0x00000020,
            Ok = 0x00000000,
        };
        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "SendEnumFromCSToCPP")]
        public static extern bool SendEnumFromCSToCPP(mEnumType mType);



        [StructLayout(LayoutKind.Sequential)]
        struct Vector3
        {
            public float X, Y, Z;
        }

        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "SendStructFromCSToCPP")]
        extern static void SendStructFromCSToCPP(Vector3 vector);

        private struct ManagedDemoStruct
        {
            public int a;
            public short b;
            public float c;
            public double d;
        }
        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "Func")]
        private extern static void myFunc(ref ManagedDemoStruct argStruct); //使用指针传递参数要声明ref


        struct CXTest
        {
            public IntPtr pData;
            public int nLen;
        }
        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "XFunction")]
        static extern bool XFunction(ref CXTest inData_, ref CXTest outData_);


        [StructLayout(LayoutKind.Sequential, Pack = 2, CharSet = CharSet.Unicode)]
        class CXTest2
        {
            public void Init()
            {
                strName = "";
                nLen = 0;
                byData = new byte[100];
            }
            [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 64)]
            public string strName;
            public int nLen;
            [MarshalAs(UnmanagedType.ByValArray, SizeConst = 100)]
            public byte[] byData;
        }
        [DllImport(@"D:\work\Interop\DLLDir\TestCPPDLL.dll", EntryPoint = "SetTest")]
        static extern bool SetTest(CXTest2 stTest_);



        static void Main(string[] args)
        {
            Console.WriteLine("以下是C#调用C++相关API的结果返回:\r\n");
            Console.WriteLine("1.基本值类型的数据封送");
            Console.WriteLine("\t调用Add(int a,int b),  Add(1,2) => " + Add(1, 2));


            Console.WriteLine("\n2.字符串的数据封送");
            Console.Write("\t调用SayHello(wchar_t* content), => ");
            SayHello("Hello");


            Console.WriteLine("\n3.指针的数据封送");
            Console.WriteLine("3.1 整形指针");
            int i = 10;
            AddInt(ref i);
            Console.Write("\t调用AddInt(int *i), i=10, => " + i);
            Console.WriteLine("");


            Console.WriteLine("\n3.2 整形数组指针[IN]");
            Console.Write("\t调用AddIntArray(int *first, int len),将C#中的数据传递到C++中,并在C++中输出 => ");
            int[] CSArray = new int[10];
            for (int iArr = 0; iArr < 10; iArr++)
            {
                CSArray[iArr] = iArr;
            }
            AddIntArray(ref CSArray[0], 10);


            Console.WriteLine("\n3.3 整形数组指针[OUT]");
            Console.Write("\t调用GetArrayFromCPP(),获取一个C++中建立的数组 => ");
            IntPtr pArrayPointer = GetArrayFromCPP();
            for (int iArr = 0; iArr < 10; iArr++)
            {
                Console.Write(Marshal.PtrToStructure(pArrayPointer, typeof(int)).ToString());
                pArrayPointer += sizeof(int);
            }
            Console.WriteLine("");


            Console.WriteLine("\n4. 函数指针的数据封送");
            Console.Write("\t调用SetCallback(CPPCallback callback),将委托传递给C++ => ");
            //让委托指向将被回调的方法
            callback = CSCallbackFunction;
            //将委托传递给C++
            SetCallback(callback);


            Console.WriteLine("\n5. 枚举的数据封送");
            Console.Write("\t调用SendEnumFromCSToCPP(mEnumType mtype),将枚举传递给C++ => ");
            SendEnumFromCSToCPP(mEnumType.IconHand);
            Console.WriteLine("");


            Console.WriteLine("\n6. 结构体的数据封送");
            Console.WriteLine("6.1 Vector3");
            Console.Write("\t调用SendStructFromCSToCPP(Vector3 vector),将vector传递给C++并在C++中输出 => ");
            //建立一个Vector3的实例
            Vector3 vector = new Vector3() { X = 10, Y = 20, Z = 30 };
            //将vector传递给C++并在C++中输出
            SendStructFromCSToCPP(vector);
            Console.WriteLine("");


            Console.WriteLine("\n6.2 结构体指针");
            Console.Write("\t调用Func(pDEMOSTRUCT p_demoStruct),将结构体传递给C++并在C++中处理后输出 => ");
            ManagedDemoStruct demoStruct = new ManagedDemoStruct();
            demoStruct.a = 10;
            demoStruct.b = 20;
            demoStruct.c = 3.5f;
            demoStruct.d = 6.8f;
            myFunc(ref demoStruct);


            Console.WriteLine("\n6.3 内嵌指针的结构体");
            Console.Write("\t调用XFunction(const CXTest &inData_, CXTest &outData_),将结构体传递给C++并在C++中处理后输出 => ");
            int nDataLen = 5;//数组长度
            CXTest stIn = new CXTest(), stOut = new CXTest();
            byte[] pIn = new byte[nDataLen];
            // 为数组赋值
            stIn.pData = Marshal.AllocHGlobal(nDataLen);
            Marshal.Copy(pIn, 0, stIn.pData, nDataLen);
            stIn.nLen = nDataLen;
            stOut.pData = Marshal.AllocHGlobal(nDataLen);
            stOut.nLen = nDataLen;
            XFunction(ref stIn, ref stOut);
            byte[] pOut = new byte[nDataLen];
            Marshal.Copy(stOut.pData, pOut, 0, nDataLen);
            // ....
            Marshal.FreeHGlobal(stIn.pData);
            Marshal.FreeHGlobal(stOut.pData);
            //pData的内存要先申请,再向里copy数据;还有最后要记得释放申请的内存。
            Console.WriteLine("");


            Console.Read();

        }

    }
}

属性的其他选项

DLLImport 和 StructLayout 属性具有一些非常有用的选项,有助于 P/Invoke 的使用。另外返回值可以Return属性进行修饰。

  • DLL Import 属性
    除了指出宿主 DLL 外,DllImportAttribute 还包含了一些可选属性,其中四个特别有趣:EntryPoint、CharSet、SetLastError 和 CallingConvention。
    • EntryPoint:在不希望外部托管方法具有与 DLL 导出相同的名称的情况下,可以设置该属性来指示导出的 DLL 函数的入口点名称。当您定义两个调用相同非托管函数的外部方法时,这特别有用。
    • CharSet:如果 DLL 函数不以任何方式处理文本,则可以忽略 DllImportAttribute 的 CharSet 属性。然而,当 Char 或 String 数据是等式的一部分时,应该将 CharSet 属性设置为 CharSet.Auto。这样可以使 CLR 根据宿主 OS 使用适当的字符集。如果没有显式地设置 CharSet 属性,则其默认值为 CharSet.Ansi。
    • SetLastError:设为true后,会导致 CLR 在每次调用外部方法之后缓存由 API 函数设置的错误。然后,在包装方法中,可以通过调用System.Runtime.InteropServices.Marshal.GetLastWin32Error 方法来获取缓存的错误值。然后检查这些期望来自 API 函数的错误值,并为这些值引发一个可感知的异常。对于其他所有失败情况(包括根本就没意料到的失败情况),则引发在 System.ComponentModel.Win32Exception异常,并将 Marshal.GetLastWin32Error 返回的值传递给它。
    • CallingConvention :通过此属性,可以给 CLR 指示应该将哪种函数调用约定用于堆栈中的参数。CallingConvention.Winapi 的默认值是最好的选择,它在大多数情况下都可行。然而,如果该调用不起作用,则可以检查 Platform SDK 中的声明头文件,看看您调用的 API 函数是否是一个不符合调用约定标准的异常 API。
  • StructLayout 属性
    • LayoutKind:结构在默认情况下按顺序布局,并且在多数情况下都适用。如果需要完全控制结构成员所放置的位置,可以使用 LayoutKind.Explicit,然后为每个结构成员添加 FieldOffset 属性。当您需要创建 union 时,通常需要这样做。
    • CharSet:控制 ByValTStr 成员的默认字符类型。
    • Pack:设置结构的压缩大小。它控制结构的排列方式。如果 C 结构采用了其他压缩方式,您可能需要设置此属性。
    • Size:设置结构大小。不常用;但是如果需要在结构末尾分配额外的空间,则可能会用到此属性。
  • 返回值
    返回值可修改返回的类型,一般都是bool类型需要处理。
    [DllImport(“user32.dll”, SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    private static extern bool GetLastInputInfo(ref XLastInputInfo stInfo_)

注意事项

  • 在被导出的函数前面一定要添加extern “C”来指明导出函数的时候使用C语言方式编译和连接,这样保证函数定义的名字和导出的名字相同,否则如果默认按C++方式导出,那个函数的名字就会变得乱七八糟,我们的程序就无法找到入口点了。
  • 在Mono中,需要设置Unity-ProjectSetting-Player-OtherSetting,勾选Allow ‘unsafe’ Code。
  • DLL放到Plugins下,直接访问[DllImport(“TestCPPDLL”)]
  • 如果您的 P/Invoke 调用失败,通常是因为某些类型的定义不正确。以下是几个常见问题:
    • long != long。在 C++ 中,long 是 4 字节的整数,但在 C# 中,它是 8 字节的整数。
    • 字符串类型设置不正确。

相关工具及文档

P/Invoke Interop Assistant工具

支持托管代码和非托管代码之间的方法签名的转换,而且直接生成相关的C#或者是VB的方法调用代码。
下载链接

  • 自动生成Native函数或者结构在.NET程序中的声明,切换到“SigImp Translate Snippet”标签,然后将Native函数或者结构的声明拷贝到“Native Code Snippet”文本框里面,然后选中“Auto Generate”对话框,点击“Generate”就可以获取对应的.NET声明,如下图所示:
    这里写图片描述
  • 查找Win32 API中在.NET中的声明,选择“SigImp Search”,并在“Name”文本框里面输入你要查找的函数或者结构名称就可以了,如下图所示:
    这里写图片描述
  • 验证或者生成.NET函数(或结构)在C 中的声明,切换到“SigExp”并且打开一个包含P/Invoke函数调用的.NET Assembly就可以了,这个程序会显示对应的C的声明,并且告诉你C#声明编写错误的地方

PInvoke.net Visual Studio Extension

常用DLL的API可以查看这个工具:下载地址
或者搜索网站:https://www.pinvoke.net/

MSDN文档

在 C++ 中使用显式 PInvoke(DllImport 特性)


C#通过C++/CLI调用C++的DLL

对于非常复杂的结构,通过P/Invoke还是很难处理的,这是可考虑使用C++ Inerop来处理。
实现起来比较简单直观,并且可以实现C#调用C++所写的类,但是问题是MONO构架不支持C++/CLI功能,因此无法实现脱离.NET Framework跨平台运行。
暂不讨论。
ref:C#/C++/CLI运行效率测试之一: C#通过CLR/C++调用Native CPP 类



C++调用C#.

C++中通过C++/CLI调用.NET编写的DLL

通过C++/CLI对.Net DLL的接口加了一层包装,然后由非托管C++调用封装好的dll。

主要步骤示例

1.创建c#类库项目 TestCSharpDLL,生成dll

using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace TestCSharpDLL
{
    public class CsharpClass
    {
        public int DoTesting(int x, int y, string testing, out string error)
        {
            error = testing + " -> testing is ok.";
            return x + y;
        }
    }
}

2.创建C++/CLI包装类库 CsharpWrap,添加对TestCSharpDLL的引用,定义需要导出的API,项目输出 TestCSharpDLL.dll和CsharpWrap.dll
这里写图片描述
CsharpWrap.h

#pragma once

#include <windows.h>  
#include <string> 

using namespace System;
using namespace TestCSharpDLL; //引用C#的命名空间
using namespace std;
using namespace Runtime::InteropServices;

namespace CsharpWrap {

    public ref class Class1
    {
        // TODO:  在此处添加此类的方法。
    };
}

CsharpWrap.cpp

// 这是主 DLL 文件。
#include "stdafx.h"
#include "CsharpWrap.h"

extern "C" _declspec(dllexport) int DoTesting(int x, int y, char* testing, char* error)
{
    try
    {
        CsharpClass ^generator = gcnew CsharpClass();
        String^ strTesting = gcnew String(testing);
        String^ strError;

        int sum = generator->DoTesting(x, y, strTesting, strError);

        if (strError != nullptr)
        {
            char* cError =
                (char*)(Marshal::StringToHGlobalAnsi(strError)).ToPointer();
            memcpy(error, cError, strlen(cError) + 1);
        }
        else
        {
            error = nullptr;
        }

        return sum;
    }
    catch (exception e)
    {
        memcpy(error, e.what(), strlen(e.what()) + 1);
        return -1;
    }
}

3.创建非托管C++项目TestCPPConsole,LoadLibrary(CsharpWrap.dll)来调用相应API
需要将TestCSharpDLL.dll和CsharpWrap.dll放在进程EXE文件同一目录下

#include "stdafx.h"
#include <windows.h>

typedef int(*pfunc)(int, int, char*, char*);

int main()
{
    HINSTANCE hInst = LoadLibrary(_T("CsharpWrap.dll"));
    if (hInst)
    {
        pfunc DoTesting = (pfunc)GetProcAddress(hInst, "DoTesting");

        if (DoTesting)
        {
            char error[100] = { NULL };
            int sum = DoTesting(1, 2, "Hello", error);
            //show testing results
            char strSum[8];
            _itoa_s(sum, strSum, 16);
            ::MessageBoxA(NULL, error, strSum, MB_OK);
        }
        else
        {
            ::MessageBoxA(NULL, "Get function fail.", "Fail", MB_OK);
        }
        //free library
        FreeLibrary(hInst);
        hInst = nullptr;
    }
    else
    {
        FreeLibrary(hInst);
    }

    return 0;
}

这里写图片描述

C++/CLI规则

相关文档

欧盟ECMA标准文档:C++/CLI Language Specification
MSDN上的相关文档:https://msdn.microsoft.com/zh-cn/library/ms235289.aspx
使用 C++/CLI (Visual C++) 进行 .NET 编程

简介

C++/CLI(CLI:Common Language Infrastructure)是一门用来代替C++托管扩展的新的语言规范。重新简化了C++托管扩展的语法,提供了更好的代码可读性。
我们可以使用C++/CLI搭建C++和.Net之间的桥梁,C++/CLI是一个比较有意思的两栖模块,它具有如下特点
1. 既可以访问.Net类库,也可以访问C++原生类库
2. 既可以被.Net程序引用,也可以被C++原生程序引用

通过上面的代码示例,我们可以简单的管中窥豹的看看C++/CLI是在C++的基础上扩充了一套语法,使其具有访问.Net原始的功能,这里用到的有:

  • 使用ref class声明CLI引用类型(C#中的class)
  • 使用^(例如如这里的String ^)来定义CLI引用类型
  • 使用gcnew创建CLI的引用类型

基本语法

托管对象的创建和引用

c#代码:

System.Object x = new System.Object();

c++代码:

P* x = new P();

其在C++/CLI中的等价代码:

System::Object^ x = gcnew System::Object();

我们不难发现,对于托管对象,主要引入了如下两个语法:
1. 用gcnew代替new实现托管对象的创建
2. 用^代替*实现托管对象的指针
这种方式创建的对象是可以直接被CLR支持的,可以在C#中使用。

托管对象指针使用的方式和传统的对象指针还是比较类似的,直接使用->即可:

    System::Object^ x = gcnew System::Object();
    auto str = x->ToString();
托管类型的定义

在CLR中,托管类型是分为引用类型(class)和值类型(struct)的,在C++/CLI中的分别定义方式如下:

引用类型:

    public ref class MyClass
    {
    };

值类型:

    public value class MyClass
    {
    };

在ISO C++中类定义中加上了ref或value标记为托管类型,还算比较容易使用。

枚举

枚举的定义和C++11的enum class一样,它像数字那样可以同时应用于托管类型和非托管类型。

public enum class SomeColors { Red, Yellow, Blue };

或者更精确的表示:

public enum class SomeColors : char { Red, Yellow, Blue };
数组

C++/CLI中新增了array ^的方式定义数组。

array<int> ^a = gcnew array<int>(100) { 1, 2, 3 };

或者使用它的完整版:

cli::array<int> ^a = gcnew cli::array<int> {1, 2, 3};
不定参数

对于C#中的不定参数的语法:

void foo(params string[] args)

在C++/CLI中对应的版本为:

void foo(... array<String^>^ args)

基本类型

数值类型

对于基本的数值类型,在C++/CLI中是可以直接映射为托管类型的数值的,可以同时应用于托管类型和非托管类型,编译器会将其自动转换。

基本类型 System命名空间中对应的类 注释/用法
bool System::Boolean bool dirty = false;
char System::SByte char sp = ’ ‘;
signed char System::SByte signed char ch = -1;
unsigned char System::Byte unsigned char ch = ‘\0’;
wchar_t System::Char wchar_t wch = ch;
short System::Int16 short s = ch;
unsigned short System::UInt16 unsigned short s = 0xffff;
int System::Int32 int ival = s;
unsigned int System::UInt32 unsigned int ui = 0xffffffff;
long System::Int32 long lval = ival;
unsigned long System::UInt32 unsigned long ul = ui;
long long System::Int64 long long etime = ui;
unsigned long long System::UInt64 unsigned long long mtime = etime;
float System::Single float f = 3.14f;
double System::Double double d = 3.14159;
long double System::Double long double d = 3.14159L;
字符串

字符串CLI已经内置了:System::String,但C++的常用字符串有char*、wchar_t*、std::string等好多种,编译器提供了char*、wchar_t*到System::String的自动转换:

    System::String^ s = "hello worold";
    System::String^ s2 = L"hello worold";

另外,也可以使用gcnew创建托管字符串:

System::String^ s = gcnew String("hello worold");

但是,对于System::String转char*,系统没有直接的语法支持。方法有很多种,我通常使用如下方式来转换:

    IntPtr ip = Marshal::StringToHGlobalAnsi(str);
    const char* ch = static_cast<const char*>(ip.ToPointer());
    //do something with ch
    Marshal::FreeHGlobal(ip);

注意事项

  • DLL多层嵌套的问题
    如果用LoadLibrary加载DLL失败,可以尝试用LoadLibraryEx,同时保证所依赖的C#DLL放到进程EXE同级目录。
    LoadLibraryEx(“DLL绝对路径”, NULL, LOAD_WITH_ALTERED_SEARCH_PATH);



参考资料:
C#界面,C++核心算法(.NET与C++的交互)
C#与C/C++的交互 - PInvoke部分
C#与C++交互之——参数传递
C#与C++回调交互
.Net调用非托管代码(P/Invoke与C++InterOP)
DLL库类的导出,C#的调用
非托管C++通过C++/CLI包装调用C# DLL
C++通过DLL调用C#代码
用C++/CLI搭建C++和C#之间的桥梁

猜你喜欢

转载自blog.csdn.net/crayon_chen/article/details/80470937