创建 C++ WinRT 组件

通过 Cpp/WinRT 项目模板创建一个 WinRT 组件工程 CppWinrtComponent.vcxproj,主要接口定义如下:

namespace CppWinrtComponent
{
	[default_interface]
	runtimeclass Class
	{
		Class();
		String GetModule();
	}
}

最终该项目 CppWinrtComponent 可以被编译生成两个 WinRT 组件的核心部分:

  • CppWinrtComponent.winmd, 提供接口描述
  • CppWinrtComponent.dll, 负责接口实现,基于 COM 技术

由于该项目 CppWinrtComponent.vcxproj 默认的平台是 Universal Windows,所以它使用的 VC 运行时函数都是 Store 版本,这导致在非 Universal Windows 应用中会出现找不到 VC 运行时库的异常。为了解决这个问题,针对该类型项目,Visual Studio 2019 version 16.9 版本开始支持属性 Windows Desktop Compatible,设置该属性后所有的 VC 运行时函数会被链接到 Deskotp 版本。

该属性可以在 VS 项目属性面板中配置,对应的 vcxproj 中配置如下:

<PropertyGroup>
	<DesktopCompatible>true</DesktopCompatible>
</PropertyGroup>

消费 C++ WinRT 组件

本文主要讨论在如下语言、框架中使用该组件:

  • C++ Desktop
  • C++ UWP
  • .Net Framework Desktop
  • .Net5 Desktop

C++ Desktop

使用 VS 的 C++ 项目模板,创建一个空的控制台应用程序 ConsumerCppConsole.vcxproj。

1. 使用 Nuget 包管理器给该项目安装 Microsoft.Windows.CppWinRT

建议安装最新版本,在 Microsoft.Windows.CppWinRT 2.0.200115.8 之后版本中,引入了对 Reg Free 的支持,可以简化 WinRT 组件使用流程,不需要创建额外的 Manifest 文件。

2. 添加对项目 CppWinrtComponent 生成的 CppWinrtComponent.WinMD 的引用

不能直接在 VS 中添加对项目 CppWinrtComponent.vcxproj 的工程引用,但是可以直接添加对文件 CppWinrtComponent.winmd 的引用,在该文件引用的属性中,VS 默认会把”Copy Local” 设置为 True,该操作会导致 CppWinrtComponent.dll 在编译后会被复制到输出目录,这也是期待的行为。

虽然在 VS 的 GUI 中无法直接添加对 CppWinrtComponent.vcxproj 的工程引用,但是可以手动编辑 ConsumerCppConsole.vcxproj 来添加!

...
<ItemGroup>
	<ProjectReference Include="..\CppWinrtComponent\CppWinrtComponent.vcxproj">
		<Project>{184b97de-746b-47d4-b055-d18f24b57dba}</Project>
	</ProjectReference>
</ItemGroup>
...

3. 使用 Nuget 包管理器给该项目安装 Microsoft.VCRTForwarders.140

因为 CppWinrtComponent 是用 C++ 代码编写,必不可少会用到 VC 的运行时库,但是默认情况下,CppWinrtComponent.vcxproj 是一个 Universal Windows 的项目,所以它链接的 VC 运行时库都是 Store 版本,Store 版本 VC 运行时库不随 Windows 分发,所以我们的 Desktop 应用在运行时找不到这些 VC 运行时库。

Microsoft.VCRTForwarders.140 的原理是把所有的 Store 版本的 VC 运行时 API 全部链接到 Desktop 版本。组件如其名,就是一个 Forwarder!

如果 CppWinrtComponent.vcxproj 中的 DesktopCompatible 被打开,则此步骤3不再必要。

4. 调用 CppWinrtComponent 组件

CppWinrt.exe 会解析 CppWinrtComponent.winmd 生成头文件 winrt/CppWinrtComponent.h, 通过该头文件,就可以调用组件 CppWinrtComponent 了。

C++/WinRT UWP

