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

 

Sonja Keserovic,项目经理
David Mortenson,首席软件设计工程师
Adam Nathan,测试中的首席软件设计工程师

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

可通过两种main方法向纯非托管调用方公开托管 API:作为 COM API 或平面 API。 对于愿意使用 Visual Studio® .NET 重新编译代码的 C++ 非托管客户端,有第三个选项:通过 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 非常简单,但托管 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) 内联、编译程序集存根和解释封送处理存根 (按从快到慢的顺序) 。

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

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

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

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

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

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

    聊天接口是进行大量转换的接口,无需在互操作边界的另一端执行任何重大工作。 例如,属性资源库和 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. 封送参数。 (这可以分配 memory.)
  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. 封送参数。 (这可以分配 memory.)
  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. 转换应用域(如果需要)。
  9. 如果 appdomain 不完全信任,请针对目标 appdomain 执行该方法可能具有的任何链接要求。
  10. 如果分配了临时内存,请初始化清理列表,以便在调用完成时快速释放。
  11. 封送参数。 (这可以分配 memory.)
  12. 查找要调用的目标托管方法。 (这涉及到将接口调用映射到目标实现。)
  13. 缓存返回值。 (如果是浮点返回值,请从浮点寄存器获取它)
  14. 更改回协作式垃圾回收模式。
  15. 如果引发异常,请提取其 HRESULT 以返回,并调用 SetErrorInfo
  16. 如果未引发异常,则向 传播和 by-ref 参数。
  17. 将扩展堆栈指针还原到原始值,以考虑调用方弹出参数。

附录 2:资源

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

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

互操作示例、Microsoft .NET Framework

亚当·内森的 博客

克里斯·布鲁姆的 博客

附录 3:术语表

AppDomain (Application Domain) 应用程序域可以视为类似于轻型 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。 例如,具有可变大小结构参数的方法很难声明,因为托管类型系统中没有等效的概念。
Interop 涵盖托管和非托管 (之间任何类型的互操作性的通用术语,也称为“本机”) 代码。 互操作是 CLR 提供的众多服务之一。
互操作程序集 一种特殊类型的托管程序集,其中包含类型库中包含的 COM 类型的托管类型等效项。 通常通过对类型库运行类型库导入程序工具 (Tlbimp.exe) 生成。
托管代码 在 CLR 控制下执行的代码称为托管代码。 例如,用 C# 或 Visual Basic .NET 编写的任何代码都是托管代码。
平台调用 CLR 互操作层提供的服务,用于从托管代码调用非托管平面 API。 平台调用适用于所有托管语言。
RCW (运行时可调用 wapper) 由 CLR 互操作层围绕从托管代码激活的 COM 对象创建的一种特殊包装器。 RCW 通过提供数据封送处理、生存期管理、标识管理、错误处理、正确的单元和线程转换等来隐藏托管模型和 COM 对象模型之间的差异。
非托管代码 在 CLR 外部运行的代码称为“非托管代码”。COM 组件、ActiveX 组件和 Win32 API 函数是非托管代码的示例。