C++/CLI封装原生C+ +库供.NET项目调用

以一个实例详细介绍C+ +/CLI封装原生C+ +库供.NET项目调用的步骤。

已得结论

在前面的文章《C+ +/CLI基本语法和最佳实践》中,已经得到了以下重要结论:

(1)托管类型(value struct / value class 和 ref struct / ref class)

  • 不能定义非托管类型的字段。
  • 可以定义非托管类型的C+ +普通指针类型。
  • 可以使用C+ +的基本数据类型,因为在这些类型可以和C#数据类型自动转换。
  • 托管类型的方法参数和返回值可以为非托管类型,但是无法传参。

(2)封装程序库供C#使用:

  • 凡是要暴露给C#项目的接口,统一使用托管类型。
  • 托管类型的指针成员变量、参数或返回值为非托管类型的方法,只能充当函数逻辑实现的辅助,无法暴露给C#项目(避免出错,最好不要使用!)
  • 封装原生C+ +代码给C#项目使用:可以定义一个C+ +类指针成员变量,作为原生C+ +代码的统一访问入口;(推荐方法)
  • 封装原生C+ +代码给C#项目使用:可在C+ +/CLI的方法内部使用原生C+ +代码中的静态方法,或实例化原生C+ +代码里面的类,从而使用既有原生C+ +的代码。

下面以一个完整实例详细介绍如何使用C+ +/CLI封装原生C+ +库供.NET项目调用。

制作原生C+ +库

这里使用动态链接库,设置预编译器定义MYDLL_EXPORTS,确保预编译阶段MYDLL_EXPORTS宏被替换为__declspec(dllexport)。

头文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#pragma once
#ifdef MYDLL_EXPORTS
#define MYDLL_API __declspec(dllexport)
#else
#define MYDLL_API __declspec(dllimport)
#endif

#include <vector>
#include <string>

class MYDLL_API MyComputer
{
public:
double Add(double a, double b);
bool Divide(double a, double b, double& res);
double Sum(const std::vector<double>& data);
bool IsStringRight(std::string text);
};

注意:MYDLL_API必须放在class关键字后面,易错!

源文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include "MyComputer.h"

double MyComputer::Add(double a, double b)
{
return a + b;
}

bool MyComputer::Divide(double a, double b, double& res)
{
if (b == 0)
return false;
res = a / b;
return true;
}

double MyComputer::Sum(const std::vector<double>& data)
{
double res = 0.0;
for (size_t i = 0; i < data.size(); i++)
res += data[i];
return res;
}

bool MyComputer::IsStringRight(std::string text)
{
return text == "LH";
}

使用C+ +/CLI封装

与原生C+ +项目一样,先设置附加包含路径(或者直接使用相对路径进行引用)、lib附加库目录以及lib输入库,确保项目可以找到原生C+ +的头文件和lib文件。

头文件

以下是C+ +/CLI封装库的头文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
#pragma once
#include <msclr/marshal.h>
#include <msclr/marshal_cppstd.h>

#include "MyComputer.h"

using namespace System;
using namespace System::Collections::Generic;

namespace lh {
public ref class MyComputerWrapper
{
private:
MyComputer* pComputer;

public:
MyComputerWrapper();
~MyComputerWrapper();

Double Add(Double a, Double b);
Boolean Divide(Double a, Double b, Double% res);
Double Sum(List<Double>^ data);
Boolean IsStringRight(String^ text);
};
}

结合前面的重要结论:

  • 暴露的接口只采用托管类型,不包含任何非托管类型成员,也没有参数或返回值为非托管类型的方法。
  • 有一个MyComputer指针,MyComputer来自原生C+ +库,设置为私有,在构造函数中初始化,在析构函数中释放。在其他成员函数中通过该指针来访问原生C+ +代码。
  • 这里的Double、Boolean也可以用double、bool替换,因为它们之间可以自动转换,但是不建议,大脑里的规则越少才越深刻!

源文件

以下是C+ +/CLI封装库的源文件:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
#include <vector>
#include "MyComputerWrapper.h"

using namespace lh;

MyComputerWrapper::MyComputerWrapper()
{
pComputer = new MyComputer();
}

MyComputerWrapper::~MyComputerWrapper()
{
delete pComputer;
}

Double MyComputerWrapper::Add(Double a, Double b)
{
return pComputer->Add(a, b);
}

Boolean MyComputerWrapper::Divide(Double a, Double b, Double% res)
{
double temp = 0.0;
bool result = pComputer->Divide(a, b, temp);
res = temp;
return result;
}

Double MyComputerWrapper::Sum(List<Double>^ data)
{
std::vector<double> vec;
for (size_t i = 0; i < data->Count; i++)
vec.push_back(data[i]);
return pComputer->Sum(vec);
}

Boolean MyComputerWrapper::IsStringRight(String^ text)
{
return pComputer->IsStringRight(msclr::interop::marshal_as<std::string>(text));
return true;
}

几点需要重点说明:

  • Double是值类型,通过Double%实现引用传参,可以修改这个值,在C#项目里会变成ref double类型参数。
  • List^转std::vector类型,通过遍历赋值的方法实现。
  • 方法的实现逻辑可以混用托管和非托管代码。

重点:数据转换

自动转换

以下类型可以相互转换:

在这个例子中,原生C+ +里面的类型是double,在C+ +/CLI方法实现中,我们直接把Double类型的值传给原生C+ +方法,不用转换。

msclr命名空间下的转换函数

  • 注意:将#include<msclr/marshal.h>尽量前置,防止会引发IServiceProvider相关报错。
  • std::string & System::String相互转换

marshal_cppstd.h头文件只提供std::string、std::wstring和System::String^相互转换的函数。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
#include <msclr/marshal_cppstd.h>
#include <iostream>
using namespace System;
using namespace System::Collections::Generic;

int main() {
// std::string & System::String相互转换
std::string str1 = "Hello CLI";
String^ str2 = msclr::interop::marshal_as<String^>(str1);
Console::WriteLine(str2);

String^ str3 = gcnew String("Hello C+ +");
std::string str4 = msclr::interop::marshal_as<std::string>(str3);
std::cout << str4 << std::endl;

std::system("pause");
return 0;
}
  • char* 转 System::String
1
2
3
4
5
6
7
8
9
10
11
12
13
14
#include <msclr/marshal.h>
#include <iostream>
using namespace System;
using namespace System::Collections::Generic;

int main() {
// char* 转 System::String
char* str5 = "Test String to Marshal";
String^ str6 = msclr::interop::marshal_as<String^>(str5);
Console::WriteLine(str6);

std::system("pause");
return 0;
}

手动转换

例如C#中的List与C+ +中的vector,可以通过遍历赋值的方式相互转换。

枚举转换时,也用同样的方式。

在C#项目中使用

最后再C#项目中使用C+ +/CLR库,与引用普通C#库别无二致!

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using lh;
using System;
using System.Collections.Generic;

namespace UseInCS {
internal class Program {
static void Main(string[] args) {
MyComputerWrapper computer = new MyComputerWrapper();

Console.WriteLine(computer.Add(12, 34));

double res = 0;
if (computer.Divide(24, 12, ref res))
Console.WriteLine(res);

List<double> datas = new List<double>() { 1, 2, 3, 4, 5, 6, 7, 8, 9 };
Console.WriteLine(computer.Sum(datas));

Console.WriteLine(computer.IsStringRight("LH"));
}
}
}

评论