托管/非托管代码互操作性概述

 

项目经理 Sonja Keserovic
David Mortenson,首席软件设计工程师
亚当·内森,测试中的首席软件设计工程师

Microsoft Corporation

2003 年 10 月

适用于:
   ® Microsoft .NET Framework
   COM 互操作

摘要: 本文提供了有关托管和非托管代码之间的互操作性的基本事实,以及从托管代码访问和包装非托管 API 以及向非托管调用方公开托管 API 的准则和常见做法。 此外,还突出显示了有关开发过程的安全性和可靠性注意事项、性能数据和常规做法。 (打印页 14 页)

先决条件: 本文档的目标受众包括需要就使用托管代码的位置做出高级决策的开发人员和经理。 为此,了解托管代码和非托管代码之间的交互方式以及当前准则如何应用于特定方案很有帮助。

内容

互操作性简介
互操作性准则
安全
可靠性
性能
附录 1:跨越互操作性边界
附录 2:资源
附录 3:术语词汇表

互操作性简介

公共语言运行时(CLR)促进托管代码与 COM 组件、COM+ 服务、Win32® API 和其他类型非托管代码的交互。 数据类型、错误处理机制、创建和销毁规则以及设计准则因托管对象模型和非托管对象模型而异。 为了简化托管和非托管代码之间的互操作以及简化迁移路径,CLR 互操作层隐藏了这些对象模型与客户端和服务器之间的差异。

