C++\CLI混合编程

如何创建Unsafe代码

如前所述,不安全代码通常是通过向托管代码中添加本机代码来创建的。通过使用不安全的操作或对象也可以只使用MSIL代码创建不安全代码。
有几种编码C++和CLI的方法,所以它不安全。以下是使程序集不安全的四种常见方法。还有其他方法,但这些方法是我在开发中经常遇到的。

  1. 托管和非托管#pragma指令
  2. 非托管数组
  3. 非托管类/结构
  4. 指针和指针算法
#pragma unmanaged
int UMadd(int a, int b) 
{
    return a + b; 
}
#pragma managed
int Madd(int a, int b) 
{
    return a + b; 
}
void main() {
Console::WriteLine("Unmanaged Add 2 + 2: {0}", UMadd(2, 2));
Console::WriteLine("Managed Add 3 + 3: {0}", Madd(3, 3)); }

通过查看UMadd()和Madd()方法的代码,您不会发现有太大区别。两者都是标准的C++\CLI代码。请注意,您甚至可以以相同的方式调用这些方法,只要它们是在托管代码块中调用的。
如果尝试在本机代码块中调用托管代码,则会出现编译时错误C3821“function”:managed type or function cannot be used in an unmanaged function。这是有意义的,因为本机代码不使用CLR运行,而托管代码必须使用CLR运行,因此无法在本机代码中执行托管代码
使用这些指令需要注意的另一件事是,它们只允许在全局范围内使用,如清单22-1所示,或者在命名空间范围内。这意味着您不能中途更改方法或类。换句话说,整个函数或类可以是托管的或本机的,但不能是组合的。

在C++/CLI之前,托管C++使用了非托管类和结构的完全相同的语法,除了在托管的类和结构加上__gc前缀。从那时起,这两种语言的语法基本相同。
C++和CLI极大地提高了C++托管扩展代码的可读性。是的,托管和非托管类和结构的声明仍然非常相似(表22-1显示了一些主要区别),但是现在创建托管类的语法大不相同,因为托管类使用了handles[^]和gcnew命令,而不是指针[*]和用于非托管类的new命令。虽然这一变化主要是为了使托管代码更容易,但在编写非托管类时也使工作变得更轻松,因为现在已经不存在将两者混淆的情况。

Unmanaged class/struct Managed class/struct
没有前缀 ref 前缀
通过CRT堆上的指针或引用或直接在值类型变量中访问 通过托管堆上的句柄或直接在值类型变量中访问
如果没有指定显式基类,则类是独立的根 如果没有指定显式基类,则类从System::Object继承
支持多继承 不支持多继承
支持友元 不支持友元
只能继承非托管类型 只能继承托管类型
可以包含指向非托管类的指针类型的数据成员,但不能包含托管类的句柄 可以包含指向非托管类的指针类型的数据成员,可以包含托管类的句柄

从现有对象复制指针似乎是无害的。但如果对象是从托管类型派生的,则即使这样也有问题。在垃圾收集过程中,指向托管堆内存中的对象的位置可能会移动,因为垃圾回收器不仅会删除托管堆内存中未使用的对象,还会压缩它。因此,在压缩过程之后,指针可能指向错误的位置。幸运的是,C++\CLI提供了两种解决指针变化的方法:内部指针和固定指针。

固定指针 pin_ptr<>

如果你是一个经验丰富的传统C++程序员,你可能会立即看到句柄处理地址的能力的问题:内存中没有访问对象的固定指针地址。在CLI\CLI之前的托管版本(C++的托管扩展)中,使用相同的语法来处理托管和非托管数据。这不仅导致了混乱,而且也没有表明那种指针是被管理的(可能会发生变化)。使用新的handle语法,对象被管理就不那么容易混淆了。
不幸的是,句柄地址的不稳定性也会导致将句柄作为非托管函数调用的参数传递给托管对象的问题将失败。为了解决这个问题,C++\CLI增加了pin_ptr<>关键字,这就阻止了CLR在垃圾收集压缩阶段改变其位置。只要锁定的指针停留在作用域内,或者在为指针分配了nullptr值之前,指针就会保持锁定状态。
固定指针可以指向托管数组的引用句柄、值类型和元素。它不能固定引用类型,但可以固定引用类型的成员。_固定指针具有本机指针的所有功能,最显著的是指针比较和运算。
固定指针和内部指针之间的一个主要区别是,固定指针转换为本机指针,而内部指针则不能,因为内部指针能够随着内存的压缩而更改。因此,即使内部指针具有本机指针的所有功能,也无法将其传递给需要本机指针的非托管(本机函数)。
可以通过interior_ptrpin_ptr来将内部指针转换成本机指针。

