用C++/CLI搭建C++和C#之间的桥梁

时间:2022-09-01 12:30:55

一、简单用法

C#和C++是非常相似的两种语言,然而我们却常常将其用于两种不同的地方,C#得益于其简洁的语法和丰富的类库,常用来构建业务系统。C++则具有底层API的访问能力和拔尖的执行效率,往往用于访问底层模块和构建有性能要求的算法。

这两种场景看起来有较大的差异,大多数的时候可以各行其道。但还是有很多时候会出现融合的情况。当我们构建分布式系统的时候,由于RPC机制一般都是语言无关的,我们大可以将其各尽所长,按需划分在最能发挥其长处的位置。然而,一旦我们需要构建融合两者需求的集中式系统的时候,就会头痛无比。

此时,我们可以使用C++/CLI搭建C++和.Net之间的桥梁,C++/CLI是一个比较有意思的两栖模块,它具有如下特点

  1. 既可以访问.Net类库,也可以访问C++原生类库
  2. 既可以被.Net程序引用,也可以被C++原生程序引用

使用C++/CLI,我们可以使用C++编写算法,用C#编写界面,也可以使用.Net Framework类库增强C++程序功能,各取所长。关于的优点,园子里有篇文章介绍的比较详细,值得一读:从C++到C++/CLI

下面我们就以一个简单的例子来演示一下它的用法:

Calculator.h:

    #pragma once

    namespace CppCliTest
    {
        public ref class Calculator
        {
        public:
            int Add(int a, int b);
        };
    }

 

Calculator.cpp

    #include "stdafx.h"
    #include "Calculator.h"

    namespace CppCliTest
    {
        int Calculator::Add(int a, int b)
        {
            return a + b;
        }
    }

 

main.cpp

    #include "stdafx.h"
    #include "Calculator.h"

    using namespace System;
    using namespace CppCliTest;

    int main(array<System::String ^> ^args)
    {
        Calculator^ calculator = gcnew Calculator();
        int result = calculator->Add(3, 2);

        Console::WriteLine(L"Result is {0}", result);
        return 0;
    }

 

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

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

具体的功能我将在后面的文章中再做介绍,MSDN中也有文档详细的介绍了这些语法:https://msdn.microsoft.com/zh-cn/library/ms235289.aspx

虽然C++/CLI同时具有两者的功能,但它使得本就比较复杂的C++语法变得更加复杂了(特别是初期的版本,非常复杂,现在已经简化了不少了),并且长期没有得到VisualStudio这宇宙第一IDE的较好支持(在VS2010的时候还不支持智能提示),是无法与拥有大量语法糖的C#比开发效率的。加上大多数需求场景可以通过分布式系统解决,这些都导致了它一直没有得到太多的关注。但是,微软还是在积极的改进它的,加上C++11的支持,现在已经比之前好用多了,如果用在合适的位置,是绝对能让你的开发如鱼得水的。

二、复杂用法

对于如下C#代码:

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

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

 

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

和传统的C++创建的语法比较下,

   

 P* x = new P();

 

我们不难发现,对于托管对象,主要引入了如下两个语法:

  1. 用gcnew代替new实现托管对象的创建
  2. 用^代替*实现托管对象的指针

这种方式创建的对象是可以直接被CLR支持的,可以在C#中使用。托管对象指针使用的方式和传统的对象指针还是比较类似的,直接使用->即可:

    System::Object^ x = gcnew System::Object();
    auto str = x->ToString();

另外,C++/CLI也有一种类似于C++的对托管对象的引用的语法:

    System::Object^ x = gcnew System::Object();
    System::Object% y = *x;
    auto str = y.ToString();

由于这种方式在C#里没有对应的语法,用起来感觉怪怪的,也不方便于其它.net语言集成。

 

托管类型的定义

我们也可以自定义托管类型,在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<T> ^的方式定义数组。

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);

这里有个需要注意的地方是在使用完转换出来的const char*后需要释放掉转换过程中的Intptr,如果没有太多需要考虑性能的地方,大可以使用一个std::string将其拷贝走,写成如下函数形式:  

#include <string>

    using namespace std;
    using namespace System;
    using namespace System::Runtime::InteropServices;

    string cast_to_string(String^ str)
    {
        IntPtr ip = Marshal::StringToHGlobalAnsi(str);
        const char* ch = static_cast<const char*>(ip.ToPointer());
        string stdStr = ch;
        Marshal::FreeHGlobal(ip);

        return stdStr;
    }

四、网络资源

关于C++/CLI的基础,我前面已经写过了几篇文章介绍过一些了,不过这些基本上都是管中窥豹,如果要详细了解C++/CLI,MSDN无疑是最好的教程。

如果需要在MFC中使用.net控件的话,可以参考如下三篇文章:

  1. 在 MFC 对话框中承载 Windows 窗体用户控件
  2. 以 MFC 视图的形式承载 Windows 窗体用户控件
  3. 以 MFC 对话框的形式承载 Windows 窗体用户控件

此外,MSDN文章如何实现 C++ 互操作 上有更加详细的文章索引。后面有空的话,我会继续继写一些相关的介绍文章的。