============================
在C++/CLI中如何让你的Managed Code和Unmanaged(Native) Code沟通自如?interior_ptr和pin_ptr是桥梁——interior_ptr充当一个Managed Heap上的Native Pointer的角色。而pin_ptr则可以把对象钉在Managed Heap上!
所以,pin_ptr允许把Managed世界里的东西带入Unmanaged(Native)世界。
而如果想反其道而行,即把Unmanaged(Native)世界里的东西带入Managed世界,则可以自己写一个ref/value class的wrapper(目前),以后微软打算允许Native Class在Managed Heap上创建,并且允许Ref Class在栈上创建(彼时ref class关键字是否还有存在的价值?),从而允许更为方便的交互。
但是这里的问题是,基本上这种交互都是处于“基本类型的数据”层次的交互。pin_ptr只能把基本类型钉在Managed Heap上,也就是说,你可以写:
pin_ptr<int> p = ...;
却不能写
pin_ptr<GC_Class> p = ...; //错误!
这是因为.NET的对象模型和Native C++的对象模型是不兼容的,即使可以把GC_Class的对象钉在GC堆上并返回其首地址,也不能把它作为一个Native Class类对象来用。反之亦然。
下面是关于这个问题的一些思考...很零碎:)
声明:这是和孟岩老师一次email内容的整理。请不要转载
===========================
孟老师,昨天我们聊到的关于Managed和Unmanaged代码如何在对象层面沟通问题,我后来想了一下,还和我的一个朋友讨论了许久,我的看法是这样的:
您记得.NET&COM Interopration吗?它面对的不正是Native类和Managed类的对象模型的不一致的问题吗?那它是怎么解决的呢?用Wrapper——如果要在COM (对应于我们说的Native Code)中调用.NET的类提供的服务,CLR运行层会负责为.NET对象创建一个Wrapper,这个Wrapper仅仅是个proxy,充当虚函数转发器,由于CLR可以通过查询元数据得知.NET类的一切信息,所以创建一个Wrapper非常容易。而在.NET中如何调用传统的COM服务呢?同样也是Wrapper,不过由于COM是编译后的Native Code,失去了类型信息,所以需要用户把IDL拿来再用.NET的编译器编译一次,以提供必要的元数据,这样CLR在运行期就可以轻松的创建Wrapper了。
也就是说,Wrapper充当了两种对象模型之间的协调者的作用。而这个Wrapper也只能由CLR来创建,因为CLR够底层,知道关于对象模型的一切细节。要在语言层面做这样一个Wrapper是肯定会失败的。所以,如果我记得不错的话,.NET的BCl里面是提供了Interop服务的——我记得有一个名字空间就叫InteropService。
同样,转到C++/CLI中,很显然,这里的矛盾在于对象模型之间的冲突。
再深一层想,这种冲突到底是不是本质上的?其实不是,关键是Native世界侧重效率,所以对象模型实现比较高效紧凑,而.NET的动态对象模型则需要有完善的运行时信息,所以对象就背了个大包:-)但是,虽然只是因为目的不同,而并非本质上的冲突,但是却实际上是很难调和的——你没有办法让Native Class的对象模型变得和.NET的对象模型一样(那样它就不是Native Class了),你更没有办法让.NET的对象模型变得和Native Class一样(那以为着丧失运行期的动态类型信息)。
那么,这么说来,问题的本质已经出现了——对象模型的理念不同,导致了它们之间的实现细节不同,而这种细节上的不同又导致了互操作的困难。几乎可以肯定的说,这里只要出现对象级别的互操作,就肯定需要某种转换机制,.NET&COM Interop使用的Wrapper就是其一。
但是,毕竟,.NET&COM Interop只是个库的实现。在语言层面,你仍然没有办法法把“^”转型为“*”(或者反过来)!这就使语言层面的对象级交互不那么trivial。到底有没有一个比.NET&COM Interop更好的方案呢?
下面就是我想的结果——
考虑Native Class和Ref Class的内存布局,显然Native Class的布局非常Naive,而Ref Class中除了和Native Class布局相似的部分之外,还有很多其它的附加信息如同步块,对元数据的引用,甚至GC信息。是不是可以这样想——通过合理安排Ref Class的二进制格局,可以使它在二进制格局上成为Native Class的二进制格局的一个超集,从而把Ref Class的实例的某个offset处的地址传给Native接口可以把它“当成”Native Class来用?
但是这种办法也只是想想而已,真正实现估计还会有更多的细节。另外,如果反过来,却不可能把Native Class“当成”Ref Class来用,因为它的虚函数表中的信息不够,这时候,是不是可以通过一个“廉价的”(who knows?:-))类似Box的操作把Native 对象中的vptr所指向的vtbl扩充成一个first class的method table?这样就可以“当成”.NET类来用了。
如果底层的细节的差异不是那么严重的话,这种方式还是可行的(who knows?:-)),这种方法的好处是没有引入中间层,所以比较轻量级。这就为语言层面的对象级交互提供了方便而高效的途径——为什么这么说呢?因为在目前,如果允许pin_ptr定住一个.net class并将它传给Native接口的话,无非要用类似于.NET&COM Interop的Wrapper方法来解决对象模型的差异问题,这是个昂贵的操作,而且更为重要的问题在于,即使创建了Wrapper,问题仍然没有解决,收到Wrapper的Native接口只能通过虚调用来调用Wrapper的方法,而没法像看起来的那样直接访问ref class的成员(Wrapper只有接口,而没有实现)。这就使pin_ptr的语义变掉了(Native程序员可以想象拥有一个指针却不能访问它所指向的对象的成员吗(假设是public的)?)。
但是,如果采用我上面“臆想”的办法,就可以进行轻量级的Interop,并且由于这种方法没有引入中间层,所以指针的语义可以得到保证。这样就只要对pin_ptr的语义进行一个扩展(使它可以指向Ref Class实例)就行了:-)
耽误老师时间了:-)只是一通臆想而已。老师有什么金砖银砖还望不要吝啬的砸过来哦,呵呵:-)
另外,我一直在讨论如何实现这个交互的问题,而并没有深入去思考“到底我们需不需要”这种对象级交互。我个人的一点浅的看法是:Managed和Unmanaged间的对象级交互主要是为了保证整个程序的结构性(整个程序能有一个OO架构),如果在两个世界的衔接点上愿意抛弃这种结构,我们的程序仍然还是可以活的很好,从本质上说,所有类最终都归结为基本类型,所以pin_ptr就能胜任了。没有“必要”的需求说我们一定要在两个世界的衔接点上维持OO架构。关于这个问题的严重性我没有直观的认识,因为没有实践经验,老师有什么看法赶快告诉我吧:-)