互操作性(“互操作”)是双向的,因此可以:

  • 从托管代码 调用非托管 API

    这可以为平面 API(静态 DLL 导出(如 Win32 API)(从 dll(如 kernel32.dll 和 user32.dll) 和 COM API(如Microsoft® Word、Excel、Internet Explorer、ActiveX® 数据对象(ADO)等公开的对象模型)执行此操作。

  • 向非托管代码公开托管 API

    执行此操作的示例包括为基于 COM 的应用程序(如 Windows Media® Player)创建加载项,或在 MFC 窗体上嵌入托管的 Windows 窗体控件。

三种互补技术可实现这些托管/非托管交互:

  • 平台调用(有时称为 P/Invoke)允许在任何非托管语言中调用任何函数,只要其签名在托管源代码中重新声明。 这类似于 Visual Basic® 6.0 中的 Declare 语句提供的功能。
  • COM 互操作允许以类似于使用普通托管组件的方式以任何托管语言调用 COM 组件,反之亦然。 COM 互操作由 CLR 提供的核心服务以及 System.Runtime.InteropServices 命名空间中的一些工具和 API 组成。
  • C++互操作(有时称为 It Just Works(IJW)是一项特定于C++的功能,它使平面 API 和 COM API 能够直接使用,因为它们一直使用。 这比 COM 互操作更强大,但它需要更多的照顾。 在使用此技术之前,请确保先检查C++资源。

互操作性准则

从托管代码调用非托管 API

有几种类型的非托管 API 和几种类型的互操作技术可用于调用它们。 本部分介绍了有关如何以及何时使用这些技术的建议。 请注意,这些建议非常普遍,并不涵盖每个方案。 应仔细评估方案,并应用适合你的方案的开发做法和/或解决方案。

调用非托管平面 API

有两种机制可用于从托管代码调用非托管平面 API:通过平台调用(适用于所有托管语言)或通过C++互操作(C++中提供)。

在决定使用这些互操作技术之一调用平面 API 之前,应确定 .NET Framework 中是否有等效的功能。 建议尽可能使用 .NET Framework 功能,而不是调用非托管 API。

对于仅调用几个非托管方法或调用简单的平面 API,建议使用平台调用而不是C++互操作。 为简单平面 API 编写平台调用声明非常简单。 CLR 将负责 DLL 加载和所有参数封送处理。 与使用C++互操作和引入以C++编写的全新模块的成本相比,即使是为复杂平面 API 编写一些平台调用声明的工作也是可以忽略不计的。

若要包装复杂的非托管平面 API,或包装在开发托管代码时更改的非托管平面 API,建议使用C++互操作而不是平台调用。 C++层可能非常薄,其余托管代码可以采用任何其他托管语言进行编写。 在这些方案中使用平台调用需要大量精力在托管代码中重新声明 API 的复杂部分,并将其与非托管 API 保持同步。 使用C++互操作可以通过允许直接访问非托管 API 来解决此问题,这不需要重写,只需包含头文件。

调用 COM API

可通过两种方法从托管代码调用 COM 组件:通过 COM 互操作(适用于所有托管语言)或通过C++互操作(C++中提供)。

若要调用与 OLE 自动化兼容的 COM 组件,建议使用 COM 互操作。 CLR 将负责 COM 组件激活和参数封送处理。

对于基于接口定义语言(IDL)调用 COM 组件,建议使用C++互操作。 C++层可能非常薄,其余托管代码可以使用任何托管语言编写。 COM 互操作依赖于类型库中的信息进行正确的互操作调用,但类型库通常不包含 IDL 文件中的所有信息。 使用C++互操作可以通过允许直接访问这些 COM API 来解决此问题。

对于拥有已交付的 COM API 的公司,请务必考虑为这些 API 传送主要互操作程序集(PIA),从而使它们易于用于托管客户端。

用于调用非托管 API 的决策树

图 1. 调用非托管 API 决策树

向非托管代码公开托管 API

有两种主要方法可将托管 API 公开给纯非托管调用方:作为 COM API 或平面 API。 C++ 对于愿意使用 Visual Studio® .NET 重新编译其代码的非托管客户端,有第三个选项:通过C++互操作直接访问托管功能。 本部分介绍了有关如何以及何时使用这些选项的建议。

直接访问托管 API

如果非托管客户端是用 C++ 编写的,则可以使用 Visual Studio .NET C++编译器编译为“混合模式映像”。完成此操作后,非托管客户端可以直接访问任何托管 API。 但是,某些编码规则确实适用于从非托管代码访问托管对象;有关更多详细信息,请查看C++文档。

直接访问是首选选项,因为它不需要托管 API 开发人员的任何特殊注意事项。 他们可以根据托管 API 设计准则(DG)设计其托管 API,并确信该 API 仍可供非托管调用方访问。

将托管 API 公开为 COM API

每个公共托管类都可以通过 COM 互操作向非托管客户端公开。 此过程很容易实现,因为 COM 互操作层负责所有 COM 管道。 因此,例如,每个托管类似乎都实现 IUnknownIDispatchISupportErrorInfo和其他一些标准 COM 接口。

尽管将托管 API 公开为 COM API 非常简单,但托管和 COM 对象模型大相径庭。 因此,向 COM 公开托管 API 应始终是显式设计决策。 托管世界中提供的某些功能在 COM 世界中没有等效功能,并且无法从 COM 客户端使用。 因此,托管 API 设计准则(DG)与与 COM 的兼容性之间往往存在紧张关系。

如果 COM 客户端很重要,请根据托管 API 设计准则编写托管 API,然后围绕托管 API 编写精简的 COM 友好托管包装器,该包装将公开给 COM。

将托管 API 公开为平面 API

有时非托管客户端无法使用 COM。 例如,它们可能已编写为使用平面 API,无法更改或重新编译。 C++是唯一允许将托管 API 公开为平面 API 的高级语言。 这样做并不像将托管 API 公开为 COM API 那么简单。 这是一种非常先进的技术,需要对C++互操作以及托管和非托管世界之间的差异有高级知识。

仅当绝对必要时,才将托管 API 公开为平面 API。 如果别无选择,请务必检查C++文档,并充分了解所有限制。

用于公开托管 API 的决策树

图 2. 公开托管 API 决策树

安全

公共语言运行时附带了一个安全系统,代码访问安全性(CAS),它根据程序集的来源信息规范对受保护资源的访问。 调用非托管代码会带来重大安全风险。 如果没有适当的安全检查,非托管代码可以在 CLR 进程中操作任何托管应用程序的任何状态。 还可以直接调用非托管代码中的资源,而无需这些资源受到任何 CAS 权限检查的约束。 因此,任何转换到非托管代码都被视为高度保护的操作,应包括安全检查。 此安全检查查找需要包含非托管代码转换的程序集以及调用它的所有程序集的非托管代码权限,以便有权实际调用非托管代码。

在一些有限的互操作方案中,完全安全检查是不必要的,并且不会过度限制组件的性能或范围。 如果从非托管代码公开的资源没有安全相关性(系统时间、窗口坐标等),或者资源仅在程序集内部使用,并且不会公开给任意调用方公开,则情况就是这种情况。 在这种情况下,可以针对相关 API 的所有调用方取消对非托管代码权限的完整安全检查。 为此,请将 SuppressUnmanagedCodeSecurity 自定义属性应用于相应的互操作方法或类。 请注意,这假设你已确定任何部分受信任的代码都无法利用此类 API 进行仔细的安全审查。

可靠性

托管代码设计为比非托管代码更可靠且更可靠。 提升这些质量的 CLR 功能的一个示例是垃圾回收,它负责释放未使用的内存,以防止内存泄漏。 另一个示例是托管类型安全性,用于防止缓冲区溢出错误和其他与类型相关的错误。

使用任何类型的互操作技术时,代码可能不如纯托管代码那么可靠或可靠。 例如,可能需要手动分配非托管内存,并记住在完成该内存后释放内存。

编写任何非普通互操作代码需要与编写非托管代码相同的可靠性和稳定性。 即使所有互操作代码都正确编写,系统也只会像非托管部分一样可靠。

性能

从托管代码到非托管代码的每个转换(反之亦然),会产生一些性能开销。 开销量取决于所使用的参数类型。 CLR 互操作层基于转换类型和参数类型使用三个级别的互操作调用优化:实时(JIT)内联、编译的程序集存根和解释封送存根(以最快到最慢的调用类型的顺序)。

平台调用的大致开销:10 台计算机指令(在 x86 处理器上)

COM 互操作调用的大致开销:50 台计算机指令(在 x86 处理器上)

这些说明完成的工作显示在附录部分中,调用平面 API:分步调用 COM API:分步调用。 除了确保垃圾回收器不会在调用期间阻止非托管线程以及处理调用约定和非托管异常外,COM 互操作还会执行额外的工作,将当前运行时可调用包装器(RCW)上的调用转换为适合当前上下文的 COM 接口指针。

每次互操作调用都会带来一些开销。 根据这些调用的发生频率以及方法实现中工作的重要性,每个调用开销的范围可以忽略不计,非常明显。

根据这些注意事项,以下列表提供了一些可能有用的常规性能建议:

  • 如果控制托管代码和非托管代码之间的接口,请将其设置为“区块”而不是“聊天”,以减少生成的转换总数。

    聊天接口是进行大量转换的接口,无需在互操作边界的另一端执行任何重大工作。 例如,属性 setter 和 getter 是聊天的。 区块接口是仅进行少量转换的接口,在边界的另一端完成的工作量非常重要。 例如,打开数据库连接并检索某些数据的方法是区块。 区块接口涉及更少的互操作转换,因此可以消除一些性能开销。

  • 如果可能,请避免 Unicode/ANSI 转换。

    将字符串从 Unicode 转换为 ANSI,反之亦然是一项昂贵的操作。 例如,如果需要传递字符串,但其内容并不重要,则可以将字符串参数声明为 IntPtr,互操作封送器不会执行任何转换。

  • 对于高性能方案,将参数和字段声明为 IntPtr 可以提高性能,尽管牺牲了易于使用和可维护性。

    有时,使用 Marshal 类上提供的方法执行手动封送处理速度更快,而不是依赖于默认互操作封送处理。 例如,如果必须跨互操作边界传递大型字符串数组,但只需要几个元素,将数组声明为 IntPtr,并且仅手动访问少数几个元素的速度要快得多。

  • 明智地使用 InAttributeOutAttribute 以减少不必要的封送处理。

    当确定某个参数是否需要在调用之前封送并在调用后封送出时,互操作封送器使用默认规则。 这些规则基于间接级别和参数类型。 其中一些操作可能不是必需的,具体取决于方法的语义。

  • 仅在之后 调用 marshal.GetLastWin32Error 时才在平台上使用 SetLastError=false

    在平台调用签名上设置 SetLastError=true 需要互操作层的其他工作才能保留最后一个错误代码。 仅在你依赖此信息时使用此功能,并在调用后使用它。

  • 如果非托管调用以不可利用的方式公开,则使用 SuppressUnmanagedCodeSecurityAttribute 减少安全检查的数量。

    安全检查非常重要。 如果 API 未公开任何受保护的资源或敏感信息,或者它们受到很好的保护,则广泛的安全检查可能会带来不必要的开销。 但是,不执行任何安全检查的成本非常高。

附录 1:跨越互操作性边界

调用平面 API:分步调用

图 3. 调用平面 API

  1. 获取 LoadLibraryGetProcAddress
  2. 从包含目标地址的签名生成 DllImport 存根。
  3. 推送被调用方保存的寄存器。
  4. 设置 DllImport 帧,并将其推送到帧堆栈上。
  5. 如果分配临时内存,请初始化清理列表,以便在调用完成后快速释放。
  6. 封送参数。 (这可以分配内存。
  7. 将垃圾回收模式从协作模式更改为抢占模式,因此可以随时发生垃圾回收。
  8. 加载目标地址并调用它。
  9. 如果设置了 SetLastError 位,请调用 GetLastError 并将结果存储在线程本地存储中的线程抽象中。
  10. 改回协作垃圾回收模式。
  11. 如果 PreserveSig=false 并且该方法返回了失败的 HRESULT,则引发异常。
  12. 如果未引发异常,传出by-ref 参数。
  13. 将扩展堆栈指针还原为其原始值,以考虑调用方弹出的参数。

调用 COM API:分步调用

图 4. 调用 COM API

  1. 从签名生成托管到非托管存根。
  2. 推送被调用方保存的寄存器。
  3. 设置托管到非托管的 COM 互操作帧,并将其推送到帧堆栈。
  4. 为转换期间使用的临时数据保留空间。
  5. 如果分配临时内存,请初始化清理列表,以便在调用完成后快速释放。
  6. 清除浮点异常标志(仅 x86)。
  7. 封送参数。 (这可以分配内存。
  8. 检索运行时可调用包装器中当前上下文的正确接口指针。 如果无法使用缓存的指针,请在 COM 组件上调用 QueryInterface 以获取它。
  9. 将垃圾回收模式从协作模式更改为抢占模式,因此可以随时发生垃圾回收。
  10. 从 vtable 指针中,按槽号编制索引,获取目标地址并调用它。
  11. 如果之前调用 QueryInterface,请在接口指针上调用 Release
  12. 改回协作垃圾回收模式。
  13. 如果未 PreserveSig标记签名,请检查失败 HRESULT 并引发异常(可能填充 IErrorInfo 信息)。
  14. 如果未引发异常,传出by-ref 参数。
  15. 将扩展堆栈指针还原到原始值,以考虑调用方弹出的参数。

从 COM 调用托管 API:分步调用

图 5. 从 COM 调用托管 API

  1. 从签名生成非托管到托管存根。
  2. 推送被调用方保存的寄存器。
  3. 设置非托管到托管的 COM 互操作帧,并将其推送到帧堆栈。
  4. 为转换期间使用的临时数据保留空间。
  5. 将垃圾回收模式从协作模式更改为抢占模式,以便随时可能发生垃圾回收。
  6. 从接口指针检索 COM 可调用包装器(CCW)。
  7. 检索 CCW 中的托管对象。
  8. 根据需要转换 appdomains。
  9. 如果 appdomain 不完全信任,则执行该方法可能针对目标 appdomain 的任何链接要求。
  10. 如果分配临时内存,请初始化清理列表,以便在调用完成后快速释放。
  11. 封送参数。 (这可以分配内存。
  12. 查找要调用的目标托管方法。 (这涉及到将接口调用映射到目标实现。
  13. 缓存返回值。 (如果它是浮点返回值,请从浮点寄存器中获取该值。
  14. 改回协作垃圾回收模式。
  15. 如果引发异常,请提取其 HRESULT 以返回,并调用 SetErrorInfo
  16. 如果未引发异常,传出by-ref 参数。
  17. 将扩展堆栈指针还原到原始值,以考虑调用方弹出的参数。

附录 2:资源

必须阅读!.NET 和 COM:Adam Nathan 的完整互操作性指南

与非托管代码互操作,Microsoft .NET Framework 开发人员指南

互操作示例,Microsoft .NET Framework

亚当·内森的 博客

克里斯·布鲁姆的 博客

附录 3:术语词汇表

AppDomain (应用程序域) 应用程序域可以被视为类似于轻型 OS 进程,并由公共语言运行时管理。
CCW (COM 可调用包装器) CLR 互操作层围绕从 COM 代码激活的托管对象创建的特殊包装器。 CCW 通过提供数据封送、生存期管理、标识管理、错误处理、正确单元转换和线程转换等来隐藏托管对象模型和 COM 对象模型之间的差异。 CCW 以 COM 友好的方式公开托管对象功能,而无需托管代码实现者知道有关 COM 管道的任何内容。
CLR 公共语言运行时。
COM 互操作 CLR 互操作层提供的服务,用于使用托管代码中的 COM API,或将托管 API 作为 COM API 公开给非托管客户端。 COM 互操作适用于所有托管语言。
C++ 互操作 C++语言编译器和 CLR 提供的服务用于在同一可执行文件中直接混合托管和非托管代码。 C++互操作通常涉及包括非托管 API 中的头文件,并遵循某些编码规则。
复杂的平面 API 具有难以以托管语言声明的签名的 API。 例如,具有可变大小结构参数的方法很难声明,因为托管类型系统中没有等效的概念。
互操作 涵盖托管和非托管(也称为“本机”)代码之间的任何类型的互操作性的一般术语。 互操作是 CLR 提供的许多服务之一。
互操作程序集 一种特殊类型的托管程序集,其中包含类型库中所包含的 COM 类型的托管类型等效项。 通常,通过在类型库上运行类型库导入程序工具(Tlbimp.exe)生成。
托管代码 在 CLR 的控制下执行的代码称为托管代码。 例如,使用 C# 或 Visual Basic .NET 编写的任何代码都是托管代码。
平台调用 CLR 互操作层提供的服务,用于从托管代码调用非托管平面 API。 平台调用适用于所有托管语言。
RCW (运行时可调用的 wapper) CLR 互操作层围绕从托管代码激活的 COM 对象创建的特殊包装器。 RCW 通过提供数据封送、生存期管理、标识管理、错误处理、正确单元转换和线程转换等来隐藏托管对象模型与 COM 对象模型之间的差异。
非托管代码 在 CLR 外部运行的代码称为“非托管代码”。COM 组件、ActiveX 组件和 Win32 API 函数是非托管代码的示例。