Microsoft .NET Compact Framework 上 P/Invoke 和 Marshaling 简介






4.33/5 (9投票s)
Microsoft .NET Compact Framework 上 P/Invoke 和 Marshaling 的简介。
本文由 MSDN 提供。
摘要
了解如何使用 .NET Compact Framework 的平台调用 (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 设备,并已开发为桌面 Framework 的子集。Compact Framework,加上 Visual Studio .NET 2003 中的智能设备项目支持,允许开发人员用 Visual Basic 或 C# 编写托管代码,这些代码在与完整 .NET Framework 中类似的公共语言运行时中执行。
然而,作为 .NET Framework 的子集,.NET Compact Framework 除了处理用户输入、消息传递和 Microsoft® SQL™ Server 2000 Windows CE Edition 2.0 的几个 .NET Compact Framework 特有类型之外,还支持整个命名空间范围内约 25% 的类型。对于开发人员来说,这意味着有些功能只能通过调用操作系统 (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 中的 P/Invoke 服务与完整 .NET Framework 之间的差异。
声明
首先,您必须在设计时告诉 .NET Compact Framework 您打算调用哪个非托管函数。您需要包含 DLL 名称(也称为模块)、函数名称(也称为入口点)和要使用的调用约定。例如,为了调用 SHGetSpecialFolderPath
函数以返回 Windows CE 设备上各种系统文件夹的路径,您可以使用 DllImportAttribute
声明该函数;或者在 VB 中使用 DllImportAttribute
或 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
的参数。实际上,该函数需要一个 CSIDL
值,它是一个 32 位整数,并映射到几个 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 上,默认字符集控制字符串参数的封送行为和要使用的确切入口点名称(P/Invoke 是附加“A”表示 ANSI 还是附加“W”表示 Unicode,具体取决于
ExactSpelling
属性),可以使用Declare
语句中的Ansi
、Auto
或Unicode
子句以及DllImportAttribute
中的CharSet
属性进行设置。虽然在完整的 .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
值(默认平台约定),它默认为 C 和 C++ 的调用约定,称为 Cdecl。 - 单向。虽然参数可以通过值或引用传递给 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
可以用于向所有顶级窗口广播自定义消息,如 Jonathan Wells 在 smartdevices.microsoftdev.com 上发布的示例代码所示。
封送数据
在调用过程中,P/Invoke 服务负责封送传递给 DLL 函数的参数值。P/Invoke 的这个组件通常被称为封送器。在本节中,我们将讨论封送器在处理常见类型、字符串、结构、非整型类型和其他一些问题方面的工作。
Blittable 类型
幸运的是,您将用于调用 DLL 函数的许多类型在 .NET Compact Framework 和非托管代码中都有共同的表示形式。这些类型被称为 blittable 类型,可以在下表中看到。
Compact Framework 类型 | Visual Basic 关键字 | C# 关键字 |
---|---|---|
System.Byte |
字节型 |
byte |
System.SByte |
不适用 | sbyte |
System.Int16 |
Short |
short |
System.UInt16 |
不适用 | ushort |
System.Int32 |
整数 |
int |
System.Int64 |
Long (仅 ByRef ) |
long (仅 ref ) |
System.UInt64 |
不适用 | ulong |
System.IntPtr |
不适用 | * 使用 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
<DllImport("coredll.dll", SetLastError:=True)> _
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 中的字符串是可复制类型,并以以 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 不支持通过值或引用将字符串传递到非托管函数并允许非托管函数修改缓冲区内容。
为了解决这个问题(因为许多 Win32 API 期望字符串缓冲区),在完整的 .NET Framework 中,您可以转而传递一个 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
参数将被封送为 out
参数。使用 out
参数会强制公共语言运行时在函数返回之前创建一个新的 String
对象,然后返回新的引用。DllImportAttribute
不会导致此行为。
无论如何,建议您在处理固定长度字符串缓冲区时使用 StringBuilder
,因为它们更容易初始化并且与完整的 .NET Framework 更一致。
传递结构
如前所述,只要结构包含可复制类型,就可以将结构传递给非托管函数而无需担心。例如,GlobalMemoryStatus
Windows CE API 被传递一个指向 MEMORYSTATUS
结构的指针,该结构由它填充,以返回设备物理内存和虚拟内存的信息。由于该结构仅包含可复制类型(非托管 DWORD
值,在 .NET Compact Framework 中转换为无符号 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
<DllImport("coredll.dll", SetLastError:=True)> _
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# 中,声明和调用都需要使用 ref
关键字。
或者,可以通过将 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 按值封送。但是,它们可以按引用封送,并作为指针传递给充当包装器或 shim 的非托管函数。
注意:64 位整数(VB 中的Long
,C# 中的long
)也是如此,它们被封送为指向 INT64 的指针。
例如,如果非托管函数接受两个 double
类型的参数并返回一个在 C 中定义为 double
的值
double DoSomeWork(double a, double b) { }
那么,为了从 .NET Compact Framework 调用该函数,您首先需要使用 eMbedded Visual C 创建一个非托管函数,该函数按引用(作为指针)接受两个参数,并通过调用原始函数将结果作为输出参数返回,如下所示
void DoSomeWorkShim(double *pa, double *pb, double *pret) { *pret = DoSomeWork(pa, pb); }
然后,您将使用 DllImportAttribute
语句在 Compact Framework 中声明 DoSomeWorkShim
函数,如下所示
<DllImport("ShimFunction.dll")> _
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# 中 unsafe
和 fixed
关键字的使用,这与 P/Invoke 相关。虽然这个主题将在后续论文中更深入地讨论,但本质上 unsafe
关键字允许 C# 代码通过使用指针直接操作内存,就像在 C 中一样。虽然这提供了更大的灵活性,但它也禁用了公共语言运行时的代码验证功能。此功能允许公共语言运行时在 JIT 过程中确保其执行的代码只读取已分配的内存,并且所有方法都使用正确数量和类型的参数进行调用。
注意:除了创建不可验证的代码之外,在完整的 .NET Framework 上,不安全代码只能在受信任的环境中执行,尽管在 .NET Compact Framework 的 1.0 版本中不包含代码访问安全性 (CAS),因此目前这不是问题。
fixed
关键字与 unsafe
结合使用,用于固定托管对象,通常是值类型或值类型数组,例如 System.Char
数组,以便 GC 在非托管函数使用它时无法移动它。
结论
如您所见,.NET Compact Framework 的平台调用功能提供了完整 .NET Framework 上可用功能的一个稳定子集。有了它,您应该能够相当容易地完成应用程序所需的大多数对非托管 DLL 的调用。
对于更复杂的情况,例如在结构中嵌入字符串,您可能需要诉诸指针和内存分配,这是后续白皮书的主题。