Marshal類支持從托管內存空間復制數據到非托管內存空間,或是從非托管內存空間到托管內存空間。如果你研究在線的MSDN文檔庫,你會看到在桌面.NET框架下這個類支持的分配非托管內存空間的方法和其他的一些與COM對象共同工作的方法。沒有任何內存空間管理和COM支持方法在.NET精簡框架的Marshal實現中出現。表4.5總結了Marshal類的被.NET精簡框架支持的成員:13方法名(有一個或多個重載版本)和1個只讀域。
表4.5 Marshal類中.NET精簡框架支持的成員
Marshal 成員 |
描述 |
在托管和非托管間復制 |
|
Copy |
在托管和非托管內存空間之間復制值類型數組。支持CLI整型,包括64位整型。支持單精度和雙精度浮點數。有14個重載的方法(7個用來復制到托管內存空間;7個用來復制到非托管內存空間) |
復制到非托管內存空間 |
|
StructureToPtr |
復制托管對象到非托管內存空間 |
WriteByte |
寫入一個字節(byte)到非托管內存空間 |
WriteInt16 |
寫入兩個字節到非托管內存空間 |
WriteInt32 |
寫入4個字節到非托管內存空間 |
復制到托管內存空間 |
|
PtrToStringUni |
在非托管內存空間中創建一個托管的字符串 |
PtrToStructure |
在非托管內存空間中創建一個對象 |
ReadByte |
從非托管內存空間中讀取一個字節 |
ReadInt16 |
從非托管內存空間中讀取兩個字節 |
ReadInt32 |
從非托管內存空間中讀取四個字節 |
信息的 |
|
IsComObject |
如果是硬編碼返回False |
SizeOf |
查詢一個對象實體的非托管大小。用來設置一些Win32函數調用的結構體大小的域值。 |
GetLastWin32Error |
調用GetLastError函數來取回Win32錯誤碼 |
SystemDefaultCharSize |
在默認的字符集中,字符大小的只讀的域。(在.NET精簡框架中返回2。)為了可移植性。 |
Marshal類的一些方法允許改寫非托管的緩存,於是你就能夠將它們作為參數傳遞到非托管函數中。這個類的另外一些方法可以讓你從非托管緩存中讀取值並寫入托管數據對象中。從緩存中改寫和讀取都是重要的,因為Win32 API(連同許多其它的基於C的API)為從一個調用者到一個被調函數的通信提供了使用緩存的擴展。
這個表不包括許多用來分配非托管內存的函數。下面的內存分配函數在MSDN庫中有所說明,並且內建在桌面.NET框架中,但是他們不被.NET精簡框架所支持。
ü AllocHGlobal
ü FreeHGlobal
ü AllocCoTaskMem
ü FreeCoTaskMem
在你從托管內存中讀取或寫入之前,你需要獲得一些非托管內存空間。在深入到Marshal類的內存復制方法中之前,我們需要看一看一個.NET精簡框架程序員如何處理內存分配。
分配非托管內存空間
我們稱之為“非托管”內存是因為運行時的垃圾收集器不會管理內存。而你必須管理你分配的內存,這就意味着當你不再使用它的時候,需要釋放這些內存。沒有釋放內存空間會導致內存泄漏。當內存的泄露到一定程度的時候,你的程序或操作系統本身可能會崩潰。你必須小心釋放任何你所分配的內存。(關於分配內存和相關的清理的Win32函數的總結,請看附件D。)
你已經具有了釋放自己分配的內存的責任心,接下來需要訓練你記住一些准則來正確的做這些工作。當我們編寫分配內存的代碼時,我們總是在寫過分配內存的代碼之后立即編寫釋放內存的代碼。然后我們要檢查以確定是否釋放了每個可能的代碼路徑分配的內存,不僅僅是成功的情況,更重要的是也包括當錯誤條件存在時的處理。這些努力是要避免內存的泄漏,即一個Win32 API的主要問題(並且是.NET的出現如此重要的原因,托管代碼可以自動完成這些工作)。
為了在.NET精簡框架下分配非托管內存空間,你不得不調用Win32分配內存的函數。可供選擇的一些函數列在下面:
ü私有分頁符: VirtualAlloc, VirtualFree
ü共享分頁符: MapViewOfFiles
ü堆分配器: HeapCreate, HeapAlloc
ü本地分配器: LocalAlloc, LocalFree
üCOM 分配器: CoTaskMemAlloc, CoTaskMemFree
每個內存類型有一個合適的用途,你能夠通過創建對應的P/Invoke聲明來選擇一個內存分配器。我們使用最后兩個類型——本地分配器和COM分配器——來實現在Marshal類中的定義。但是在.NET精簡框架中沒有實現的四個內存分配函數。我們選擇這些函數來協助從桌面導入.NET代碼到設備中。代碼清單4.8包含YaoDurant.Allocator.Marshal類的源代碼,它實現了四個分配器函數。我們把它留給你用來包含進你自己的函數中去,或者你能夠如你所需地復制粘貼這些代碼到你的工程中。
using System;
using System.Data;
using System.Runtime.InteropServices;
namespace YaoDurant.Allocator
{
/// <summary>
/// Summary description for Class1.
/// </summary>
public class Marshal
{
public Marshal()
{
//
// TODO: Add constructor logic here
//
}
//------------------------------------------------------------
// Allocate / free COM memory
//------------------------------------------------------------
[DllImport("ole32.dll")]
public static extern IntPtr CoTaskMemAlloc(int cb);
[DllImport("ole32.dll")]
public static extern void CoTaskMemFree(IntPtr pv);
public static IntPtr AllocCoTaskMem(int cb)
{
return CoTaskMemAlloc(cb);
}
public static void FreeCoTaskMem(IntPtr ptr)
{
CoTaskMemFree(ptr);
}
//------------------------------------------------------------
// Allocate / free regular heap memory
//------------------------------------------------------------
[DllImport("coredll.dll")]
public static extern IntPtr
LocalAlloc(int fuFlags, int cbBytes);
public const int LMEM_FIXED = 0x0000;
public const int LMEM_ZEROINIT = 0x0040;
public static IntPtr AllocHGlobal(int cb)
{
return LocalAlloc(LMEM_FIXED | LMEM_ZEROINIT, cb);
}
public static IntPtr AllocHGlobal(IntPtr cb)
{
return AllocHGlobal(cb.ToInt32());
}
[DllImport("coredll.dll")]
public static extern IntPtr LocalFree(IntPtr hMem);
public static void FreeHGlobal(IntPtr hglobal)
{
LocalFree(hglobal);
}
} // class
} // namespace
在代碼清單4.6中有兩個分配器函數:一個用於常規的堆內存(AllocHGlobal),另一個是用於COM共享組件的分配器函數。在大多數情況中,你可以使用常規的堆內存分配器。COM分配器允許一個組件分配一些其他組件釋放的內存。
Win32 API的長期愛好者可能想知道為什么我們調用LocalAlloc函數來處理常規的堆內存而不是使用GlobalAlloc函數。這兩個函數在”恐龍”的年代中是非常不同的(當然是指16位的windows統治世界的時候),而在Win32 API中這兩套函數間的差異在已經消失了。因為這些冗余,全局的分配器在Windows CE中不被支持,只留下本地的分配器作為”史前”分配器的唯一幸存者。
復制到非托管內存
非托管函數的許多參數是傳值的簡單值類型。一些值類型是傳引用的。在這兩個例子中,.NET精簡框架的內建P/Invoke支持是足夠的。一個傳引用的值類型參數使我們得到一個發送到非托管代碼的指針,而.NET精簡框架知道如何創建這種隱含的指針。.NET精簡框架甚至能夠處理許多結構體,只要結構體只包含簡單的值類型。這些結構體被作為傳值參數發送,這使得一個指針被獲得並發送到非托管代碼中15。在所有的這些內建情況中.NET精簡框架完成了全部的工作而不需要使用IntPtr和Marshal。
當處理手工參數傳遞時,當參數的方向是[in]或者[in][out]時,在調用目標函數之前,你必須通過復制值到內存來初始化非托管內存。在這一節,我們會討論復制到非托管內存空間。
傳入一些數據到非托管內存空間,你首先要分配一個非托管緩存空間並存儲結果指針在一個IntPtr中。接下來,調用Marshal類的一個成員來復制數據到緩存中。然后傳遞一個指針到非托管函數。最后一個步驟是釋放分配的內存。
為了演示,讓我們重新定義MessageBox函數來接受一個用來說明的InPtr(代替在前面例子中的字符串)。為了簡化,我們改變了兩個字符串參數中的一個,caption:
// 帶IntPtr標題的消息框
[DllImport("coredll.dll")]
public static extern int
MessageBox(int hWnd, String lpText, IntPtr lpCaption,
UInt32 uType);
注意IntPtr參數是一個傳值參數。點擊按鈕響應來調用這個函數的代碼在代碼清單4.7中。
private void button1_Click(object sender, System.EventArgs e)
{
// 創建字符串
string strCaption = "Caption";
string strText = "Text";
// 獲得字符串中字符的數量
int cch = strCaption.Length;
// 從字符串創建字符數組
char[] achCaption = new char[cch];
strCaption.CopyTo(0, achCaption, 0, cch);
// 分配非托管緩存
IntPtr ipCaption = AllocHGlobal((cch+1) *
Marshal.SystemDefaultCharSize);
if (! ipCaption.Equals(IntPtr.Zero))
{
// 復制字符到非托管緩存
Marshal.Copy(achCaption, 0, ipCaption, cch);
MessageBox(IntPtr.Zero, strText, ipCaption, 0);
FreeHGlobal(ipCaption);
}
}
代碼顯示了當你使用IntPtr和Marshal來發送參數數據到一個非托管的函數中總是應該遵循的四個步驟:
分配內存空間。內存分配由調用AllocHGlobal函數來實現,我們前面已經寫過的包裝Win32 LocalAlloc內存分配器的函數。
復制數據到內存空間。我們通過調用Marshal.Copy方法來復制數據到內存空間。
傳遞一個指針作為函數參數,如在我們例子中的MessageBox函數的第三個參數。注意我們需要一個與前面我們遇到的不同的MessageBox函數的P/Invoke聲明。
釋放內存空間。我們通過調用FreeHGlobal函數來做這個工作,它是我們為LocalFree函數編寫的包裝。
如果被調用函數寫入任何值到緩存中,我們可能需要另外的步驟。附加的步驟是在步驟三之后,步驟四之前,它從緩存中復制這些值到托管內存。
當然我們不需要做這些來傳遞一個字符串到非托管函數。但是同樣的方法在任何種類的數組來處理手工參數封送處理時同樣奏效。畢竟一個字符串是一個字符的數組。當然,在Windows CE中這意味着一個2字節的Unicode字符數組。
字符串類的封送處理
在桌面.NET框架中,字符串被自動封送處理使用了在這里顯示的相似的技術——內存被分配並且字符串被復制到內存空間。相對地,在.NET精簡框架中自動字符串封送處理不涉及額外的內存空間和復制工作,所以它更快並使用更少的內存。在.NET精簡框架中,非托管函數獲得一個指向托管字符串的內部內存空間的指針。
從非托管內存創建對象
當你手工進行[in][out]或者[out]方向的參數傳遞時,被調用函數寫入一些數據到非托管內存空間中。為了檢索這些數據,你要在非托管內存空間上創建一個托管對象。
當調用一個Win32函數來接受一個指針參數時,調用者分配緩存空間16。這是一個用於Win32 API中每個部分的標准。當我們教授Windows編程課程時,我們注意到剛接觸Win32的程序員經常會被這個問題絆倒。但是經過實踐,任何程序員都可以成為這種函數調用風格的專家。
當返回結構只包含簡單值類型的時候,自動的P/Invoke支持能夠為你復制這些值到一個托管結構體中。當結構體包含數組,字符串,指針,或者任何其他引用類型時,你必須手工創建一個托管對象。FindMemoryCard例子獲得從非托管函數調用返回的內存空間,並且顯示出如何使用Marshal類的不同靜態方法來裝配托管代碼對象。
例子:FindMemoryCard
就像我們在第十一章中討論的那樣,每個Windows CE系統的主要的存儲區域是對象存儲(object store),它和其他的內存部分中包含一個基於RAM的文件系統。這個文件系統具有許多和桌面Windows一樣的元素:由目錄和文件組成的等級結構,支持長文件名,同樣的最大路徑長度(260字符)。因為對象存儲是在可變RAM上的,電池可操作(battery-operated)的智能設備通常有一個備用的電源來防止災難性的數據丟失。
為了補充對象存儲,許多Windows CE設備有不可變的存儲17。例如,Pocket PC通常有一個插槽提供給緊湊式閃存卡,或者一個安全數據卡(也叫多媒體存儲卡)。許多構建在Windows Mobile技術上的Smartphone有一個安全數據卡插槽。另外的一些可以用於Windows CE設備的存儲設備包括硬盤驅動器,軟盤驅動器和可擦寫CD設備。
與桌面Windows不同,桌面Windows下可能會有一個C:驅動器和一個D:驅動器,Windows CE沒有使用字母來區分不同的文件系統。而是一個可安裝的文件系統以一個路徑安裝在文件系統的根目錄下。例如,我們通常遇到/Storage Card作為一個緊湊式閃存卡的根路徑。你通過在路徑字符串包含着一個根路徑來讀寫這個存儲設備上的路徑和文件,你可以傳遞這個路徑字符串到各種文件訪問函數。於是,要在前面的命名的精簡閃存卡的根路徑下創建一個名叫data.dat的文件,你就要創建一個名叫/Storage Card/data.dat的文件。
然而,不是所有的存儲設備都使用這個根/Storage Card。為了確定“已安裝的文件系統是否存在,如果存在那么它根的名字是什么”這些問題,你可以調用下面的Win32函數:
ü FindFirstFlashCard
ü FindNextFlashCard
這些函數需要填寫一個名叫WIN32_FIND_DATA的結構體,順便提一下,通用目的的Win32文件系統枚舉函數FindFirstFile和FindNextFile填寫的也是這個結構體。數據結構定義如下:
typedef struct _WIN32_FIND_DATA {
DWORD dwFileAttributes;
FILETIME ftCreationTime;
FILETIME ftLastAccessTime;
FILETIME ftLastWriteTime;
DWORD nFileSizeHigh;
DWORD nFileSizeLow;
DWORD dwOID;
TCHAR cFileName[MAX_PATH];
} WIN32_FIND_DATA;
WIN32_FIND_DATA結構體包含四個使它不可能應用自動參數封送處理的元素:三個FILETIME值和一個字符數組。我們的例子帶有這兩個使用非托管內存的Win32函數,並且它的調用已經被放置到LocalAlloc函數中。調用之后進入訪問非托管內存塊,並復制結構體的值來提供與非托管數據結構等價的數據結構的函數中,顯示如下。
public struct WIN32_FIND_DATA
{
public int dwFileAttributes;
public FILETIME ftCreationTime;
public FILETIME ftLastAccessTime;
public FILETIME ftLastWriteTime;
public int nFileSizeHigh;
public int nFileSizeLow;
public int dwOID;
public String cFileName;
};
對在這個結構體中的每個元素,轉換函數讀取一個值,然后自增指針准備讀取下一個值。轉換函數顯示在代碼清單4.8中,使得IntPtr類型參數pln作為一個指向非托管內存的指針。這個函數假設輸出已經獲得並發送一個已經被實例化類型為WIN32_FIND_DATA的托管對象。因為我們喜歡重用代碼,我們編寫的這個函數為了你在調用泛形Win32文件系統遍歷函數時也能使用它(FindFirstFile和FindNextFile)。
private static void
CopyIntPtr_to_WIN32_FIND_DATA(IntPtr pIn,
ref WIN32_FIND_DATA pffd)
{
// Handy values for incrementing IntPtr pointer.
int i = 0;
int cbInt = Marshal.SizeOf(i);
FILETIME ft = new FILETIME();
int cbFT = Marshal.SizeOf(ft);
// int dwFileAttributes
pffd.dwFileAttributes = Marshal.ReadInt32(pIn);
pIn = (IntPtr)((int)pIn + cbInt);
// FILETIME ftCreationTime;
Marshal.PtrToStructure(pIn, pffd.ftCreationTime);
pIn = (IntPtr)((int)pIn + cbFT);
// FILETIME ftLastAccessTime;
Marshal.PtrToStructure(pIn, pffd.ftLastAccessTime);
pIn = (IntPtr)((int)pIn + cbFT);
// FILETIME ftLastWriteTime;
Marshal.PtrToStructure(pIn, pffd.ftLastWriteTime);
pIn = (IntPtr)((int)pIn + cbFT);
// int nFileSizeHigh;
pffd.nFileSizeHigh = Marshal.ReadInt32(pIn);
pIn = (IntPtr)((int)pIn + cbInt);
// int nFileSizeLow;
pffd.nFileSizeLow = Marshal.ReadInt32(pIn);
pIn = (IntPtr)((int)pIn + cbInt);
// int dwOID;
pffd.dwOID = Marshal.ReadInt32(pIn);
pIn = (IntPtr)((int)pIn + cbInt);
// String cFileName;
pffd.cFileName = Marshal.PtrToStringUni(pIn);
}
在非托管代碼和托管代碼間通信
P/Invoke支持單向的函數調用——從托管代碼到非托管代碼。這與我們在.NET框架中能夠找到的支持有所不同,在.NET框架中回調函數被支持(查看前面“比較P/Invoke支持”的章節,可以獲得更多的細節)。在本節中,我們講到一些可用的機制,使用這些機制你可以進行另外一種方向的通信——從非托管代碼到托管代碼