Microsoft .NET Compact Framework 上的 P/Invoke 和封送处理简介
Microsoft .NET Compact Framework 上的 P/Invoke 和封送处理简介
发布日期 : 10/29/2004 | 更新日期 : 10/29/2004
Jon Box, Dan Fox
Quilogy
编著:
Jonathan Wells
微软公司
Jim Wilson
JW Hedgehog, Inc.
适用于:
Microsoft_ .NET Compact Framework 1.0
Microsoft_ Visual Studio_ .NET 2003
摘要:学会如何使用 .NET Compact Framework 的平台调用(P/Invoke) 功能。
本页内容
简介
使用 P/Invoke
封送处理数据
小结
简介
为了实现任何地点任何时间在任何设备上访问信息的目标,开发人员需要可靠的功能丰富的工具。随着 Microsoft .NET Compact Framework 和 Visual Studio .NET 2003 中智能设备项目的发布,Microsoft 使开发人员可以利用其现有的 Microsoft .NET Framework 的知识更容易地将他们的应用程序扩展到智能设备上。
.NET Compact Framework 面向 Pocket PC 2000、2002,以及嵌入式 Microsoft_ Windows_ CE .NET 4.1设备,而且作为桌面框架的一个子集进行开发。Compact Framework 和 Visual Studio .NET 2003 支持的智能设备项目允许开发人员编写在公共语言运行库上执行的 Visual Basic 或 C# 托管代码,这些代码与完整的 .NET 框架上的代码类似。
但是,作为 .NET Framework 的一个子集,.NET Compact Framework 支持整个命名空间中大约 25% 的类型,另外还有一些 .NET Compact Framework 特定的类型,用于处理用户输入、消息以及 Microsoft_ SQL?? Server 2000 Windows CE Edition 2.0。对于开发人员而言,这意味着有一些功能要通过操作系统 (Windows CE) API 才能访问。例如,Pocket PC 2002 平台中包含的通知服务在 .NET Compact Framework 中没有相应的托管服务。另外,您可能需要访问许多第三方以及自定义的 DLL:例如,使用 Pocket PC 2002 Phone Edition 设备进行电话呼叫以及检索呼叫日志的 API。
幸运的是,.NET Compact Framework(与所对应的桌面版类似)确实支持平台调用(P/Invoke) 服务。这项服务允许托管代码调用驻留于 DLL 中的非托管函数。虽然 .NET Compact Framework 支持 P/Invoke,但它和完整的 .NET Framework 稍有不同。在本篇及之后的一篇白皮书中,我们将探讨在 Compact Framework 中使用 P/Invoke 的问题,主要集中于您将遇到的各种相同及不同之处。本文假定您对完整的 .NET Framework 有基本的了解。
返回页首
使用 P/Invoke
与在 .NET Framework 中一样,在 .NET Compact Framework 中使用 P/Invoke 服务包括三个主要步骤:声明、调用,以及错误处理。在讲述完这些步骤后,我们将查看 .NET Compact Framework 和完整的 .NET Framework 中 P/Invoke 服务的区别。
声明
首先,在设计时,您必须告诉 .NET Compact Framework您想要调用的非托管函数。您必须包括 DLL 名(也称为模块),函数名(也称为入口点)以及所使用的调用约定。例如,如果要调用 SHGetSpecialFolderPath 函数以返回 Windows CE 设备上各个系统文件夹的路径,您需要使用 the DllImportAttribute; 或 VB 中的 DllImportAttribute 或 the Declare 语句声明此函数。
[VB]
Private Declare Function SHGetSpecialFolderPath Lib "coredll.dll" ( _
ByVal hwndOwner As Integer, _
ByVal lpszPath As String, _
ByVal nFolder As ceFolders, _
ByVal fCreate As Boolean) As Boolean
[C#]
[DllImport("coredll.dll", SetLastError=true)]
private static extern bool SHGetSpecialFolderPath(
int hwndOwner,
string lpszPath,
ceFolders nFolder,
bool fCreate);
在这两个例子中,您可以看到声明包含了该函数所驻留的 DLL 的名字(coredll.dll,许多 Windows CE API 驻留其中,类似于 Win32 API 中的 kernel32.dll 和 user32.dll )和函数名(SHGetSpecialFolderPath)。另外,此 VB 函数被标记为 Private ,所以该函数只能在声明此函数的类中进行调用。它也可以被标记为 Public 或 Friend ,这取决于您的应用程序如何调用该函数。
注 与基于完整的 .NET Framework 的应用程序一样,将非托管的 API 声明分组到一个适当的命名空间(如 Quilogy.CeApi )的类中并通过共享的包装方法公开其函数是一种很好的实践做法。该类可以打包到其自身的程序集中,然后分发到您的团队或组织中的所有开发人员。
在 C# 声明中,必须用 static 和 extern 关键字来指明函数是在外部实现的,可以在没有创建该类实例的情况下调用(虽然函数也被标记为 private,因此它仍是隐藏的)。
您还可以看到,函数需要一个 ceFolders 类型的参数。事实上,函数需要一个 32 位的整数 CSIDL 值,并映射到其中一个 Windows CE API 常量。在这些情况下,用枚举公开常量值(参见 msdn.microsoft.com中的 API 文档)是一种很好的实践做法,如下面的代码片段所示。
[VB]
Private Enum ceFolders As Integer
PROGRAMS = 2 ???? \Windows\Start Menu\Programs
PERSONAL = 5 ???? \My Documents
STARTUP = 7 ???? \Windows\StartUp
STARTMENU = &HB ???? \Windows\Start Menu
FONTS = &H14 ???? \Windows\Fonts
FAVORITES = &H16 ???? \Windows\Favorites
End Enum
正如 .NET Framework 一样,Declare 语句支持 Alias 子句,该子句允许您在 DLL 中为该函数指定一个不同的名称。这在 DLL 中的函数名与关键字或者您代码中已定义的另一个函数相冲突时非常有用。 DllImportAttribute 属性也支持这个功能,它通过 EntryPoint 属性(该属性可以被添加到属性声明中)实现,如下所示。
[DllImport("coredll.dll", EntryPoint="SHGetSpecialFolderPath")]
static extern bool GetFolderPath( //the rest of the declaration
注 Alias 子句或 EntryPoint 属性均能包含 DLL 中函数的序号值,格式为 “#num”,如“#155”。
最后您可以看到,在 C# 声明中, SetLastError 属性被设置为 true (默认值为 false )。这指定公共语言运行库调用 Windows CE GetLastError 函数以缓存返回的错误值,这样其他函数就不会重写该值。然后,您可以用 Marshal.GetLastWin32Error 安全地检索错误。在 Declare 语句中,此值假定为 true,在 VB 中使用 DllImportAttribute 时也是如此。
调用
一旦正确声明了要调用的函数(通常在一个实用工具类中),您就可以在该类的方法中对函数调用进行包装。常用的技巧是将包装函数声明为该类的一个 Public Shared (或 C# 中的 static )方法,如下所示。
Namespace Quilogy.CeApi
Public Class FileSystem
Private Sub New()
???? Prevents creation of the class
End Sub
Public Shared Function GetSpecialFolderPath( _
ByVal folder As ceFolders) As String
Dim sPath As String = New String(" "c, MAX_PATH)
Dim i As Integer
Dim ret As Boolean
Try
ret = SHGetSpecialFolderPath(0, sPath, folder, False)
Catch ex As Exception
HandleCeError(ex, "GetSpecialFolderPath")
Return Nothing
End Try
If Not ret Then
???? API Error so retrieve the error number
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"SHGetSpecialFolderPath returned False, " & _
"likely an invalid constant", errorNum), _
"GetSpecialFolderPath")
End If
Return sPath
End Function
???? Other methods
End Class
End Namespace
在这个例子中,对 SHGetSpecialFolderPath 的调用包装于 Quilogy.CeApi.FileSystem 类的 GetSpecialFolderPath 方法中,并且负责向 DLL 函数传递正确的参数以及处理各种错误。在本例中,只公开包装中需要客户端提供的参数是一种很好的实践做法。其余的可以默认设为适当的值。您还可以看到该类包含了一个私有的构造函数以防止创建该类的实例。
然后,该方法可以由调用方以下面的方式进行调用:
Imports Quilogy
...
Dim docs As String
docs = CeApi.FileSystem.GetSpecialFolderPath(ceApi.ceFolders.PERSONAL)
当这段代码在运行时被实时 (JIT) 编译之后,公共语言运行库的 P/Invoke 服务将 Declare 或 DllImportAttribute 定义从程序集的元数据中提取出来,对包含该函数的 DLL 进行定位并加载到内存,然后使用入口点信息检索该函数的地址。如果一切运行正常,该函数将被调用,其参数被封送处理,并将各种返回值返回到调用方。
处理错误
虽然开发人员从来不希望他们的代码会产生运行时错误,但是要记住使用 P/Invoke 在 DLL 上调用的函数可能会产生两种错误。
第一种是由 PInvoke 服务本身产生的异常。当传给此方法的参数包含无效数据,或函数本身以不恰当的参数进行声明时,这种情况就会产生。在此情况下,将会引发 NotSupportedException 。出现这种情况时,您应该重新检查您的声明以确定它是否与实际的 DLL 定义相匹配。或者,P/Invoke 可能引发 MissingMethodException,此异常正如其名字所示,是由于找不到入口点而产生的。在 GetSpecialFolderPath 方法中,这些异常在 Try Catch 块中被捕获,然后传递到另一个名为 HandleCeError????自定义方法中。该方法检查传入异常的类型,然后用适当的消息引发一个 WinCeException(派生于 ApplicationException)类型的自定义异常,它们均在下面的列表中显示。这种包装异常的技术在 .NET Compact Framework 中是有效的,因为它集中了错误处理并允许将自定义成员(如Windows CE 错误号)添加到自定义的异常类中。
Private Shared Sub HandleCeError(ByVal ex As Exception, _
ByVal method As String)
???? Do any logging here
???? Swallow the exception if asked
If Not ExceptionsEnabled Then
Return
End If
If TypeOf ex Is NotSupportedException Then
???? Bad arguments or incorrectly declared
Throw New WinCeException( _
"Bad arguments or incorrect declaration in " & method, 0, ex)
End If
If TypeOf ex Is MissingMethodException Then
???? Entry point not found
Throw New WinCeException( _
"Entry point not found in " & method, 0, ex)
End If
If TypeOf ex Is WinCeException Then
Throw ex
End If
???? All other exceptions
Throw New WinCeException( _
"Miscellaneous exception in " & method, 0, ex)
End Sub
Public Class WinCeException : Inherits ApplicationException
Public Sub New()
End Sub
Public Sub New(ByVal message As String)
MyBase.New(message)
End Sub
Public Sub New(ByVal message As String, ByVal apiError As Integer)
MyBase.New(message)
Me.APIErrorNumber = apiError
End Sub
Public Sub New(ByVal message As String, _
ByVal apiError As Integer, ByVal innerexception As Exception)
MyBase.New(message, innerexception)
Me.APIErrorNumber = apiError
End Sub
Public APIErrorNumber As Integer = 0
End Class
您可以看到 HandleCeError 方法首先检查共享字段 ExceptionsEnabled ,如果为 false,则只需从方法中返回。这个共享字段允许调用方指定异常是否会从该类的方法中引发。
注 前面的代码列表中显示的错误字符串也可以放置于一个资源文件中,打包成附属的程序集,然后使用 ResourceManager 类动态检索,正如在 VS .NET 帮助中所描述的那样。
第二种可能产生的错误类型是从 DLL 函数本身返回的错误。在 GetSpecialFolderPath 方法的例子中,当 SHGetSpecialFolderPath 返回 False 时这种情况就会发生(例如,如果枚举包含一个无效的或不受支持的常量)。如果是这种情况,该方法使用 GetLastWin32Error 检索出错误号,并将错误号通过其重载构造函数传递给 HandleCeError 方法。
.NET Compact Framework 的不同之处
虽然上文显示的声明在 .NET Compact Framework 和完整的 .NET Framework中是一样的(除了模块名),但还是有一些细微的差别。
全部是 Unicode编码,并且在任何时候。在完整的 .NET Framework 中,默认的字符集可以使用 Declare 语句中的 Ansi、 Auto,或 Unicode 子句和 DllImportAttribute 中的 CharSet 属性进行设置,默认的字符集控制字符串参数的封送处理行为和要使用的精确入口点名称(P/Invoke 可附加“A”指定 ANSI 或者附加“W”指定 Unicode,这取决于 ExactSpelling 属性)。在完整的 .NET Framework 中,默认的是 ANSI,然而 .NET Compact Framework 只支持 Unicode,因此只包含 CharSet.Unicode (和等同于 Unicode 的 CharSet.Auto)值,也不支持 Declare 语句的任何子句。这意味着 ExactSpelling 属性同样是不受支持的。因此,如果您的 DLL 函数期望的是一个 ANSI 字符串,您就需要在 DLL 中执行转换,或在调用函数之前使用 ASCIIEncoding 类重载的 GetBytes 方法将字符串转换为一个字节数组,因为 .NET Compact Framework 总是传递指向 Unicode 字符串的指针。
一个调用约定。完整的 .NET Framework 支持三种不同的调用约定(调用规则确定了参数传递给函数的顺序以及谁负责清理堆栈这些问题),使用用于 DllImportAttribute 的 CallingConvention 属性的 CallingConvention 枚举。但是,对于 .NET Compact Framework 而言,只支持 Winapi 值(默认的平台约定),也就是默认的被称为 Cdecl 的 C 和 C++ 调用规则。
单向。虽然参数可以通过值或引用传递给 DLL 函数,允许 DLL 函数将数据返回给 .NET Compact Framework 应用程序,但 .NET Compact Framework 中的 P/Invoke 并不能像完整的 .NET Framework 一样支持回调。在完整的 .NET Framework 中,回调通过使用传递给 DLL 函数的委派(面向对象的函数指针)而得到支持。然后,DLL 函数在该委派的地址中利用函数的结果调用托管的应用程序。一个使用回调的典型例子是 EnumWindows API 函数,它用以枚举所有顶层窗口并将其句柄传递给回调函数。
不同的异常。在完整的 .NET Framework 中,如果不能定位函数或函数没有正确声明,则(通常)分别会引发 EntryPointNotFoundException 和 ExecutionEngineException 异常。而在 .NET Compact Framework 中,正如上文所述,将引发 MissingMethodException 和 NotSupportedException 类型的异常。
处理 Windows 消息。通常,当处理操作系统 API 时,必须将窗口的句柄 (hwnd) 传递给函数,或为操作系统发送的消息添加自定义处理。在完整的 .NET Framework Form 类公开了 Handle 属性以满足前者的要求,而以重写 DefWndProc 方法来处理后者。事实上,这些成员是通过 Control 基类公开的,所以此功能对所有类都是可用的,包括 ListBox、 Button(从 Control派生)等。.NET Compact Framework 中的 Form 类两者均不包含,但它包括了 Microsoft.WindowsCE.Forms 命名空间中的 MessageWindow 和 Message 类。 MessageWindow 类可以继承,而且其 WndProc 方法可以被重写,用以捕捉 Message 类型的特定消息。基于 .NET Compact Framework 的应用程序甚至可以使用 MessageWindow 类的 SendMessage 和 PostMessage 方法向其它窗口发送消息。例如,当检查确定基于 .NET Compact Framework 的应用程序是否已经运行时, PostMessage 可以用来向所有顶层窗口广播自定义消息,请参见在 smartdevices.microsoftdev.com Jonathan Wells 张贴的示例代码。
返回页首
封送处理数据
在调用过程中,P/Invoke 服务负责封送处理传递给 DLL 函数的参数值。P/Invoke 的组件通常被称为封送拆收器。本节我们将讨论封送拆收器在处理通用类型、字符串、结构、非整数类型和一些其它问题过程中要完成的工作。
Blittable 类型
幸运的是,许多用来调用 DLL 函数的类型在 .NET Compact Framework 和非托管代码中具有同样的表示方法。这些类型被称为 blittable 类型,如下表所示。
Compact Framework 类型Visual Basic 关键字C# 关键字
System.ByteBytebyte
System.SByten/asbyte
System.Int16Shortshort
System.UInt16n/aushort
System.Int32Integerint
System.Int64Long (only ByRef)long (only ref)
System.UInt64n/aulong
System.IntPtrn/a* using unsafe
或者说,封送拆收器在托管和非托管代码间转换时不需要对这些类型定义的参数执行任何特殊处理。事实上,通过扩展,封送拆收器不需要转换这些类型的一维数组,或甚至是只包含这些类型的结构和类。
此行为与在完整的 .NET Framework 框架上一样,另外 .NET Compact Framework 也包括 System.Char (VB 中的Char ,C# 中的 char ), System.String (VB 中的String ,C# 中的 string ), System.Boolean (VB 中的Boolean ,C# 中的 bool )作为 blittable 类型。对于 System.Char 和 System.String,正如前文所述,这是由于 .NET Compact Framework ??????§???? Unicode,所以封送拆收器总是将前者作为一个 2 字节的 Unicode 字符进行封送处理,而后者则是作为一个 Unicode 数组。对于 System.Boolean,.NET Compact Framework的封送拆收器使用 1 字节的整数值,而在完整的 .NET Framework 中使用 4 字节的整数值。
警告 虽然 System.String 在 .NET Compact Framework 中是 blittable 类型,但当它用于复杂对象(如结构或类)内部时,它就不是 blittable 类型了。这将在后面的文章中详细探讨。
很明显,在 .NET Compact Framework 应用程序中使用这些类型可使您的代码简化。另外一个使用 blittable 类型的简单例子是 CeRunAppAtEvent 函数的调用,它允许 ActiveSync 数据同步结束时启动 .NET Compact Framework 的应用程序。
Public Enum ceEvents
NOTIFICATION_EVENT_NONE = 0
NOTIFICATION_EVENT_TIME_CHANGE = 1
NOTIFICATION_EVENT_SYNC_END = 2
NOTIFICATION_EVENT_DEVICE_CHANGE = 7
NOTIFICATION_EVENT_RS232_DETECTED = 9
NOTIFICATION_EVENT_RESTORE_END = 10
NOTIFICATION_EVENT_WAKEUP = 11
NOTIFICATION_EVENT_TZ_CHANGE = 12
End Enum
Public Class Environment
Private Sub New()
End Sub
_
Private Shared Function CeRunAppAtEvent(ByVal appName As String, _
ByVal whichEvent As ceEvents) As Boolean
End Function
Public Shared Function ActivateAfterSync() As Boolean
Dim ret As Boolean
Try
Dim app As String
app = _
System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase
ret = CeRunAppAtEvent(app, ceEvents.NOTIFICATION_EVENT_SYNC_END)
If Not ret Then
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"CeRunAppAtEvent returned false", errorNum), _
"ActivateAfterSync")
End If
Return ret
Catch ex As Exception
HandleCeError(ex, "ActivateAfterSync")
Return False
End Try
End Function
Public Shared Function DeactivateAfterSync() As Boolean
Dim ret As Boolean = False
Try
Dim app As String
app = _
System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase
ret = CeRunAppAtEvent(app, ceEvents.NOTIFICATION_EVENT_NONE)
If Not ret Then
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"CeRunAppAtEvent returned false", errorNum), _
"DeactivateAfterSync")
End If
Return ret
Catch ex As Exception
HandleCeError(ex, "DeactivateAfterSync")
Return False
End Try
End Function
???? other stuff
End Class
注 在调用 ActivateAfterSync 方法后,ActiveSync 同步结束时,应用程序的实例将以一个特定的命令行参数自动启动。为了检测命令行并激活已经存在的应用程序实例,通常要向智能设备项目中添加额外的代码。
在本例中, SHGetSpecialFolderPath 中所有参数和返回值都是 blittable 类型,所以您或者封送拆收器均不需要进行额外的工作。您会注意到,在这种情况下, ceEvents 枚举是作为基础值类型被封送处理的,默认为 blittable System.Int32 。但是,应该注意到,封送拆收器只能处理 32 位或更少位的返回类型。
传递字符串
如上文所述,.NET Compact Framework 中的字符串是 blittable 类型的,它对于非托管函数而言是以 null 终止的 Unicode 字符数组。在调用时,因为 System.String 是引用类型(在托管的堆上进行分配,其地址保存于引用变量中),所以即使它是通过值传递的,封送拆收器也是将字符串的指针传递给非托管函数,在 SHGetSpecialFolder 和 CeRunAppAtEvent 中就是如此。
注 .NET Compact Framework 总是传递引用类型的指针,它不支持通过引用传递引用类型(VB 中的 ByRef,C# 中的 ref)。
但是,您一定注意到 SHGetSpecialFolder 期望的是一个固定长度的字符串缓冲区,它可以用于保存路径值,而 CeRunAppAtEvent 只是简单地读取字符串。对于前者的情况,固定长度如下进行声明(c 表示转换为 System.Char)。
Dim sPath As String = New String(" "c, MAX_PATH)
字符串的指针被传递给非托管函数之后,非托管的代码将字符串看作是 WCHAR * (或 TCHAR *、 LPTSTR, 也可能是 LPSTR),并且可以使用指针对字符串进行操作。当函数完成后,调用方可以正常检查字符串。
这一行为和完整的 .NET Framework 是完全不同的,后者的封送拆收器必须考虑字符集。因此,完整的 .NET Framework 不支持通过值或引用向非托管函数传递字符串,也不允许非托管函数修改缓冲区的内容。
要在完整的 .NET Framework 中解决决此问题(因为许多 Win32 API 要求字符串缓冲区),您可以改为传递一个 System.Text.StringBuilder 对象;指针将由封送拆收器传递给非托管函数进行操作。唯一要警告的是 StringBuilder 必须为返回值分配足够的空间,否则文本将溢出,从而使 P/Invoke 引发异常。
事实上 StringBuilder 在 .NET Compact Framework 中也能以同样的方式使用,因此 SHGetSpecialFolderPath 的声明可变为:
Private Declare Function SHGetSpecialFolderPath Lib "coredll.dll" ( _
ByVal hwndOwner As Integer, _
ByVal lpszPath As StringBuilder, _
ByVal nFolder As ceFolders, _
ByVal fCreate As Boolean) As Boolean
调用语法为:
Dim sPath As New StringBuilder(MAX_PATH)
ret = SHGetSpecialFolderPath(0, sPath, folder, False)
实际上,在这个特定的例子中,改为 StringBuilder 会更加有效,因为在 VB 中使用 Declare 语句时, ByVal String 参数将被作为一个外部参数进行封送处理。在函数在返回新引用之前返回时,使用外部参数将强制公共语言运行库创建一个新的 String 对象。 DllImportAttribute 不会引起这种行为。
在任何情况下,建议您在处理固定长度的字符串缓冲区时,使用 StringBuilder ,因为它们更容易初始化,并且与完整的 .NET Framework具 有更高的一致性。
传递结构
如上文所述,只要结构包含 blittable 类型,您就可以放心地将结构传递给非托管函数。例如, GlobalMemoryStatus Windows CE API 被传递了一个需要其填充的 MEMORYSTATUS 结构的指针,返回关于设备的物理及虚拟内存信息。因为该结构只包含 blittable 类型(在 .NET Compact Framework 中,非托管的 DWORD 值被转换为 32 位的无符号整数值 System.UInt32),所以此函数可以很容易地进行调用,如下面的代码片段所示。
Private Structure MEMORY_STATUS
Public dwLength As UInt32
Public dwMemoryLoad As UInt32
Public dwTotalPhys As UInt32
Public dwAvailPhys As Integer
Public dwTotalPageFile As UInt32
Public dwAvailPageFile As UInt32
Public dwTotalVirtual As UInt32
Public dwAvailVirtual As UInt32
End Structure
_
Private Shared Sub GlobalMemoryStatus(ByRef ms As MEMORY_STATUS)
End Sub
Public Shared Function GetAvailablePhysicalMemory() As String
Dim ms As New MEMORY_STATUS
Try
GlobalMemoryStatus(ms)
Dim avail As Double = CType(ms.dwAvailPhys, Double) / 1048.576
Dim sAvail As String = String.Format("{0:###,##}", avail)
Return sAvail
Catch ex As Exception
HandleCeError(ex, "GetAvailablePhysicalMemory")
Return Nothing
End Try
End Function
注意在本例中, GetAvailablePhysicalMemory 函数实例化了一个 MEMORY_STATUS 结构并将它传递给 GlobalMemoryStatus 函数。因为函数需要此结构的指针(在 Windows CE 中定义为 LPMEMORYSTATUS ), GlobalMemoryStatus 的声明指出 ms 参数是 ByRef。在 C# 中,声明和调用都需要使用引用关键字。
或者说,此函数可以通过将 MEMORY_STATUS 声明为一个类而不是一个结构进行调用。因为该类是引用类型的,您不需要将参数声明为通过引用传递,而是可以依赖于这样一个事实:封送拆收器总是向非托管函数传递一个 4 字节的指向引用类型的指针。
注意引用类型总是以其出现于托管代码中的次序进行封送处理,这一点很重要。这意味着,这些字段将如非托管函数所期望的那样布局于内存中。因此,您不需要用 StructLayoutAttribute 和 LayoutKind.Sequential对结构进行修饰,虽然这与在完整的 .NET Framework 上一样都是受支持的。
虽然您可以向非托管函数传递结构(和类),但是 .NET Compact Framework 的封送拆收器不支持封送处理从一个非托管函数返回的结构的指针。在这些情况下,您需要使用 Marshal 类的 PtrToStructure 方法对结构进行手动封送处理。
最后,.NET Compact Framework 中的封送拆收器和完整的 .NET Framework 中的封送拆收器之间的主要区别之一是:在 .NET Compact Framework中,封送拆收器不能在结构中封送处理复杂的对象(引用类型)。这意味着,如果结构中的任何字段含有除了上文表中列出的类型以外的类型(包括字符串或字符串数组),则不能对该结构进行完全的封送处理。这是由于 .NET Compact Framework 不支持完成的 .NET Framework 中使用的 MarshalAsAttribute,它向封送拆收器提供了关于如何封送处理数据的显式指令。但是,在之后的一篇文章中,我们将探讨引用类型(例如嵌入在结构中的字符串)如何被传递到非托管函数以及如何从非托管函数中返回。
传递非整型
注意在 blittable 类型表中没有提及浮点变量。这些类型是非整型的(不表示一个整数),不能由 .NET Compact Framework 通过值进行封送处理。但是,可以通过引用对它门进行封送处理,而且可以作为指向非托管函数(作为包装或填充程序)的指针传递。
注 将 64 位整数(VB 中Long ,C# 中 long )作为一个指向 INT64 的指针进行封送处理也是如此。
例如,如果非托管函数接受 double 类型的两个参数并返回一个 double,在 C 中定义如下:
double DoSomeWork(double a, double b) { }
接下来,为了从 .NET Compact Framework 中调用该函数,您首先需要使用嵌入式 Visual C 创建一个非托管函数,它通过引用(作为指针)接受两个参数,并且通过调用原始函数以输出参数的形式返回结果,如下所示:
void DoSomeWorkShim(double *pa, double *pb, double *pret)
{
*pret = DoSomeWork(pa, pb);
}
然后,使用 DllImportAttribute 语句在 Compact Framework 中声明 DoSomeWorkShim 函数,如下所示:
_
Private Shared Sub DoSomeWorkShim(ByRef a As Double, _
ByRef b As Double, ByRef ret As Double)
End Sub
最后,您可以再用一个附加的托管包装函数包装对 DoSomeWorkShim 的调用,此附加的包装函数维护了原始 DoSomeWork 的调用签名。
Public Shared Function DoSomeWork(ByVal a As Double, _
ByVal b As Double) As Double
Dim ret As Double
DoSomeWorkShim(a, b, ret)
Return ret
End Function
其它问题
一个让开发人员担心的有关 P/Invoke 的问题是它与公共语言运行库中的垃圾回收器 (GC) 的互操作性。因为当回收发生时,.NET Compact Framework 和完整的 .NET Framework 中的垃圾回收器都可能在托管堆上重新安排对象,所以如果非托管函数试图使用传递给它的指针操作内存,就可能出现问题。幸运的是,.NET Compact Framework 的封送拆收器在调用期间将自动锁定(将对象锁定在其当前内存位置)任何传递给非托管函数的引用类型。
第二个开发人员经常遇到的问题是 C# 中与 P/Invoke 相关的 unsafe 和 fixed 关键字的使用。基本上 unsafe 关键字允许 C# 代码直接通过使用指针对内存进行操作,正如???? C 中一样,虽然这允许了更大程度上的灵活性,但同时也使得公共语言运行库中的代码验证功能变为无效。该主题会在以后的文章中更深入地讨论。该功能使得公共语言运行库在 JIT 的过程中可以确保它所执行的代码只读取已分配的内存,并且所有方法在调用时,参数的数目和类型都正确。
注 除了创建无法验证的代码以外,在完整的 .NET Framework ????,不安全代码只能在一个受信任的环境中执行,虽然在 .NET Compact Framework 1.0 版中没有包括代码访问安全性 (CAS),但这现在已经不是问题。
fixed 关键字与 unsafe 关键字一起用于锁定托管对象,这通常是一个值类型或值类型数组,例如一个 System.Char 数组,所以当它被非托管函数使用时,GC不能对它进行移动。
返回页首
小结
正如您所看到的,.NET Compact Framework 的平台调用功能是完整的 .NET Framework 中可用功能的一个有用的子集。有了这项功能,您可以相当容易地实现大多数应用程序所需的对非托管 DLL 的调用。
对于更复杂的情况,例如传递嵌入于结构中的字符串,您可能需要求助于指针和内存分配,这正是下一篇白皮书的主题。
本文转自
http://msdn2.microsoft.com/zh-cn/library/aa446536.aspx
发布日期 : 10/29/2004 | 更新日期 : 10/29/2004
Jon Box, Dan Fox
Quilogy
编著:
Jonathan Wells
微软公司
Jim Wilson
JW Hedgehog, Inc.
适用于:
Microsoft_ .NET Compact Framework 1.0
Microsoft_ Visual Studio_ .NET 2003
摘要:学会如何使用 .NET Compact Framework 的平台调用(P/Invoke) 功能。
本页内容
简介
使用 P/Invoke
封送处理数据
小结
简介
为了实现任何地点任何时间在任何设备上访问信息的目标,开发人员需要可靠的功能丰富的工具。随着 Microsoft .NET Compact Framework 和 Visual Studio .NET 2003 中智能设备项目的发布,Microsoft 使开发人员可以利用其现有的 Microsoft .NET Framework 的知识更容易地将他们的应用程序扩展到智能设备上。
.NET Compact Framework 面向 Pocket PC 2000、2002,以及嵌入式 Microsoft_ Windows_ CE .NET 4.1设备,而且作为桌面框架的一个子集进行开发。Compact Framework 和 Visual Studio .NET 2003 支持的智能设备项目允许开发人员编写在公共语言运行库上执行的 Visual Basic 或 C# 托管代码,这些代码与完整的 .NET 框架上的代码类似。
但是,作为 .NET Framework 的一个子集,.NET Compact Framework 支持整个命名空间中大约 25% 的类型,另外还有一些 .NET Compact Framework 特定的类型,用于处理用户输入、消息以及 Microsoft_ SQL?? Server 2000 Windows CE Edition 2.0。对于开发人员而言,这意味着有一些功能要通过操作系统 (Windows CE) API 才能访问。例如,Pocket PC 2002 平台中包含的通知服务在 .NET Compact Framework 中没有相应的托管服务。另外,您可能需要访问许多第三方以及自定义的 DLL:例如,使用 Pocket PC 2002 Phone Edition 设备进行电话呼叫以及检索呼叫日志的 API。
幸运的是,.NET Compact Framework(与所对应的桌面版类似)确实支持平台调用(P/Invoke) 服务。这项服务允许托管代码调用驻留于 DLL 中的非托管函数。虽然 .NET Compact Framework 支持 P/Invoke,但它和完整的 .NET Framework 稍有不同。在本篇及之后的一篇白皮书中,我们将探讨在 Compact Framework 中使用 P/Invoke 的问题,主要集中于您将遇到的各种相同及不同之处。本文假定您对完整的 .NET Framework 有基本的了解。
返回页首
使用 P/Invoke
与在 .NET Framework 中一样,在 .NET Compact Framework 中使用 P/Invoke 服务包括三个主要步骤:声明、调用,以及错误处理。在讲述完这些步骤后,我们将查看 .NET Compact Framework 和完整的 .NET Framework 中 P/Invoke 服务的区别。
声明
首先,在设计时,您必须告诉 .NET Compact Framework您想要调用的非托管函数。您必须包括 DLL 名(也称为模块),函数名(也称为入口点)以及所使用的调用约定。例如,如果要调用 SHGetSpecialFolderPath 函数以返回 Windows CE 设备上各个系统文件夹的路径,您需要使用 the DllImportAttribute; 或 VB 中的 DllImportAttribute 或 the Declare 语句声明此函数。
[VB]
Private Declare Function SHGetSpecialFolderPath Lib "coredll.dll" ( _
ByVal hwndOwner As Integer, _
ByVal lpszPath As String, _
ByVal nFolder As ceFolders, _
ByVal fCreate As Boolean) As Boolean
[C#]
[DllImport("coredll.dll", SetLastError=true)]
private static extern bool SHGetSpecialFolderPath(
int hwndOwner,
string lpszPath,
ceFolders nFolder,
bool fCreate);
在这两个例子中,您可以看到声明包含了该函数所驻留的 DLL 的名字(coredll.dll,许多 Windows CE API 驻留其中,类似于 Win32 API 中的 kernel32.dll 和 user32.dll )和函数名(SHGetSpecialFolderPath)。另外,此 VB 函数被标记为 Private ,所以该函数只能在声明此函数的类中进行调用。它也可以被标记为 Public 或 Friend ,这取决于您的应用程序如何调用该函数。
注 与基于完整的 .NET Framework 的应用程序一样,将非托管的 API 声明分组到一个适当的命名空间(如 Quilogy.CeApi )的类中并通过共享的包装方法公开其函数是一种很好的实践做法。该类可以打包到其自身的程序集中,然后分发到您的团队或组织中的所有开发人员。
在 C# 声明中,必须用 static 和 extern 关键字来指明函数是在外部实现的,可以在没有创建该类实例的情况下调用(虽然函数也被标记为 private,因此它仍是隐藏的)。
您还可以看到,函数需要一个 ceFolders 类型的参数。事实上,函数需要一个 32 位的整数 CSIDL 值,并映射到其中一个 Windows CE API 常量。在这些情况下,用枚举公开常量值(参见 msdn.microsoft.com中的 API 文档)是一种很好的实践做法,如下面的代码片段所示。
[VB]
Private Enum ceFolders As Integer
PROGRAMS = 2 ???? \Windows\Start Menu\Programs
PERSONAL = 5 ???? \My Documents
STARTUP = 7 ???? \Windows\StartUp
STARTMENU = &HB ???? \Windows\Start Menu
FONTS = &H14 ???? \Windows\Fonts
FAVORITES = &H16 ???? \Windows\Favorites
End Enum
正如 .NET Framework 一样,Declare 语句支持 Alias 子句,该子句允许您在 DLL 中为该函数指定一个不同的名称。这在 DLL 中的函数名与关键字或者您代码中已定义的另一个函数相冲突时非常有用。 DllImportAttribute 属性也支持这个功能,它通过 EntryPoint 属性(该属性可以被添加到属性声明中)实现,如下所示。
[DllImport("coredll.dll", EntryPoint="SHGetSpecialFolderPath")]
static extern bool GetFolderPath( //the rest of the declaration
注 Alias 子句或 EntryPoint 属性均能包含 DLL 中函数的序号值,格式为 “#num”,如“#155”。
最后您可以看到,在 C# 声明中, SetLastError 属性被设置为 true (默认值为 false )。这指定公共语言运行库调用 Windows CE GetLastError 函数以缓存返回的错误值,这样其他函数就不会重写该值。然后,您可以用 Marshal.GetLastWin32Error 安全地检索错误。在 Declare 语句中,此值假定为 true,在 VB 中使用 DllImportAttribute 时也是如此。
调用
一旦正确声明了要调用的函数(通常在一个实用工具类中),您就可以在该类的方法中对函数调用进行包装。常用的技巧是将包装函数声明为该类的一个 Public Shared (或 C# 中的 static )方法,如下所示。
Namespace Quilogy.CeApi
Public Class FileSystem
Private Sub New()
???? Prevents creation of the class
End Sub
Public Shared Function GetSpecialFolderPath( _
ByVal folder As ceFolders) As String
Dim sPath As String = New String(" "c, MAX_PATH)
Dim i As Integer
Dim ret As Boolean
Try
ret = SHGetSpecialFolderPath(0, sPath, folder, False)
Catch ex As Exception
HandleCeError(ex, "GetSpecialFolderPath")
Return Nothing
End Try
If Not ret Then
???? API Error so retrieve the error number
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"SHGetSpecialFolderPath returned False, " & _
"likely an invalid constant", errorNum), _
"GetSpecialFolderPath")
End If
Return sPath
End Function
???? Other methods
End Class
End Namespace
在这个例子中,对 SHGetSpecialFolderPath 的调用包装于 Quilogy.CeApi.FileSystem 类的 GetSpecialFolderPath 方法中,并且负责向 DLL 函数传递正确的参数以及处理各种错误。在本例中,只公开包装中需要客户端提供的参数是一种很好的实践做法。其余的可以默认设为适当的值。您还可以看到该类包含了一个私有的构造函数以防止创建该类的实例。
然后,该方法可以由调用方以下面的方式进行调用:
Imports Quilogy
...
Dim docs As String
docs = CeApi.FileSystem.GetSpecialFolderPath(ceApi.ceFolders.PERSONAL)
当这段代码在运行时被实时 (JIT) 编译之后,公共语言运行库的 P/Invoke 服务将 Declare 或 DllImportAttribute 定义从程序集的元数据中提取出来,对包含该函数的 DLL 进行定位并加载到内存,然后使用入口点信息检索该函数的地址。如果一切运行正常,该函数将被调用,其参数被封送处理,并将各种返回值返回到调用方。
处理错误
虽然开发人员从来不希望他们的代码会产生运行时错误,但是要记住使用 P/Invoke 在 DLL 上调用的函数可能会产生两种错误。
第一种是由 PInvoke 服务本身产生的异常。当传给此方法的参数包含无效数据,或函数本身以不恰当的参数进行声明时,这种情况就会产生。在此情况下,将会引发 NotSupportedException 。出现这种情况时,您应该重新检查您的声明以确定它是否与实际的 DLL 定义相匹配。或者,P/Invoke 可能引发 MissingMethodException,此异常正如其名字所示,是由于找不到入口点而产生的。在 GetSpecialFolderPath 方法中,这些异常在 Try Catch 块中被捕获,然后传递到另一个名为 HandleCeError????自定义方法中。该方法检查传入异常的类型,然后用适当的消息引发一个 WinCeException(派生于 ApplicationException)类型的自定义异常,它们均在下面的列表中显示。这种包装异常的技术在 .NET Compact Framework 中是有效的,因为它集中了错误处理并允许将自定义成员(如Windows CE 错误号)添加到自定义的异常类中。
Private Shared Sub HandleCeError(ByVal ex As Exception, _
ByVal method As String)
???? Do any logging here
???? Swallow the exception if asked
If Not ExceptionsEnabled Then
Return
End If
If TypeOf ex Is NotSupportedException Then
???? Bad arguments or incorrectly declared
Throw New WinCeException( _
"Bad arguments or incorrect declaration in " & method, 0, ex)
End If
If TypeOf ex Is MissingMethodException Then
???? Entry point not found
Throw New WinCeException( _
"Entry point not found in " & method, 0, ex)
End If
If TypeOf ex Is WinCeException Then
Throw ex
End If
???? All other exceptions
Throw New WinCeException( _
"Miscellaneous exception in " & method, 0, ex)
End Sub
Public Class WinCeException : Inherits ApplicationException
Public Sub New()
End Sub
Public Sub New(ByVal message As String)
MyBase.New(message)
End Sub
Public Sub New(ByVal message As String, ByVal apiError As Integer)
MyBase.New(message)
Me.APIErrorNumber = apiError
End Sub
Public Sub New(ByVal message As String, _
ByVal apiError As Integer, ByVal innerexception As Exception)
MyBase.New(message, innerexception)
Me.APIErrorNumber = apiError
End Sub
Public APIErrorNumber As Integer = 0
End Class
您可以看到 HandleCeError 方法首先检查共享字段 ExceptionsEnabled ,如果为 false,则只需从方法中返回。这个共享字段允许调用方指定异常是否会从该类的方法中引发。
注 前面的代码列表中显示的错误字符串也可以放置于一个资源文件中,打包成附属的程序集,然后使用 ResourceManager 类动态检索,正如在 VS .NET 帮助中所描述的那样。
第二种可能产生的错误类型是从 DLL 函数本身返回的错误。在 GetSpecialFolderPath 方法的例子中,当 SHGetSpecialFolderPath 返回 False 时这种情况就会发生(例如,如果枚举包含一个无效的或不受支持的常量)。如果是这种情况,该方法使用 GetLastWin32Error 检索出错误号,并将错误号通过其重载构造函数传递给 HandleCeError 方法。
.NET Compact Framework 的不同之处
虽然上文显示的声明在 .NET Compact Framework 和完整的 .NET Framework中是一样的(除了模块名),但还是有一些细微的差别。
全部是 Unicode编码,并且在任何时候。在完整的 .NET Framework 中,默认的字符集可以使用 Declare 语句中的 Ansi、 Auto,或 Unicode 子句和 DllImportAttribute 中的 CharSet 属性进行设置,默认的字符集控制字符串参数的封送处理行为和要使用的精确入口点名称(P/Invoke 可附加“A”指定 ANSI 或者附加“W”指定 Unicode,这取决于 ExactSpelling 属性)。在完整的 .NET Framework 中,默认的是 ANSI,然而 .NET Compact Framework 只支持 Unicode,因此只包含 CharSet.Unicode (和等同于 Unicode 的 CharSet.Auto)值,也不支持 Declare 语句的任何子句。这意味着 ExactSpelling 属性同样是不受支持的。因此,如果您的 DLL 函数期望的是一个 ANSI 字符串,您就需要在 DLL 中执行转换,或在调用函数之前使用 ASCIIEncoding 类重载的 GetBytes 方法将字符串转换为一个字节数组,因为 .NET Compact Framework 总是传递指向 Unicode 字符串的指针。
一个调用约定。完整的 .NET Framework 支持三种不同的调用约定(调用规则确定了参数传递给函数的顺序以及谁负责清理堆栈这些问题),使用用于 DllImportAttribute 的 CallingConvention 属性的 CallingConvention 枚举。但是,对于 .NET Compact Framework 而言,只支持 Winapi 值(默认的平台约定),也就是默认的被称为 Cdecl 的 C 和 C++ 调用规则。
单向。虽然参数可以通过值或引用传递给 DLL 函数,允许 DLL 函数将数据返回给 .NET Compact Framework 应用程序,但 .NET Compact Framework 中的 P/Invoke 并不能像完整的 .NET Framework 一样支持回调。在完整的 .NET Framework 中,回调通过使用传递给 DLL 函数的委派(面向对象的函数指针)而得到支持。然后,DLL 函数在该委派的地址中利用函数的结果调用托管的应用程序。一个使用回调的典型例子是 EnumWindows API 函数,它用以枚举所有顶层窗口并将其句柄传递给回调函数。
不同的异常。在完整的 .NET Framework 中,如果不能定位函数或函数没有正确声明,则(通常)分别会引发 EntryPointNotFoundException 和 ExecutionEngineException 异常。而在 .NET Compact Framework 中,正如上文所述,将引发 MissingMethodException 和 NotSupportedException 类型的异常。
处理 Windows 消息。通常,当处理操作系统 API 时,必须将窗口的句柄 (hwnd) 传递给函数,或为操作系统发送的消息添加自定义处理。在完整的 .NET Framework Form 类公开了 Handle 属性以满足前者的要求,而以重写 DefWndProc 方法来处理后者。事实上,这些成员是通过 Control 基类公开的,所以此功能对所有类都是可用的,包括 ListBox、 Button(从 Control派生)等。.NET Compact Framework 中的 Form 类两者均不包含,但它包括了 Microsoft.WindowsCE.Forms 命名空间中的 MessageWindow 和 Message 类。 MessageWindow 类可以继承,而且其 WndProc 方法可以被重写,用以捕捉 Message 类型的特定消息。基于 .NET Compact Framework 的应用程序甚至可以使用 MessageWindow 类的 SendMessage 和 PostMessage 方法向其它窗口发送消息。例如,当检查确定基于 .NET Compact Framework 的应用程序是否已经运行时, PostMessage 可以用来向所有顶层窗口广播自定义消息,请参见在 smartdevices.microsoftdev.com Jonathan Wells 张贴的示例代码。
返回页首
封送处理数据
在调用过程中,P/Invoke 服务负责封送处理传递给 DLL 函数的参数值。P/Invoke 的组件通常被称为封送拆收器。本节我们将讨论封送拆收器在处理通用类型、字符串、结构、非整数类型和一些其它问题过程中要完成的工作。
Blittable 类型
幸运的是,许多用来调用 DLL 函数的类型在 .NET Compact Framework 和非托管代码中具有同样的表示方法。这些类型被称为 blittable 类型,如下表所示。
Compact Framework 类型Visual Basic 关键字C# 关键字
System.ByteBytebyte
System.SByten/asbyte
System.Int16Shortshort
System.UInt16n/aushort
System.Int32Integerint
System.Int64Long (only ByRef)long (only ref)
System.UInt64n/aulong
System.IntPtrn/a* using unsafe
或者说,封送拆收器在托管和非托管代码间转换时不需要对这些类型定义的参数执行任何特殊处理。事实上,通过扩展,封送拆收器不需要转换这些类型的一维数组,或甚至是只包含这些类型的结构和类。
此行为与在完整的 .NET Framework 框架上一样,另外 .NET Compact Framework 也包括 System.Char (VB 中的Char ,C# 中的 char ), System.String (VB 中的String ,C# 中的 string ), System.Boolean (VB 中的Boolean ,C# 中的 bool )作为 blittable 类型。对于 System.Char 和 System.String,正如前文所述,这是由于 .NET Compact Framework ??????§???? Unicode,所以封送拆收器总是将前者作为一个 2 字节的 Unicode 字符进行封送处理,而后者则是作为一个 Unicode 数组。对于 System.Boolean,.NET Compact Framework的封送拆收器使用 1 字节的整数值,而在完整的 .NET Framework 中使用 4 字节的整数值。
警告 虽然 System.String 在 .NET Compact Framework 中是 blittable 类型,但当它用于复杂对象(如结构或类)内部时,它就不是 blittable 类型了。这将在后面的文章中详细探讨。
很明显,在 .NET Compact Framework 应用程序中使用这些类型可使您的代码简化。另外一个使用 blittable 类型的简单例子是 CeRunAppAtEvent 函数的调用,它允许 ActiveSync 数据同步结束时启动 .NET Compact Framework 的应用程序。
Public Enum ceEvents
NOTIFICATION_EVENT_NONE = 0
NOTIFICATION_EVENT_TIME_CHANGE = 1
NOTIFICATION_EVENT_SYNC_END = 2
NOTIFICATION_EVENT_DEVICE_CHANGE = 7
NOTIFICATION_EVENT_RS232_DETECTED = 9
NOTIFICATION_EVENT_RESTORE_END = 10
NOTIFICATION_EVENT_WAKEUP = 11
NOTIFICATION_EVENT_TZ_CHANGE = 12
End Enum
Public Class Environment
Private Sub New()
End Sub
_
Private Shared Function CeRunAppAtEvent(ByVal appName As String, _
ByVal whichEvent As ceEvents) As Boolean
End Function
Public Shared Function ActivateAfterSync() As Boolean
Dim ret As Boolean
Try
Dim app As String
app = _
System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase
ret = CeRunAppAtEvent(app, ceEvents.NOTIFICATION_EVENT_SYNC_END)
If Not ret Then
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"CeRunAppAtEvent returned false", errorNum), _
"ActivateAfterSync")
End If
Return ret
Catch ex As Exception
HandleCeError(ex, "ActivateAfterSync")
Return False
End Try
End Function
Public Shared Function DeactivateAfterSync() As Boolean
Dim ret As Boolean = False
Try
Dim app As String
app = _
System.Reflection.Assembly.GetExecutingAssembly().GetName().CodeBase
ret = CeRunAppAtEvent(app, ceEvents.NOTIFICATION_EVENT_NONE)
If Not ret Then
Dim errorNum As Integer = Marshal.GetLastWin32Error()
HandleCeError(New WinCeException( _
"CeRunAppAtEvent returned false", errorNum), _
"DeactivateAfterSync")
End If
Return ret
Catch ex As Exception
HandleCeError(ex, "DeactivateAfterSync")
Return False
End Try
End Function
???? other stuff
End Class
注 在调用 ActivateAfterSync 方法后,ActiveSync 同步结束时,应用程序的实例将以一个特定的命令行参数自动启动。为了检测命令行并激活已经存在的应用程序实例,通常要向智能设备项目中添加额外的代码。
在本例中, SHGetSpecialFolderPath 中所有参数和返回值都是 blittable 类型,所以您或者封送拆收器均不需要进行额外的工作。您会注意到,在这种情况下, ceEvents 枚举是作为基础值类型被封送处理的,默认为 blittable System.Int32 。但是,应该注意到,封送拆收器只能处理 32 位或更少位的返回类型。
传递字符串
如上文所述,.NET Compact Framework 中的字符串是 blittable 类型的,它对于非托管函数而言是以 null 终止的 Unicode 字符数组。在调用时,因为 System.String 是引用类型(在托管的堆上进行分配,其地址保存于引用变量中),所以即使它是通过值传递的,封送拆收器也是将字符串的指针传递给非托管函数,在 SHGetSpecialFolder 和 CeRunAppAtEvent 中就是如此。
注 .NET Compact Framework 总是传递引用类型的指针,它不支持通过引用传递引用类型(VB 中的 ByRef,C# 中的 ref)。
但是,您一定注意到 SHGetSpecialFolder 期望的是一个固定长度的字符串缓冲区,它可以用于保存路径值,而 CeRunAppAtEvent 只是简单地读取字符串。对于前者的情况,固定长度如下进行声明(c 表示转换为 System.Char)。
Dim sPath As String = New String(" "c, MAX_PATH)
字符串的指针被传递给非托管函数之后,非托管的代码将字符串看作是 WCHAR * (或 TCHAR *、 LPTSTR, 也可能是 LPSTR),并且可以使用指针对字符串进行操作。当函数完成后,调用方可以正常检查字符串。
这一行为和完整的 .NET Framework 是完全不同的,后者的封送拆收器必须考虑字符集。因此,完整的 .NET Framework 不支持通过值或引用向非托管函数传递字符串,也不允许非托管函数修改缓冲区的内容。
要在完整的 .NET Framework 中解决决此问题(因为许多 Win32 API 要求字符串缓冲区),您可以改为传递一个 System.Text.StringBuilder 对象;指针将由封送拆收器传递给非托管函数进行操作。唯一要警告的是 StringBuilder 必须为返回值分配足够的空间,否则文本将溢出,从而使 P/Invoke 引发异常。
事实上 StringBuilder 在 .NET Compact Framework 中也能以同样的方式使用,因此 SHGetSpecialFolderPath 的声明可变为:
Private Declare Function SHGetSpecialFolderPath Lib "coredll.dll" ( _
ByVal hwndOwner As Integer, _
ByVal lpszPath As StringBuilder, _
ByVal nFolder As ceFolders, _
ByVal fCreate As Boolean) As Boolean
调用语法为:
Dim sPath As New StringBuilder(MAX_PATH)
ret = SHGetSpecialFolderPath(0, sPath, folder, False)
实际上,在这个特定的例子中,改为 StringBuilder 会更加有效,因为在 VB 中使用 Declare 语句时, ByVal String 参数将被作为一个外部参数进行封送处理。在函数在返回新引用之前返回时,使用外部参数将强制公共语言运行库创建一个新的 String 对象。 DllImportAttribute 不会引起这种行为。
在任何情况下,建议您在处理固定长度的字符串缓冲区时,使用 StringBuilder ,因为它们更容易初始化,并且与完整的 .NET Framework具 有更高的一致性。
传递结构
如上文所述,只要结构包含 blittable 类型,您就可以放心地将结构传递给非托管函数。例如, GlobalMemoryStatus Windows CE API 被传递了一个需要其填充的 MEMORYSTATUS 结构的指针,返回关于设备的物理及虚拟内存信息。因为该结构只包含 blittable 类型(在 .NET Compact Framework 中,非托管的 DWORD 值被转换为 32 位的无符号整数值 System.UInt32),所以此函数可以很容易地进行调用,如下面的代码片段所示。
Private Structure MEMORY_STATUS
Public dwLength As UInt32
Public dwMemoryLoad As UInt32
Public dwTotalPhys As UInt32
Public dwAvailPhys As Integer
Public dwTotalPageFile As UInt32
Public dwAvailPageFile As UInt32
Public dwTotalVirtual As UInt32
Public dwAvailVirtual As UInt32
End Structure
_
Private Shared Sub GlobalMemoryStatus(ByRef ms As MEMORY_STATUS)
End Sub
Public Shared Function GetAvailablePhysicalMemory() As String
Dim ms As New MEMORY_STATUS
Try
GlobalMemoryStatus(ms)
Dim avail As Double = CType(ms.dwAvailPhys, Double) / 1048.576
Dim sAvail As String = String.Format("{0:###,##}", avail)
Return sAvail
Catch ex As Exception
HandleCeError(ex, "GetAvailablePhysicalMemory")
Return Nothing
End Try
End Function
注意在本例中, GetAvailablePhysicalMemory 函数实例化了一个 MEMORY_STATUS 结构并将它传递给 GlobalMemoryStatus 函数。因为函数需要此结构的指针(在 Windows CE 中定义为 LPMEMORYSTATUS ), GlobalMemoryStatus 的声明指出 ms 参数是 ByRef。在 C# 中,声明和调用都需要使用引用关键字。
或者说,此函数可以通过将 MEMORY_STATUS 声明为一个类而不是一个结构进行调用。因为该类是引用类型的,您不需要将参数声明为通过引用传递,而是可以依赖于这样一个事实:封送拆收器总是向非托管函数传递一个 4 字节的指向引用类型的指针。
注意引用类型总是以其出现于托管代码中的次序进行封送处理,这一点很重要。这意味着,这些字段将如非托管函数所期望的那样布局于内存中。因此,您不需要用 StructLayoutAttribute 和 LayoutKind.Sequential对结构进行修饰,虽然这与在完整的 .NET Framework 上一样都是受支持的。
虽然您可以向非托管函数传递结构(和类),但是 .NET Compact Framework 的封送拆收器不支持封送处理从一个非托管函数返回的结构的指针。在这些情况下,您需要使用 Marshal 类的 PtrToStructure 方法对结构进行手动封送处理。
最后,.NET Compact Framework 中的封送拆收器和完整的 .NET Framework 中的封送拆收器之间的主要区别之一是:在 .NET Compact Framework中,封送拆收器不能在结构中封送处理复杂的对象(引用类型)。这意味着,如果结构中的任何字段含有除了上文表中列出的类型以外的类型(包括字符串或字符串数组),则不能对该结构进行完全的封送处理。这是由于 .NET Compact Framework 不支持完成的 .NET Framework 中使用的 MarshalAsAttribute,它向封送拆收器提供了关于如何封送处理数据的显式指令。但是,在之后的一篇文章中,我们将探讨引用类型(例如嵌入在结构中的字符串)如何被传递到非托管函数以及如何从非托管函数中返回。
传递非整型
注意在 blittable 类型表中没有提及浮点变量。这些类型是非整型的(不表示一个整数),不能由 .NET Compact Framework 通过值进行封送处理。但是,可以通过引用对它门进行封送处理,而且可以作为指向非托管函数(作为包装或填充程序)的指针传递。
注 将 64 位整数(VB 中Long ,C# 中 long )作为一个指向 INT64 的指针进行封送处理也是如此。
例如,如果非托管函数接受 double 类型的两个参数并返回一个 double,在 C 中定义如下:
double DoSomeWork(double a, double b) { }
接下来,为了从 .NET Compact Framework 中调用该函数,您首先需要使用嵌入式 Visual C 创建一个非托管函数,它通过引用(作为指针)接受两个参数,并且通过调用原始函数以输出参数的形式返回结果,如下所示:
void DoSomeWorkShim(double *pa, double *pb, double *pret)
{
*pret = DoSomeWork(pa, pb);
}
然后,使用 DllImportAttribute 语句在 Compact Framework 中声明 DoSomeWorkShim 函数,如下所示:
_
Private Shared Sub DoSomeWorkShim(ByRef a As Double, _
ByRef b As Double, ByRef ret As Double)
End Sub
最后,您可以再用一个附加的托管包装函数包装对 DoSomeWorkShim 的调用,此附加的包装函数维护了原始 DoSomeWork 的调用签名。
Public Shared Function DoSomeWork(ByVal a As Double, _
ByVal b As Double) As Double
Dim ret As Double
DoSomeWorkShim(a, b, ret)
Return ret
End Function
其它问题
一个让开发人员担心的有关 P/Invoke 的问题是它与公共语言运行库中的垃圾回收器 (GC) 的互操作性。因为当回收发生时,.NET Compact Framework 和完整的 .NET Framework 中的垃圾回收器都可能在托管堆上重新安排对象,所以如果非托管函数试图使用传递给它的指针操作内存,就可能出现问题。幸运的是,.NET Compact Framework 的封送拆收器在调用期间将自动锁定(将对象锁定在其当前内存位置)任何传递给非托管函数的引用类型。
第二个开发人员经常遇到的问题是 C# 中与 P/Invoke 相关的 unsafe 和 fixed 关键字的使用。基本上 unsafe 关键字允许 C# 代码直接通过使用指针对内存进行操作,正如???? C 中一样,虽然这允许了更大程度上的灵活性,但同时也使得公共语言运行库中的代码验证功能变为无效。该主题会在以后的文章中更深入地讨论。该功能使得公共语言运行库在 JIT 的过程中可以确保它所执行的代码只读取已分配的内存,并且所有方法在调用时,参数的数目和类型都正确。
注 除了创建无法验证的代码以外,在完整的 .NET Framework ????,不安全代码只能在一个受信任的环境中执行,虽然在 .NET Compact Framework 1.0 版中没有包括代码访问安全性 (CAS),但这现在已经不是问题。
fixed 关键字与 unsafe 关键字一起用于锁定托管对象,这通常是一个值类型或值类型数组,例如一个 System.Char 数组,所以当它被非托管函数使用时,GC不能对它进行移动。
返回页首
小结
正如您所看到的,.NET Compact Framework 的平台调用功能是完整的 .NET Framework 中可用功能的一个有用的子集。有了这项功能,您可以相当容易地实现大多数应用程序所需的对非托管 DLL 的调用。
对于更复杂的情况,例如传递嵌入于结构中的字符串,您可能需要求助于指针和内存分配,这正是下一篇白皮书的主题。
本文转自
http://msdn2.microsoft.com/zh-cn/library/aa446536.aspx