void incr (int *i) 
{
    (*i) += 10;
 }
#pragma managed
void main () {
    Test ^test = gcnew Test();
    interior_ptr ip = &test->i;
    // incr( ip );   // Invalid
    pin_ptr i = ip;  // i is a pinned interior pointer
    incr( i );   // Pinned pointer to interior pointer passed to a 
            //native function call expecting a native pointer
    (*ip) = 5;
    Console::WriteLine ( test->i );
 }

将托管类放在非托管类中

您已经看到,在托管类中放置非托管类指针没有问题,但由于垃圾回收器无法维护非托管类中的成员句柄,因此您无法执行相反的操作(将托管类句柄放入非托管类中),如下例所示。(实际上,非托管类甚至不理解句柄的语法,因此垃圾回收器在此也是不可用的。)

class ClassMember {}; 
ref class RefClassMember {};
class Class 
{
public:
    RefClassMember ^hrc;  // Big fat ERROR
}
ref class RefClass 
{
public:
    ClassMember *pc;  // No problemo
};

Listing 22-8. 托管类嵌入非托管类
您不能将托管类的句柄放入未命名的类中。但可以使用内部指针,有时还使用固定指针,而不能是句柄。
可能还需要使用.NETFrameworkSystem::Runtime::InteropServices::GCHandle或更简单的模板gcroot对托管类进行包装

#include "stdio.h".  
using namespace System;
using namespace System::Runtime::InteropServices;
ref class MClass 
{
public:
    int x;
    ~MClass() { Console::WriteLine("MClass disposed"); } 
protected:
    !MClass() { Console::WriteLine("MClass finalized"); } 
};
#pragma unmanaged // works with or without this line
class UMClass
{
public:
    void* mclass;
    ~UMClass() { printf("UMClass deleted\n"); } 
};
#pragma managed
void main() 
{
    UMClass *umc = new UMClass();
    // Place ref class on unmanaged void* pointer
    umc->mclass = GCHandle::ToIntPtr(GCHandle::Alloc(gcnew MClass())).ToPointer();
    // access int variable x by typecasting void* 
    ((MClass^)GCHandle::FromIntPtr(System::IntPtr(umc->mclass)).Target)->x = 4;
    // Manage print int variable x Console::WriteLine("Managed Print {0}",
    ((MClass^)(GCHandle::FromIntPtr(System::IntPtr(umc->mclass))).Target)->x);
    // Unmanage print int variable x printf("Unmanaged Print %d\n",
    ((MClass^)(GCHandle::FromIntPtr(System::IntPtr(umc->mclass))).Target)->x);
    delete umc; 
}

#pragma managed(push, on)#pragma managed(pop) 成对出现,为压栈形式,其间的代码为托管代码,不影响之前和之后的代码形式(或者为托管,或者为Native代码)

The C++ Support Library

C++/CLI附带了几个支持类,称为C++支持库,旨在简化C++\CLI程序员的工作。您不需要使用C++支持库,因为还有其他方法来执行它提供的功能。但是C++支持库的目标是让事情变得更容易,为什么不使用它呢?
C++支持库不处理unsafe和unmanaged代码。它还处理智能指针和句柄、支持同步、COM包装以及.NET3.5框架的新特性,简化了封送处理(marshaling)。正如您所看到的,在处理互操作性时,这些方法中的大多数都会派上用场。
C++支持库提供的各种功能概览如下:

  • auto_handle<>类,提供自动资源管理.
  • gcroot<>类,它简化了将托管类嵌入到非托管类中的过程.
  • auto_gcroot<>类,它简化了托管类的嵌入,实现了自动资源管理,并将其嵌入到非托管类中.
  • com::ptr,它简化了为com对象创建托管包装类的过程.
  • PtrToStringChars()函数,用于将字符串转换为const wchar.
  • marshal_as<>模板函数和arshal_context<>类,这是在本机和托管数据类型之间转换的一种简单方法.
  • _safe_bool,当您需要ref类型充当bool时,这是一种更安全的数据类型.
  • lock类,它提供了一种简单的方法同步多个线程。

