P/Invoke提供了方便的.NET和c++ dll交互接口,通过P/Invoke可以将native的对象转化成managed object,从而享受.NET带来的种种便利.
但是,假如dll中返回的参数,不是形如int, double, bool这样可以直接转化为.NET类型的对象,又该如何使用P/Invoke呢?
比如我有这样一个接口:
Code
1 #ifdef DLLPROJECT
2 #define DLLEXP __declspec(dllexport)
3 #else
4 #define DLLEXP __declspec(dllimport)
5 #endif
6
7 struct group
8 {
9 char* groupName;
10 int userCount;
11 char** userNames;
12 };
13
14 struct groupList
15 {
16 int count;
17 group* groups;
18 };
19
20 extern "C"
21 {
22 DLLEXP groupList* getGroupList();
23 }
getGroupList返回一个嵌套struct的结构体,如何在.NET中获取该对象呢?
如果查阅MSDN,通常会得到这样的答案:
声明一个带Attribute的结构体
Code
1 [StructLayout(LayoutKind.Sequential)]
2 public class GroupList
3 {
4 String GroupName;
5 int UserCount;
6 String[] UserNames;
7 }
然后写一个如下的函数,试图通过对Attribute的修饰来达到获取返回的结构体的目的.
Code
1 [DllImport("mydll.dll")]
2 [return: MarshalAs(UnmanagedType.LPStruct)]
3 public static extern void getGroupList();
假如你正在采用类似的方法解决问题,基本上你会得到一个Memory Corrupt的错误信息. 或许有人要说,结构体/String数组不应该作为返回值传递,而是应该放到参数中,由getGroupList来为参数赋值. 的确,有很多这样调用的例子,网上能搜到一大把,可惜的是,这样的方法只适用于定长的结构. 比如,不包括的struct,或者是定长的String数组. MSDN上有很多类似的例子,请看这里.
既然MSDN上已经有成功的例子了,那我这里要说明的是什么呢? 注意struct groupList中,groups的个数是不确定的,它是一个指像group数组的指针. .NET在Marshal的时候自然不知道如何将这样的结构体转换成.NET Object. 但是,我们可以手动写一个转换:
Code
1 [DllImport("mydll.dll")]
2 public static extern IntPtr getGroupList();
3
4 class Group
5 {
6 public string Name;
7 public List<string> UserList;
8 public Group()
9 {
10 UserList = new List<string>();
11 }
12 }
13
14 static List<Group> parseGroupList(IntPtr groupListPtr)
15 {
16 List<Group> ret = new List<Group>();
17 int groupCount = Marshal.ReadInt32(groupListPtr);
18 for (int i = 0; i < groupCount; i++)
19 {
20 Group group = new Group();
21 IntPtr groupPtr = Marshal.ReadIntPtr(groupListPtr, 4);
22 group.Name = Marshal.PtrToStringAnsi(Marshal.ReadIntPtr(groupPtr));
23 int userCount = Marshal.ReadInt32(groupPtr, 4);
24 IntPtr usernameListPtr = Marshal.ReadIntPtr(groupPtr, 8);
25 for (int j = 0; j < userCount; j++)
26 {
27 IntPtr usernamePtr = Marshal.ReadIntPtr(usernameListPtr, j * 4);
28 string name = Marshal.PtrToStringAnsi(usernamePtr);
29 group.UserList.Add(name);
30 }
31 ret.Add(group);
32 }
33 return ret;
34 }
35
36 static void Main(string[] args)
37 {
38 IntPtr groupList = getGroupList();
39 parseGroupList(groupList);
40 }
41
关于IntPtr,可以在网上找一些相关的信息,这里,只要把它想象成c++中的void*类型即可. 在Main中,我们读到了一个IntPtr类型的groupList,即指向dll返回结构体的指针. 然后,在parseGroupList中,我们一步一步地解析这个指针.
struct groupList的第一个member是int count.所以,我们通过
int groupCount = Marshal.ReadInt32(groupListPtr) 把它读出来
第二个member是group*.那就可以用
IntPtr groupPtr = Marshal.ReadIntPtr(groupListPtr, 4);
读出来.注意这里4这个参数表示位移,我们之前已经读到一个int了,所以要位移4bytes.
以此类推,如此我们可以把c++中的结构体,转换成.NET中的List<Group>类型. 全归功于Marshal的强大功能.
总结
以上的方法,可以读取任何的结构体,关于如何解析字符串数组,可以看code project上的经典文章
http://www.codeproject.com/KB/cs/marshalarrayofstrings.aspx
我就是看了这篇文章后受到启发,把它扩展应用到返回struct上的.
最后还要提一下,通常情况下,还是把结构体放在返回值里,原因一,返回值要留给ErrorCode用;原因二,这样的写法通常会忘记释放内存(注意,groupList是在dll中用malloc分配的,还需要在同一个dll中free掉).一个更好的做法是设计一组GroupListAlloc/GroupListFree/int GetGroupList(GroupList*)的接口. 当然,解析的过程还是一样的. :)