COM 是一种 Windows 技术,用于定义和操作对象和类型,这些对象和类型可由 Windows 平台的各种客户端应用程序使用。 请参阅组件对象模型(COM)。
可以在 .NET 项目中引用 COM 组件,在这种情况下,必须在托管程序集(称为主互作程序集或 PIA)中投影它。 互作程序集包含对应于 COM 对象类型的托管类型(由类型库中所述的接口表示),并将 API 调用转发到 COM。 有关 COM 互作的一般信息,请参阅 COM 互作。
可以在 .NET Framework 项目或 .NET Core(包括 .NET 5 或更高版本)项目中引用 COM 组件。 Visual Studio 提供了添加对 COM 对象的引用的方法。 例如,作为 Windows UI 控件(ActiveX 控件)的 COM 组件可能在工具箱中显示,当你将其拖放到 Windows 窗体或 Windows Presentation Foundation(WPF)窗体时,它将作为项目文件中的一个 COMReference 添加。
还可以直接在解决方案资源管理器中添加 COM 引用。 右键单击 “依赖项”,然后选择“ 添加 COM 引用”。
MSBuild 可以为 COM 引用创建包装程序集。 在生成过程中, ResolveComReference 任务 运行并使用系统注册表查找任何引用的 COM 对象,通过调用 tlbimp.exe生成包装器,并将其写入项目文件夹下的磁盘。
接下来,本文介绍引用 COM 组件的各种方法,以及使用每个方法时可能发生的一些可能的错误。
COMReference
项目类型 COMReference 通过使用系统注册表引用 COM 组件。 GUID 和版本是用于查找组件的主键。
<ItemGroup>
<COMReference Include="MyComLibrary">
<Guid>{01234567-89AB-CDEF-0123-456789ABCDEF}</Guid>
<VersionMajor>1</VersionMajor>
<VersionMinor>0</VersionMinor>
<Lcid>0</Lcid>
<WrapperTool>tlbimp</WrapperTool>
</COMReference>
</ItemGroup>
</Project>
由于使用了系统注册表,因此必须在生成计算机上注册组件。 如果仅在本地计算机上从 Visual Studio 生成,可以控制所安装的内容,并且你可能具有写入注册表的管理员权限,则此方法最有效。 但是,在服务器或容器(如 CI/CD 方案中)上生成时,必须确保在生成服务器上安装正确的产品,以及对服务器所做的更改(例如安装新版本的 Office 或卸载软件包),都会带来破坏生成的风险。 或者更糟的是,没有破坏构建,而是使用了与你的代码所针对的组件版本不同的版本。
另一个问题是从注册表获取的 COM 组件可能与编写代码的组件存在一些细微差异。 这取决于组件的发布者以及是否已安装任何已修改它的更新,而不更改版本号。 这种类型的不兼容不会导致构建错误,但如果更改足够显著,在运行时可能会导致应用依赖的某些功能出现问题。
有关如何在注册表中表示 COM 组件的常规信息,请参阅 注册 COM 应用程序。
COMFileReference
项 COMFileReference 类型按文件路径引用 COM 组件。 在生成时不使用注册表。 如果不希望构建过程依赖于系统注册表,那么 COMReference 是一个重要的替代方法。 如果使用 COMFileReference,则无需担心在安装应用程序并在用户计算机上运行之前如何注册应用所需的 COM 组件,这是一大好处,因为生成过程可能正在运行,而不需要写入注册表所需的提升权限。
<ItemGroup>
<COMFileReference Include="Controls\MyCom32.dll" />
</ItemGroup>
若要避免写入注册表,请使用 无注册表 COM 和清单。
如果引用的路径不在你的控制之下,组件可能仍然与所编码的内容不一致。 如果软件包在生成计算机上更新组件,而不更新版本信息,你可能会发现加载另一个组件时可能存在不兼容。 若要避免这种情况,可以使用已缓存的 COM 二进制文件的已知版本,并且仅引用该版本。
Reference
推荐的方法是使用较新版本的 .NET 来引用 COM 组件。 包装程序集是预生成的,而不是在每个生成会话中生成,并且输出将直接引用为托管程序集。
有时包装程序集(PIA)由 COM 组件的提供程序分发。 如果是这种情况,则可以将其直接引用为托管程序集。 这与引用任何其他 .NET 程序集没有什么不同,除非在运行时,对要安装和注册的 COM 组件存在依赖关系。
若要为要使用的 COM 组件自行创建包装程序集,请使用 tlbimp.exe 或 aximp.exe 用于 ActiveX 控件。
在构建时使用此方法,可以避免写入系统注册表需要提升权限。 如果生成自己的封装程序集并将其存储在一个由你控制的地方,比如 NuGet 包或你的解决方案可以访问的文件夹中,就可以避免受到超出你控制的更改的影响。
Bitness
如果项目引用 32 位 COM 组件,则必须使用 MSBuild.exe 或 Visual Studio 生成,而不是 dotnet build。 这是因为 dotnet build 运行 64 位版本的 MSBuild,而 MSBuild 无法使用 32 位 COM 组件。
COM 组件曾经全部编译为 32 位二进制文件。 后来,当引入 64 位技术时,可以将 COM 组件编译为 32 位和 64 位二进制文件。 同一组件通常用于 32 位和 64 位二进制文件。 在这种情况下,标识唯一组件的 GUID 或 CLSID 对于 32 位和 64 位二进制文件都是相同的,但注册表本身被分为 32 位和 64 位部分。
如果项目未正确配置以引用所需的 COM 组件的正确位数,则可能会出现错误。 如果 COM 组件仅可用作 32 位二进制文件,则你的应用只能在它作为 32 位进程运行时使用它。 如果 .NET 程序集是作为 32 位程序集构建的,则它可以引用 32 位 COM 组件。 如果它是作为 64 位程序集生成的,则可以引用 64 位 COM 组件。 但是,如果程序集构建为 Any CPU,则在引用 COM 组件时必须小心,因为 COM 组件没有等效的 Any CPU。 最好在 32 位和 64 位表单中生成应用程序,该表单引用正确的 COM 组件,假设 COM 组件在两种位形式中都可用。
还有另一个生成属性 Prefer32bit (也是 Visual Studio 中的复选框),这会导致在 64 位计算机上始终以 32 位的形式生成 Any CPU 程序集。 这适用于使用 32 位 COM 组件运行,但对于以后使用该项目的任何人来说,这可能会产生误导。
可以使用 Condition 属性上的 PlatformTarget 属性来引用单个 COM 组件的两种不同位形式。 例如,
<ItemGroup Condition="'$(PlatformTarget)' == 'x86'">
<COMFileReference Include="Controls\MyCom32.dll" />
</ItemGroup>
<ItemGroup Condition="'$(PlatformTarget)' == 'x64'">
<COMFileReference Include="Controls\MyCom64.dll" />
</ItemGroup>
为 x86 生成时,引用 32 位 COM DLL,但在为 x64 生成时,将引用 64 位版本。
MSBuild 如何解析 COM 引用
此处介绍了基本算法,包括解析引用的步骤序列、运行哪些可执行文件(例如), tlbimp.exe以及使用哪些 Windows API 调用。
在标准 .NET SDK 生成过程中,任务 ResolveComReference 在名为 ResolveComReferences 的目标中的常见目标文件中调用。 每个项目都会调用目标一次,并处理所有 COM 引用 COMReference 和 COMFileReference。 有关详细信息,请参阅 ResolveComReference 任务。
该任务将遍历依赖树,尝试解析所有引用。 单个引用的大多数错误不是致命的;MSBuild 继续尝试解析其他引用。 如果某些错误会同样影响所有引用,则这些错误是致命的。
如果在注册表或文件系统中找到 COM 组件,MSBuild 通常会尝试重复使用以前生成的包装器程序集,但如有必要,它会生成包装器。 使用默认设置时,运行 tlbimp.exe 生成包装程序集,并将它们放置在项目文件夹下的一个文件夹中。
通过设置属性,可以自定义提供给 ResolveComReference 任务的参数和设置。 您可以配置是否使用以前生成的包装器程序集,或者使用缓存中的程序集(如果启用了缓存选项)。 可以通过将 EmbedInteropTypes 选项设置为 True 来自定义输出文件夹。 此方法将投射类型嵌入正在生成的库或可执行文件中,而不是嵌入到单独的包装程序集。
诊断工具
若要诊断特定的生成错误,需要查看失败任务的详细输入 ResolveComReference 。
冗长诊断
可以在 MSBuild 命令行或 Visual Studio IDE 中使用开关 -v:diag 来设置详尽诊断信息。
在 “工具>选项 ”窗格中,展开“ 所有设置>项目”和“解决方案>生成和运行 ”部分,并将 MSBuild 项目生成输出详细程度 和 MSBuild 项目生成日志文件详细 选项设置为 “诊断”。
在 “工具>选项 ”对话框中,展开 “项目和解决方案>生成和运行 ”部分,并将 MSBuild 项目生成输出详细程度 和 MSBuild 项目生成日志文件详细 选项设置为 “诊断”。
查看二进制日志
生成二进制日志(-bl 在 MSBuild 命令行上切换),并使用 结构化日志查看器,该查看器提供一个 UI,以便更轻松地查看生成中的详细步骤、任务输入参数的值等。
这是在结构化日志查看器中对目标ResolveComReferences的视图。 可以检查参数和输出,它们表示已解析的引用路径和包装程序集。 在此示例中,将展开包装程序集位置以显示生成的包装程序集的位置和文件名。
确定组件的名称、GUID 和生成失败的版本后,即可查看提供给 ResolveComReference 任务的所有属性和项,并从系统注册表收集有关此组件的信息。 可以使用注册表编辑器 regedit.exe,但编辑注册表需要管理员权限。
注册表编辑器 (RegEdit)
熟悉 COM 组件的注册表位置,包括 32 位和 64 位注册表位置。 标识 COM 类类型的 GUID 称为类 ID(CLSID),存储在注册表中的 CLSID 下。 在 64 位计算机上,64 位组件注册到 HKEY_LOCAL_MACHINE\Software\Classes\CLSID\ 中,而 32 位组件注册到 HKEY_LOCAL_MACHINE\Software\WOW6432Node\Classes\CLSID\ 中。 在 32 位计算机上,32 位组件在 HKEY_LOCAL_MACHINE\Software\Classes\CLSID\ 中注册。 通常通过在注册表编辑器中搜索组件的名称或 GUID 来查找组件。 如果组件的注册位于配置单元(如 Visual Studio 组件的情况一样),您可能需要找到并打开该配置单元。 请参阅 编辑 Visual Studio 实例的注册表。
OleView
你可能希望用于 oleview.exe 调查单个 COM 类型,并获取类型库及其实现的接口等信息。 OLEView 不需要管理员权限,使用起来比regedit.exe更容易。
Procmon
可以使用进程监视器 procmon 监视在运行时使用 COM 的应用程序,并监视注册表更改。
常见问题
本部分介绍使用 COM 引用时可能发生的常见问题。
注册问题
组件是否已在生成计算机上注册? 如果未注册自己的某个组件,则可以使用命令行工具 regsvr32.exe手动注册它。 (此命令需要在计算机上提升的权限。如果组件是软件包(如 Office)的一部分,请确保安装了分发和注册组件的产品的正确版本。 请尝试修复或重新安装软件产品或包。
确认 . COMReference的属性。 名称、GUID 和版本是否与注册表中的一致且正确? 检查错字、拼写错误、版本不匹配或其他不一致。
如果两个版本都可用,请考虑组件是 32 位还是 64 位组件。 如果要面向 ARM64,请参阅 生成 Arm64X 二进制文件。
使用 COMFileReference 时,可以通过其文件位置引用 COM 组件。 检查文件路径的正确性,并确保它是正确的;如果它是相对路径,则需考虑当前工作目录。
类型库问题
生成互作程序集需要类型库。 类型库可以嵌入到二进制文件中,例如 DLL,也可以是单独的文件,即 TLB 文件(.tlb 扩展名)。
如果找不到 COM 组件的类型库,则通常可以生成一个类型库。 通常不必生成类型库。 它应随组件一起安装,由组件提供程序分发。 常见解决方案包括安装、升级或重新安装软件包。
在极少数情况下,需要一个类型库的情况下,可以通过 COM 二进制文件生成类型库。 例如,可以在 Visual Studio 资源编辑器或第三方资源编辑器中打开二进制文件,找到类型库资源,将其导出,并使用 “另存为” 将其另存为扩展名的 tlb 文本文件。
无法扫描依赖项
如果扫描引用的依赖项时出现问题,则会发生MSB3304。 该 ResolveComReference 任务尝试遍查整个依赖关系图,因此识别依赖关系的任何问题都会导致此错误。 给定的错误消息取决于具体问题。 如果错误来自 tlbimp.exe,还可以尝试 tlbimp.exe 在命令行上运行以获取有关问题的更多详细信息。
生成包装程序集时出现问题
如果指定了此选项,则 MSBuild 会尝试从以前的运行中找到缓存中的现有包装器程序集,或者重复使用以前生成的包装器。 如有必要,它会生成包装器。
tlbimp.exe该工具用于生成封装器程序集。 创建一个文件夹以包含包装程序集。 默认情况下,该文件夹在项目文件夹中创建,但这是由 WrapperAssemblyLocation 任务的 ResolveComReference 参数设置的。 如果此过程失败,则会提供 MSB3290 错误 或 MSB3283 错误 ,以及操作系统中的错误信息。 请参阅建议的故障排除提示,了解这些特定错误。
错误的 COMReference 或 COMFileReference 语法
如果 MSBuild 无法从项目文件转换元数据,则 会出现错误MSB3095。 如果发生这种情况,请检查项目文件中 COM 引用(或 COM 引用中使用的任何属性)中的拼写错误或其他错误。 根据记录在COMReference中的预期元数据,检查元素的子元素,以发现语法错误或缺少的元数据。
文件 IO 错误
该 ResolveComReference 任务读取和写入文件系统,这意味着它可以失败并出现系统 IO 错误,该错误由 MSBuild 捕获,并在通用 MSBuild 错误代码下报告。 请查看来自操作系统的消息中的错误详细信息。 检查任何路径的拼写和正确性,对于从 MSBuild 属性构造的路径,检查这些属性以确保其已被定义并具有预期值。
Warnings
COM 引用解析的一些问题会被报告为警告。 你可能会看到 MSB3305,这是在基础函数调用报告非严重问题(例如潜在的类型转换问题)时给出的。 通过将 MSBuild 属性 ResolveComReferenceSilent 设置为 true.,可以禁止显示此类警告。 我们不建议永久使用此设置,但如果你了解问题并想要暂时取消通知,则可能很有用。 还可以使用标准技术来禁止显示警告。 请参阅 “抑制生成警告”。
Windows 本机函数调用
可能调用 ResolveComReference 某些 Windows API 函数。 这些 API 调用中的错误将传递到生成输出。 此处列出了这些内容以供参考。
| 函数名称 | Description |
|---|---|
| FreeLibrary | 释放加载的动态链接库 (DLL) 模块。 |
| GetModuleFileName | 检索包含指定模块的文件的完全限定路径。 |
| LoadLibrary | 将指定的模块加载到调用进程的地址空间中。 |
| LoadRegTypeLib | 使用注册表信息加载类型库。 |
| QueryPathOfRegTypeLib | 检索已注册类型库的路径。 |