C++/WinRT 有一个 VS 的拓展包,提供了几种基于 C++/WinRT 的项目模板。(https://marketplace.visualstudio.com/items?itemName=CppWinRTTeam.cppwinrt101804264)

用模板创建一个 UWP 项目 ConsumerCppWinrtUWP.vcproj。

1. 添加对项目 CppWinrtComponent 生成的工程引用

因为 ConsumerCppWinrtUWP 和 CppWinrtComponent 都是 Universal Windows 项目,所以可以直接通过 VS 添加对 CppWinrtComponent 的工程引用,CppWinrtComponent 输出的 CppWinrtComponent.winmd 和 CppWinrtComponent.dll 会被复制到 ConsumerCppWinrtUWP 的生成目录。

2. 调用 CppWinrtComponent 组件

CppWinrt.exe 会解析 CppWinrtComponent.winmd 生成头文件 winrt/CppWinrtComponent.h, 通过该头文件,就可以调用组件 CppWinrtComponent 了。

.Net Framework Desktop

在 VS 里面创建一个 .Net Framework console 项目 ConsumerNetFrameworkConsole.csproj,对于使用 Winrt 组件而言,基于.Net Framework 的 WPF 和 WinFrom 应用与 Console 应用没有任何区别。

值得注意的是,微软在 .Net Framework 4.5引入了对 WinRT 的支持。在 .Net5 中移除了对 WinRT 的原生支持。

1. 添加对项目 CppWinrtComponent 生成的工程引用

2. 创建 Reg Free Manimest 文件

C++ WinRT 组件实际是一种进程内(In Process)COM 服务器,使用之前需要向 OS 注册。在 UWP 应用中,Package Manifest 里面可以声明所有用到的 WinRT 组件,在运行时或者安装时,OS 可以根据此文件完成 COM 服务器的注册调用(此处具体细节暂时不清楚,但大致原理应该是这样)。

同样的,针对桌面应用也可以生成一个 manifest 文件,用来描述 WinRT 组件,该文件会被嵌入到桌面应用生成的可执行文件中,在运行时帮助我们完成对 WinRT 组件的加载(COM调用)。

该文件结构如下:

<?xml version="1.0" encoding="utf-8"?>
<assembly manifestVersion="1.0" xmlns="urn:schemas-microsoft-com:asm.v1">
	<!--名称 MyApplication.app 无关紧要,确保唯一就可以-->
	<assemblyIdentity version="1.0.0.0" name="MyApplication.app"/>
	<!-- WinRT 组件的 DLL 名称 -->
	<file name="CppWinrtComponent.dll">
		<!-- 线程模型默认 both(代表 STA 和 MTA),name 是在 WinRT 组件中定义的 "命名空间.接口名称" -->
		<activatableClass
			name="CppWinrtComponent.Class"
			threadingModel="both"
			xmlns="urn:schemas-microsoft-com:winrt.v1" />
	</file>
</assembly>

可以将该文件任意命名,此处命名为 app.manifest, 然后把它添加到 ConsumerNetFrameworkConsole.csproj 中,为避免出错,选择手动编辑该项目文件,加入如下节点:

<PropertyGroup>
	<ApplicationManifest>app.manifest</ApplicationManifest>
</PropertyGroup>

最后编译项目 ConsumerNetFrameworkConsole.csproj,该文件会被嵌入到 ConsumerNetFrameworkConsole.exe 中。所以在分发 ConsumerNetFrameworkConsole.exe 的时候,并不需要将 manifest 文件一起分发。

3. 使用 Nuget 包管理器给该项目安装 Microsoft.VCRTForwarders.140

参考 C++ Desktop 步骤3。

4. 调用 CppWinrtComponent 组件

因为添加了对项目 CppWinrtComponent 的引用,VS 可以从 CppWinrtComponent.winmd 中读取到接口的元数据信息,所以和普通的 .Net 对象一样,可以通过命名空间、类名访问到该 WinRT 组件。

.Net5 Desktop

在 VS 里面创建一个 .Net5 Console 项目 ConsumerNet5Console.csproj。

微软把 WinRT 的支持从 .Net5 中拿掉后,创建了项目 C#/WinRT, 它包含一个 cswinrt.exe, 可以解析 WinRT 组件的 wimmd 文件并创建相应的 C# 代码,这些代码是关于如果通过 COM 的方式访问 WinRT 组件。这些代码和 C++ Desktop 中 CppWinRT 生成的代码非常一致,几乎就是同一份代码 C# 改写版。实际上 C#/WinRT 和 CppWinRT 是用相同的解析器来解析 winmd 文件的。 详情可参考微软开源项目 xlang

1. 创建 .Net5 Library CppWinrtComponentProjection.csproj

因为 C#/WinRT 会创建用来访问 WinRT 组件的代码,为了方便使用这些代码,在这儿创建一个单独的 .Net5 Library 来包装这些代码。 实际上可以略过这一步,直接在项目 ConsumerNet5Console.csproj 上执行以下步骤!

2. 使用 Nuget 包管理器给 CppWinrtComponentProjection.csproj 安装 Microsoft.Windows.CsWinRT

3. 给 CppWinrtComponentProjection.csproj 添加项目 CppWinrtComponent.csproj 的工程引用

C#/WinRT 会检查项目 CppWinrtComponentProjection.csproj 依赖的所有 WinRT 组件,并根据步骤4中指定的 WinRT 组件中的命名空间,为其创建C#访问代码。

4. 添加CsWinRTIncludes

C#/WinRT 会读取该项目属性,它指定了引入的 WinRT 组件中的命名空间,这样才能为这个命名空间生成 C# 访问代码。

需要手动在 CppWinrtComponentProjection.csproj 中添加如下节点:

<PropertyGroup>
	<CsWinRTIncludes>CppWinrtComponent</CsWinRTIncludes>
</PropertyGroup>

5. 编译CppWinrtComponentProjection.csproj

编译这个 Library, 在该项目 Obj 目录 obj\x64\Debug\net5.0-windows10.0.19041.0\Generated Files\ 会生成如下四个文件:

  • CppWinrtComponent.cs
  • WinRT.cs
  • WinRT_Interop.cs
  • WinRTEventHelpers.cs

它们就是用来访问 WinRT 组件 CppWinrtComponent 的全部 C# 代码! 同时它们也会被编译成一个 .Net5 Library CppWinrtComponentProjection, 这个 Library 就是所谓的 CppWinrtComponent 的 Projection。

6. 给ConsumerNet5Console.csproj 添加工程引用 CppWinrtComponentProjection.csproj

因为最终需要在 ConsumerNet5Console.csproj 使用 WinRT 组件,所以加入对 projection 项目的引用,该引用是一个标准的 .Net5 Library。

7. 复制 CppWinrtComponent.dll 到输出项目ConsumerNet5Console的输出目录

前6步所作的工作只是用来调用 WinRT 组件,真正的组件服务器是 CppWinrtComponent.Dll,它会在运行时被加载到 ConsumerNet5Console 的进程中。

8. 使用 Nuget 包管理器给该项目 ConsumerNet5Console.csproj 安装 Microsoft.VCRTForwarders.140

9. 调用 CppWinrtComponent 组件

添加完对 CppWinrtComponentProjection 的引用后,就可以直接用 命名空间.类名 访问WinRT 组件 CppWinrtComponent。

示例代码

参考