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*)的 接口. 當然,解析的過程還是一樣的.