以下是引用片段:
// from ListLib.h
struct NativeNode {
int a, b;
TCHAR *str;
struct NativeNode *next;
}; |
ListWrap.cpp 中的包装类 ManagedNode 模拟 NativeNode 的定义,不过有几个细微区别:本机
char* 被替换为托管 String,并且由于我使用 ArrayList 来实现列表结构,所以没有 next 指针。在代码中,它应该如下所示:
以下是引用片段:
// managed equivalent of NativeNodepublic __gc class ManagedNode {public: int a, b; String* str;}; |
定义好 ManagedNode 之后,下一步就是编写一些代码将 NativeNode 转换成 ManagedNode。但在开始编写之前,请稍微考虑一下转换器函数应该是什么样子的,它应该有什么样的参数和返回值。一种方式是编写一个函数,将
NativeNode 的本机列表作为参数并返回 ManagedNode 的托管列表,它可能销毁进程中的本机列表。.NET
客户端应用程序会直接调用 ListLib DLL(或者您的 getmystruct)来获取本机列表,将其作为 IntPtr,然后将这个
IntPtr 传递给转换函数,如下所示:
以下是引用片段:
// call DLL directly through interop
IntPtr nativeList = AllocateList(7);
// call wrapper to convert
ArrayList amanagedList = ListWrap.Convert(nativeList); |
在大多数情况下,客户端要负责调用 DLL 来释放本机列表,或者由 Convert 函数自动完成释放过程。
另一种方法是完全隐藏 DLL,其做法是:将本机函数 AllocateList 包装在一个包装器中,由它分配列表、转换并释放原始本机列表,再将托管列表作为
ArrayList 返回。哪种方法更好呢?第一种策略的优点是您只需编写一个简单的转换函数,在任何具有本机列表的地方都可以使用它。第二种策略需要包装创建列表的每个函数。如果您有多个创建列表的函数,这种方式会有些麻烦,但是它的优点是对
.NET 客户端完全隐藏了所有本机内容。客户端不需要处理 IntPtrs,甚至不需要导入 DLL;ListWrap
将这一切都隐藏了。我采取的是这种方式,我也鼓励您在自己的应用程序中使用这种方式。它要对库进行完全包装还需要做很多工作,但结果会比较可靠,而且完全封装。
有了 ManagedNode 之后,剩下的工作就是包装 AllocateList 了。这个过程十分简单。首先,调用
AllocateList 来分配本机列表,然后创建一个空的 ArrayList。接下来将所有 NativeNode
复制到 ManagedNode,并将它们添加到托管列表中,您在进行这些操作时会将它们删除。图 3 显示了完整的细节。托管
C++ 的好处之一就是所有代码看起来很简单、整洁,即使您处理的是混合对象。将本机 char* 复制到托管 String
只是一个简单的赋值,如以下的代码行所示:
以下是引用片段:
mn->str = nn->str; // String = char*: it just works! |
不需要调用转换函数,编译器知道如何去做。CreateList 在运行时会删除本机节点。这比在最后删除节省存储空间。
通过将整个列表转换成托管对象(而不是通过 interop 和 StructLayout 将它导出),您可以为托管客户端营造一种托管环境。这叫入乡随俗!毕竟,一些程序员选择
.NET 的主要原因之一就是它具有自动垃圾回收的功能。如果您直接通过 interop 导出列表,您还必须导出 FreeList,并要求使用其他基于
.NET 语言的程序员要记得调用它。
一般情况下,如果您导出到托管环境,最好是将尽可能多的数据转换成托管对象。什么情况下例外呢?您的客户端也是采用 C++
编写的。当然,这条规则并不总是适用。有时更好的方式是直接导出结构,并要求客户端释放它们 — 例如,如果复制对性能或内存造成的影响太大而不可接受,就需要这样做。您必须使用判断来决定是进入托管环境还是进入本机环境。
问:我正在使用 C++ 托管扩展来包装现有的 C++ 库,使基于 .NET 的语言可以访问它。在托管
C++ 中,我可以写为
以下是引用片段:
String* s = new String();
s = _T("Hello, world"); |
但如何将托管 String 再次转换为本机 TCHAR*?
Matthew Brady
答:一旦您了解了神奇的 voodoo,这会变得很简单。您必须调用 PtrToStringChars
并 pin 结果。代码如下所示:
以下是引用片段:
String __gc* s = S"Hello";
const wchar_t __pin* p = PtrToStringChars(s); |
不要忘记对从 PtrToStringChars 返回的指针进行 __pin 操作。Pin 是必须的,因为 PtrToStringChars
将一个托管 (__gc) 指针返回给托管内存中的 String 对象的第一个字符,而垃圾回收器随时都会回收托管内存,除非您显式对它进行
__pin。一般情况下,每次将 __gc 指针传递给本机(非托管)函数时都必须使用 __pin。
图 4 显示了一个小程序,它将托管 String 转换成宽字符和 ANSI 字符串。要转换成 ANSI,可以使用自己喜欢的转换函数,如
wcstombs 或 ATL W2A 宏。如果您使用 MFC Cstrings,则不必进行任何操作,因为 CString
对 char* 和 wchar_t 都有赋值运算符:
以下是引用片段:
// both will work
CString s1 = "hello, world";
CString s2 = L"Hello, world"; |