auto_handle<> 自动调用delete的资源管理

基本上,auto_handle类会在超出范围时调用delete方法。乍一看,您可能认为这个类是无用的;毕竟,当句柄超出范围时,这不是正常情况吗?事实上,它不是。当托管对象超出范围时,会调用finalize析构函数(终结器),而不是调用析构函数。
自动句柄的工作方式与下面对象的编码方式完全相同:

MyObj^ obj;
try
{
    obj = gcnew MyObj();
    // do stuff with obj 
}
finally 
{
    delete obj; 
}

使用auto_handle需要包含#include 头文件

gcroot<>auto_gcroot<>

在上面,我向您展示了一种将托管数据类型放入非托管数据类型的痛苦解决方案(直接使用GCHandle)。现在我要告诉你一个简洁的做法:使用gcroot模板类,该类包装了值类GCHandle,允许您声明托管数据类型并将其用作非托管数据类型的成员。
使用include包含gcroot模板的头文件.

  • gcroot<>调用的是ref class的终结器 !class()
  • auto_gcroot<>调用的是ref class的析构函数 ~class()
#include "stdio.h"
#include 
using namespace System; using namespace msclr;
ref class MClass 
{
public:
    int x;
    ~MClass() { Console::WriteLine("MClass disposed"); }
protected:
    !MClass() { Console::WriteLine("MClass finalized"); }
};
#pragma unmanaged
class UMClass
{
public:
    gcroot mclass;
    ~UMClass() { printf("UMClass deleted\n"); }
};
#pragma managed
void main() 
{
    UMClass *umc = new UMClass(); umc->mclass = gcnew MClass();
    umc->mclass->x = 4;
    Console::WriteLine("Managed Print {0}", umc->mclass->x); 
    printf("Unmanaged Print %d\n", umc->mclass->x);
    delete umc; 
}

[图片上传失败...(image-46d4ae-1599813341888)]

PtrToStringChars函数

C++ Support Library包含一个实用函数PtrToStringChars()。此实用函数将托管字符串转换为wchar_t类型的常量内部指针。这个方便的小实用程序允许您更高效地直接使用内部存储的字符数据,而不是将其复制到非托管的wchar_t数组中。
有一个小问题。请记住,需要本机指针的非托管函数不能使用内部指针。因此,像wprintf()这样的函数将要求您在使用指针之前先固定指针。

String ^hstr = "Hello World!";
pin_ptr pstr = PtrToStringChars(hstr);
wprintf(pstr);

marshal_asmarshal_context

在两个不同的实体(两个线程或者进程甚至机器、在Managed和Unmanaged之间)进行方法调用和参数传递的时候,具体的调用方法和参数的内存格式可能需要一定的转换,这个转换的过程叫做Marshal。
在上一章中,我们看到了一种互操作封送(interop marshaling)处理数据类型的方法(互操作封送意味着在本机类型和托管类型之间转换数据),前面的例子有点复杂。下面有种更简单的解决方案。
在C++支持库中支持两种封送处理方法。

  • 模板函数marshal_as
    ToType newValue=marshal_as(原始值);

  • 使用marshal_content

marshal_context ^context=gcnew marshal_context();
ToType newValue=context->marshal_as(OriginalValue);
//使用newValue
delete context;

所有marshal_as模版函数和marshal_context都位于命名空间msclr::interop中
使用context时封送处理的结果只有在封送处理context对象被销毁之前才有效。要保留封送处理的结果,必须复制数据。
为了方便起见,同一封送处理context可以用于多个数据转换,重用上下文不会影响以前任何封送处理调用的结果。这意味着您可以创建一个封送处理上下文作为成员变量,在构造函数中初始化它,然后将其用于需要使用封送处理上下文的所有发生事件。只要包含marshal_context的成员变量的类没有被销毁,封送的数据将保持有效。

From Type ToType Marshal Method h文件
System::String^ const char * marshal_context marshal.h
const char* System::String^ marshal_as marshal.h
System::String^ const wchar_t* marshal_context marshal.h
const wchar_t* System::String^ marshal_as marshal.h

你可能感兴趣的:(C++\CLI混合编程)