托管代码和非托管代码的交互技术有3种:平台调用(PInvoke)、COM Interop、利用C++/CLI作为代理中间层
本文暂不讨论COM Interop。
本文代码测试平台:Windows7 64位,VS2015 Pro,.NET4.5
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 |
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<
该代码的功能就是将输入的字符串通过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<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
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属性进行修饰。
支持托管代码和非托管代码之间的方法签名的转换,而且直接生成相关的C#或者是VB的方法调用代码。
下载链接
常用DLL的API可以查看这个工具:下载地址
或者搜索网站:https://www.pinvoke.net/
在 C++ 中使用显式 PInvoke(DllImport 特性)
对于非常复杂的结构,通过P/Invoke还是很难处理的,这是可考虑使用C++ Inerop来处理。
实现起来比较简单直观,并且可以实现C#调用C++所写的类,但是问题是MONO构架不支持C++/CLI功能,因此无法实现脱离.NET Framework跨平台运行。
暂不讨论。
ref:C#/C++/CLI运行效率测试之一: C#通过CLR/C++调用Native CPP 类
通过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
#include
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
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;
}
欧盟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原始的功能,这里用到的有:
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 ^ 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);
参考资料:
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#之间的桥梁