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#长度
shortshort2Bytes
intint4Bytes
longint4Bytes
boolbool1Byte
char(Ascii码字符)byte1Byte
wchar_t(Unicode字符,该类型与C#中的Char兼容)char2Bytes
floatfloat4Bytes
doubledouble8Bytes

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

API数据类型类型描述C#类型API数据类型类型描述C#类型\
WORD16位无符号整数ushortCHAR字符char
LONG32位无符号整数intDWORDLONG64位长整数long
DWORD32位无符号整数uintHDC设备描述表句柄int
HANDLE句柄,32位整数intHGDIOBJGDI对象句柄int
UINT32位无符号整数uintHINSTANCE实例句柄int
BOOL32位布尔型整数boolHWM窗口句柄int
LPSTR指向字符的32位指针stringHPARAM32位消息参数int
LPCSTR指向常字符的32位指针StringLPARAM32位消息参数int
BYTE字节byteWPARAM32位消息参数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命名空间中对应的类注释/用法
boolSystem::Booleanbool dirty = false;
charSystem::SBytechar sp = ’ ‘;
signed charSystem::SBytesigned char ch = -1;
unsigned charSystem::Byteunsigned char ch = ‘\0’;
wchar_tSystem::Charwchar_t wch = ch;
shortSystem::Int16short s = ch;
unsigned shortSystem::UInt16unsigned short s = 0xffff;
intSystem::Int32int ival = s;
unsigned intSystem::UInt32unsigned int ui = 0xffffffff;
longSystem::Int32long lval = ival;
unsigned longSystem::UInt32unsigned long ul = ui;
long longSystem::Int64long long etime = ui;
unsigned long longSystem::UInt64unsigned long long mtime = etime;
floatSystem::Singlefloat f = 3.14f;
doubleSystem::Doubledouble d = 3.14159;
long doubleSystem::Doublelong 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#之间的桥梁

  • 7
    点赞
  • 35
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值