从原生 C++ 使用 .NET 类/模块






4.97/5 (34投票s)
本文旨在描述一种或多或少通用的方法,用于从原生 C++ 应用程序访问 .NET 托管对象。
引言
本文旨在描述一种或多或少通用的方法,用于从原生 C++ 应用程序访问 .NET 托管对象。我将介绍一个动态链接库 (DLL),它可以用于,例如,增强传统 C++ 应用程序的托管代码能力。该库用 C++/CLI 编写,这是唯一可以完成此类任务的 .NET 语言。所有代码都是用 Visual C++ 2008 编写的;使用以前版本的 Microsoft C++ 编译器也可以做到这一点,但 Microsoft 对 VS 2008 的 C++/CLI 进行了大量更改,因此现在使用起来比旧版本容易得多。第一句话中的“更”通用意味着该库可以用于调用任何托管类的任何函数(带无限数量的参数)。“或多或少”意味着参数类型仅限于原生 C++ 类型和少数用户定义类型(字符串、日期/时间等)。很容易为你自己的类型提供支持,但为此,你必须自己扩展 DLL 的代码。
概述
图 1 显示了该想法背后的模型
原生 C++ 应用程序持有托管类 A 的代理类 A'' 的实例。该代理将所有调用传递给桥接 DLL,该 DLL 持有 A 的托管 C++ 代理类 A'。托管代理类最终调用托管类 A。返回值和输出参数以相反的方向传回原生代理。代理类 A'' 向原生 C++ 应用程序呈现与托管类 A 向托管应用程序呈现的相同公共接口(转换为 C++),因此托管类 A 可以从 C++ 应用程序中透明地使用。
代码组织
代码分为三个部分。原生到托管的桥接库,原生适配器静态链接库,以及原生工具静态链接库。
原生适配器静态库
此库包含原生代理类的基类 NativeProxyBase
。此基类处理动态链接库的加载和释放。它包含成员函数,用于请求从桥接库创建和释放托管类的实例。它还包含函数,用于将函数调用和属性访问请求传递给桥接库。应用程序中的代理类不应直接使用 NativeProxyBase
类。此类的两个派生类可方便地从你自己的代理类访问。NativeProxy
类应用于访问托管类的所有非静态成员。NativeProxyStatic
用于静态成员。
原生到托管的桥接动态库
此库导出一组函数来操作托管类。它还包含原生适配器库中原生代理类的托管对应项。该库使用 .NET 反射来调用托管类的成员函数。它能够通过文件名或完整的 .NET 程序集名称加载程序集。
原生工具静态库
此库包含用于将参数从 C++ 传递到托管 C++/CLI 的类型和转换函数。
参数传递
为了传递任意数量的任意类型参数,我们需要一种特殊的机制来处理这个问题。Boost.Any 就是这样一个强大的工具(参见链接部分)。参数作为 Boost.Any 的 std::vector
(std::vector<boost::any>
) 传递。方法调用的每个参数都封装在一个 Boost.Any 对象中。在托管 C++/CLI 侧,参数被解封装并转换为相关的托管类型。这种解封装是导致引入中提到的有限泛化的原因。每个类型及其托管关联类型,封装在 Boost.Any 中,都必须被桥接库中的解封装代码知晓。为了消除对 Boost.Any 的直接依赖,原生工具库中定义了别名 AnyType
(typedef boost::any AnyType
)。你的所有代码都应该使用 AnyType
而不是 boost::any
,这样就可以很容易地用自制工具替换 Boost.Any。
如何使用
一个简单的例子:假设我们有一个 .NET 程序集 (hello.dll) 中的托管类 Hello
。
//C#
namespace Universe
{
class Hello
{
public void HelloWorld()
{
Console.Writeline("Hello World!");
}
}
}
现在我们需要为 Hello
定义一个原生代理类
//C++
namespace Universe {
class Hello
{
public:
Hello() : wrapper_("hello.dll", "Universe.Hello") {}
void HelloWorld()
{
wrapper_("Hello");
}
private:
nativeAdapter::NativeProxy wrapper_;
};
}
在我们的主函数中,我们可以像使用托管类一样使用代理
//C++
int main(int, char **)
{
Universe::Hello hello;
hello.Hello();
return 0;
}
如果你运行这个小程序,控制台将按预期打印“Hello world!”。NativeProxy
类的 wrapper_
实例在其构造函数中加载 NativeToManagedBridge.dll。它还加载程序集 hello.dll 并创建托管 Hello
类的实例。该对象被锁定以避免被垃圾回收器释放,因为没有托管引用指向托管对象。锁被 NativeProxy
类的析构函数释放,该析构函数还释放 NativeToManagedBridge.dll 的句柄。析构函数将从 C++ Hello
类析构函数(当 hello
对象超出范围时调用)中调用。要调用托管类的方法,请使用 NativeProxy
的重载函数调用运算符之一,或调用 executeManaged
成员函数。第一个参数始终是托管类的方法名称。
参数、返回值、输出参数和属性
C# 中一个简单的人物表示
//C#
namespace Universe
{
class Person
{
public HelloWorld(string firstname, string lastname, int age)
{
firstname_ = firstname;
lastname_ = lastname;
age_ = age;
}
public string fullName()
{
Return firstname_ + " " + lastname_;
}
public string Age
{
get {return age_;}
set {age_ = value;}
}
public int getAgeAndNames(out string first, out string last)
{
first = firstname_;
last = lastname_;
return age_;
}
private string firstname_, lastname_;
private int age_;
}
}
Person
的 C++ 代理
//C++
namespace Universe {
class Person
{
public:
Person(const AnyUnicodeString &firstname,
const AnyUnicodeString &lastname, int age)
{
AnyTypeArray params(3);
params[0] = anyFromUnicodeString(firstname);
params[1] = anyFromUnicodeString(lastname);
params[2] = anyFromInt32(age);
wrapper_ = new nativeAdapter::NativeProxy("hello.dll",
"Universe.Person", params);
}
~Person()
{
delete wrapper_;
}
int getAge() const
{
AnyType age;
wrapper_->get("Age", age);
return int32FromAny(age);
}
void setAge(int age)
{
wrapper_->set("Age", anyFromInt32(age));
}
int getAgeAndNames(AnyUnicodeString &first, AnyUnicodeString &last)
{
AnyTypeArray params(2);
params[0] = anyFromUnicodeString(first);
params[1] = anyFromUnicodeString(last);
AnyType result;
(*wrapper_)("getAgeAndNames", params, result);
first = unicodeStringFromAny(params[0]);
last = unicodeStringFromAny(params[1]);
return int32FromAny(result);
}
private:
nativeAdapter::NativeProxy *wrapper_;
};
}
此示例演示如何在构造函数中传递参数以及如何处理返回值和 .NET 属性。要传递参数,必须构造一个 AnyTypeArray
(std::vector<:any>
的别名)并用构造函数中的参数值填充。要将值包装到 Any 对象中,请使用原生工具库提供的包装函数。为了支持 .NET 属性,NativeProxy
提供了两个方法 get
和 set
。字符串参数是属性名称。get
方法直接将属性值包装到 AnyType
对象中返回。set
方法将新属性值作为第二个参数。要检索方法调用的返回值,只需传入一个 AnyType
类型的 result
对象。要解包 Any 对象,请使用原生工具库提供的解包函数。输出和引用参数在传递给桥接库时没有区别处理。它们将在从托管成员函数返回后由桥接库填充/覆盖。
那静态成员函数呢?
//C#
namespace Universe
{
class Planet
{
public static void ShowVersion()
{
Console.Writeline("v1.1");
}
}
}
//C++
namespace Universe {
class Planet
{
public:
static void ShowVersion()
{
using namespace nativeAdapter;
NativeProxyStatic wrapper("hello.dll", "Universe.Planet");
wrapper("ShowVersion");
}
};
}
如你所见,NativeProxyStatic
类的工作方式与 NativeProxy
类几乎相同。不同之处在于它不创建托管类的实例。参数、返回值和属性的处理方式与 NativeProxy
中相同。
参数类型
预定义
以下类型可以用作参数或返回值。它们在原生工具库的 anyType.h 头文件中定义。
关于 | 任何类型名称 | C++ 类型 | C# 类型 |
布尔值 | AnyBoolean |
bool | bool |
有符号整数 | AnyInt64 , AnyInt32 , AnyInt16 , AnyInt8 |
__int64 , __int32 , __int16 , __int8 |
Int64 , Int32 , Int16 , SByte |
无符号整数 | AnyUInt64 , AnyUInt32 , AnyUInt16 , AnyUInt8 |
unsigned __int64 , unsigned __int32 , unsigned __int16 , unsigned __int8 |
UInt64 , UInt32 , UInt16 , byte |
浮点 | AnyFloat128 , AnyFloat64 , AnyFloat32 |
long double , double , float |
-, Double , Single |
日期/时间 | AnyDateTime |
tm |
日期时间 |
字符 | AnyAsciiChar , AnyUnicodeChar , AnyTChar |
char , wchar_t , TCHAR |
char |
字符串 | AnyAsciiString , AnyUnicodeString , AnyTString |
std::string , std::wstring , std::basic_string<TCHAR> |
字符串 |
数组 | AnyByteArray , AnyAsciiStringArray , AnyUnicodeStringArray , AnyTStringArray |
std::vector<unsigned __int8> , std::vector<std::string> , std::vector<std::wstring> , std::vector<std::basic_string <TCHAR> > |
byte[] , string[] , string[] , string[] |
对于表 1 中的类型,原生工具库提供了包装和解包装函数。
MFC/Qt 类型
一些 MFC 和 Qt 数据类型可以轻松转换为表 1 中预定义的类型;请参阅 testMFC 项目的 mfc_any_conversions.h 和 testQt 项目的 qt_any_conversions.h 以了解包装和解包装函数。
MFC 类型 | AnyType | C++ 类型 | C# 类型 |
CString |
AnyAsciiString 或 AnyUnicodeString |
std::string 或 std::wstring |
字符串 |
CByteArray |
AnyByteArray |
std::vectory<unsigned __int8> |
byte[] |
COleDateTime |
AnyDateTime |
tm |
日期时间 |
CStringArray |
AnyTStringArray |
std::vector<std::string> 或 std::vector<std::wstring> |
string[] |
Qt 类型 | AnyType | C++ 类型 | C# 类型 |
QChar |
AnyUnicodeChar |
wchar_t |
char |
QString |
AnyUnicodeString |
std::wstring |
字符串 |
QByteArray |
AnyByteArray |
std::vector<unsigned __int8> |
byte[] |
QDateTime |
AnyDateTime |
tm |
日期时间 |
QStringList |
AnyUnicodeStringArray |
std::vector<std::wstring> |
string[] |
将你自己的类型传递给托管类
如果预定义类型不足,则需要做一些额外的工作。请记住,该类型必须与 C++/CLI 兼容。
- 为其定义一个
AnyType...
别名(原生工具库中的 anyType.h)。 - 提供一个包装函数
anyFrom... ()
(原生工具库中的 anyType.h)。 - 提供一个解包函数
...FromAny()
(原生工具库中的 anyType.h) - 扩展原生到托管桥接库中 anyHelper.cpp 中的
toObject(...)
函数,将你的类型(从 any 对象中解包)转换为托管System::Object
。 - 扩展原生到托管桥接库中 anyHelper.cpp 中的
toAny(...)
函数,将System::Object
转换为你的类型并将其包装为 Any 对象。
定位 .NET 程序集
通过文件名定位程序集
如果将 .NET 程序集的文件名传递给 NativeProxy
类的构造函数,原生到托管的桥接库会按以下方式尝试加载此程序集文件:
- 尝试不修改路径加载程序集(例如,如果指定了完整路径)。
- 如果 1.) 失败,加载器会在应用程序目录的子目录 netmodules 中查找。
- 如果 1.) 和 2.) 失败,则会在应用程序目录中搜索程序集。
通过完整程序集名称定位程序集
如果将完全限定的程序集名称传递给 NativeProxy
类的构造函数,.NET Framework 会尝试按照 MSDN Library 中所述的方式加载程序集(运行时如何定位程序集)。桥接库通过 Assembly.Load
方法加载程序集。
//C++
NativeProxy form("System.Windows.Forms, "
"Version=2.0.0.0, "
"Culture=neutral, "
"PublicKeyToken=b77a5c561934e089, "
"processorArchitecture=MSIL"
, System.Windows.Forms.Form);
form("show");
错误处理
为了将 .NET 代码中引发的异常传递给原生 C++ 代码,所有 .NET 异常都在桥接库中捕获。完整的异常文本将返回到原生代理类。在 NativeProxyBase
类中,会再次引发相同文本的 ManagedException
类型的 C++ 异常。nativeAdapter 库包含从 std::exception
派生的异常类 ManagedException
的声明。
//C++
try
{
Universe::Hello hello;
hello.Hello();
}
catch(std::exception &ex)
{
std::cout << ex.what() << std::endl;
}
链接
- Qt 框架:http://qt.nokia.com/
- Boost C++ 库:https://boost.ac.cn
- MSDN Library:http://msdn.microsoft.com