Marshal类支持从托管内存空间复制数据到非托管内存空间
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支持”的章节,可以获得更多的细节)。在本节中,我们讲到一些可用的机制,使用这些机制你可以进行另外一种方向的通信——从非托管代码到托管代码