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 = 
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值