P/Invoke提供了方便的.NET和c++ dll交互接口,通過P/Invoke可以將native的對象轉化成managed object,從而享受.NET帶來的種種便利.
但是,假如dll中返回的參數,不是形如int, double, bool這樣可以直接轉化為.NET類型的對象,又該如 何使用P/Invoke呢?
比如我有這樣一個接口:
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的結構體
1 [StructLayout(LayoutKind.Sequential)]
2 public class GroupList
3 {
4 String GroupName;
5 int UserCount;
6 String[] UserNames;
7 }
然後寫一個如下的函數,試圖通過對Attribute的修飾來達到獲取返回的結構體的目的.
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. 但是,我們可以手動寫一個轉換:
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.所以,我們通過 groupCount = Marshal.ReadInt32 (groupListPtr) 把它讀出來
int
第二個member是group*.那就可以用 groupPtr = Marshal.ReadIntPtr(groupListPtr, 4);
IntPtr
讀出來.注意這裡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*)的 接口. 當然,解析的過程還是一樣的.