首页
/ Windows Terminal 新组件开发指南:创建 C++/WinRT 组件 DLL 并完成工程引用链接入

Windows Terminal 新组件开发指南:创建 C++/WinRT 组件 DLL 并完成工程引用链接入

2026-09-05 12:19:30作者:廉彬冶Miranda

本文基于 OpenConsole(Windows Terminal 与 Windows 控制台宿主共存的仓库)官方指南 doc/creating_a_new_project.md,完整讲解在该仓库中创建一个全新 C++/WinRT 组件 DLL 的全过程:MSBuild props 的 Import 顺序、WinMD 引用声明、.def 导出文件,以及 AppXManifest.xml 中 WinRT 激活机制的底层原理。读完本文,你可以独立完成"新 WinRT DLL 从零创建到被主工程成功 GetActivationFactory 激活"的全链路操作,并掌握三类典型构建/运行时故障的定位手段。

一、背景:仓库中 C++/WinRT 工程的组织方式

Windows Terminal 的 C++ 侧大量使用 C++/WinRT 与 XAML。要新增一个可被其他工程引用的 WinRT 组件 DLL,原作者的建议非常直接:拿一个现成 DLL 的 .vcxproj 当模板来抄,比如终端控件的 TerminalControl.vcxproj,"大部分内容照抄,同时对照本文清单逐项复核"。

在动手之前,先理解仓库现有的两层工程结构,这是模板能抄对的前提。以 TerminalControl 组件为例,它被拆成两个工程:

这一结构由两条公共 props 链驱动:

  • common.openconsole.props:因为 wapproj 项目中 $(SolutionDir) 永远无法被正确求值,仓库改用该文件定义 $(OpenConsoleDir) 来替代 $(SolutionDir)第 9-11 行);
  • src/cppwinrt.build.pre.props:核心前置配置。它设置 OpenConsoleCppWinRTProject=trueCppWinRTEnabled=true,强制使用 pch.h 预编译头并加 /bigobj 选项(第 63-77 行);对 DynamicLibrary 类型的工程,自动追加 _WINRT_DLL 预处理器宏,并在 第 96-103 行 做了关键魔法——只要项目目录下存在 $(ProjectName).def,就自动将其设为 ModuleDefinitionFile
<!-- src/cppwinrt.build.pre.props -->
<ItemDefinitionGroup Condition="'$(ConfigurationType)'=='DynamicLibrary'">
  <ClCompile>
    <PreprocessorDefinitions>_WINRT_DLL;%(PreprocessorDefinitions)</PreprocessorDefinitions>
  </ClCompile>
  <Link>
    <ModuleDefinitionFile Condition="Exists('$(ProjectName).def')">$(ProjectName).def</ModuleDefinitionFile>
  </Link>
</ItemDefinitionGroup>

二、创建新 WinRT 组件 DLL 的完整步骤

以下步骤逐条继承自 doc/creating_a_new_project.md 的原始清单,并结合当前仓库的真实工程文件补充了上下文。

步骤 1:以现有 .vcxproj 为基线拷贝

dll/TerminalControl.vcxproj 这类现有 DLL 工程为蓝本新建自己的 .vcxproj。注意其中几个必须保留的关键属性(第 3-25 行):

<ConfigurationType>DynamicLibrary</ConfigurationType> <!-- 构建 dll 而不是 exe -->
<OpenConsoleUniversalApp>true</OpenConsoleUniversalApp>  <!-- 触发一组 Windows Universal 属性 -->
<CppWinRTNamespaceMergeDepth>3</CppWinRTNamespaceMergeDepth>

其中 CppWinRTNamespaceMergeDepth 值得特别留意:项目内注释说明,若项目中存在 XAML 文件,C++/WinRT 默认会把命名空间合并深度设为 1,导致 Microsoft.Terminal.Control 被压缩成 Microsoft,生成的 WinMD 被视为"包含整个 Microsoft 命名空间",所有依赖它的工程都无法正常编译,因此必须显式设为 3。

步骤 2:把 pre/post props 放在正确位置

原文档第一条检查项:pre props 必须放在 .vcxproj 最顶部,post props 必须放在最底部。原文给出的标准骨架:

<!-- pre props -->
<Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />

<!-- everything else -->

<!-- post props -->
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.post.props" />

对照真实工程 TerminalControlLib.vcxproj 可以看到实际写法(第 27-29 行):

<Import Project="..\..\..\common.openconsole.props" Condition="'$(OpenConsoleDir)'==''" />
<Import Project="$(OpenConsoleDir)src\common.nugetversions.props" />
<Import Project="$(OpenConsoleDir)src\cppwinrt.build.pre.props" />

文件尾部(第 182-185 行)则是:

<Import Project="$(OpenConsoleDir)src\cppwinrt.build.post.props" />
<!-- 注意:此行必须在 cppwinrt.build.post.props 之后,
     因为它包含的 VS 自带 props 会覆盖 cppwinrt.targets 的产物 -->
<Import Project="$(OpenConsoleDir)src\common.nugetversions.targets" />

可以看到现网工程比文档骨架多了一条 common.nugetversions.props(NuGet 依赖版本管理),新建工程时建议一并保留。

步骤 3:在 WindowsTerminal.vcxprojTerminalApp.vcxproj 中添加 ProjectReference

原文档第二条检查项:新 DLL 工程创建后,必须同时WindowsTerminal.vcxprojTerminalApp.vcxproj 中添加指向它的 <ProjectReference>

这一步不只是"让主工程依赖你",而是把新工程接入了通往打包工程的引用树:只有在引用树中,新 DLL 的 WinMD 才能被 Centennial 打包工程 CascadiaPackage 枚举到,进而自动写入 AppXManifest.xml(原理见第三节)。漏掉这一步是后文"Class not registered"故障的最常见根因。

步骤 4:在 TerminalAppLib.vcxproj 中引用新的 WinMD

原文档第三条检查项:除了 ProjectReference,还需要在 TerminalAppLib.vcxproj 中显式添加一条 WinMD <Reference>。原文给出的模板(NewDLL/TerminalNewDLL 为占位名,替换为你自己的组件):

<Reference Include="Microsoft.Terminal.NewDLL">
  <HintPath>$(OpenConsoleCommonOutDir)\TerminalNewDLL\Microsoft.Terminal.NewDLL.winmd</HintPath>
  <IsWinMDFile>true</IsWinMDFile>
  <Private>false</Private>
  <CopyLocalSatelliteAssemblies>false</CopyLocalSatelliteAssemblies>
</Reference>

当前仓库中这条声明的真实形态可对照 TerminalAppLib.vcxproj 第 446-451 行Microsoft.Terminal.Control 的引用,属性完全一致:IsWinMDFile=true 告诉构建系统这是元数据文件,Private=falseCopyLocalSatelliteAssemblies=false 则确保 WinMD 不会被复制到输出目录——这正是后文第一类故障("type already exists")的预防手段。

步骤 5:为新工程编写 .def 文件

原文档第四条检查项:确保新工程有一个 .def 文件,且必须导出 WINRT_GetActivationFactory,否则其他工程无法通过 GetActivationFactory 拿到你 DLL 中的类。原文给出的最小内容:

EXPORTS
DllCanUnloadNow = WINRT_CanUnloadNow                    PRIVATE
DllGetActivationFactory = WINRT_GetActivationFactory    PRIVATE

仓库中的真实样本 Microsoft.Terminal.Control.def 前 4 行正是上述 WinRT ABI 部分,随后还追加了一组 Flat C ABI 导出(CreateTerminalTerminalSendCharEvent 等,供 C 接口宿主直接调用):

EXPORTS
  ; WinRT ABI
  DllCanUnloadNow = WINRT_CanUnloadNow                    PRIVATE
  DllGetActivationFactory = WINRT_GetActivationFactory    PRIVATE

  ; Flat C ABI
  AvoidBuggyTSFConsoleFlags
  CreateTerminal
  ...

也就是说:两行 WinRT ABI 导出是必备项,其余导出按需追加。得益于 src/cppwinrt.build.pre.propsModuleDefinitionFile 的自动探测逻辑,只要文件命名为 $(ProjectName).def 并放在工程目录下,链接器会自动挂上它,无需在 .vcxproj 中额外配置(但 dll/TerminalControl.vcxproj 第 58 行 仍会以 <None> 项显式列出,方便在解决方案资源管理器中可见)。

三、WinRT 激活机制与 AppXManifest.xml

原文档用一段话解释了上述 .def 导出为什么必不可少,这里完整展开:

AppXManifest.xml 定义了每个类归属于哪个 DLL。当某个项目想使用类 X.Y.Z 时,运行时/宿主会在清单定义中查到它来自 X.Y.dll,随后加载该 DLL 并调用其中的特定函数 GetActivationFactory(L"X.Y.Z") 拿到想要的类。因此:

  • .def 中的 DllGetActivationFactory = WINRT_GetActivationFactory 是整条激活链的入口点,缺了它清单定义写得再对也无法激活;
  • 清单中的类定义是激活正常工作所必需的前提——原作者在实操中就反复核对过"期望的定义确实在文件里"。

关于清单条目是否需要手写,原文档给出了一条分界线(原文使用了 "Centennial Packaging project" 指代 CascadiaPackage 这个打包工程):

  • 如果你的新库最终会汇入 CascadiaPackage:无需手动编辑 AppXManifest.xml。该打包工程会自动遍历 WinMD 的引用树,把类定义信息缝合进清单;
  • 如果你的新工程不经过任何会自动生成清单的打包工程:必须自己把类定义手动加进 AppXManifest.xml

这与步骤 3"同时引用进 WindowsTerminal.vcxprojTerminalApp.vcxproj"的要求正好呼应:引用树决定清单能否自动生成。

四、故障排查:三类典型报错的定位与修复

原文档的 Troubleshooting 一节给出了三类高价值排障经验,以下逐条继承并结合仓库证据补充。

4.1 报错:type already exists in file ... Microsoft.UI.Xaml.winmd

现象:构建期出现形如以下错误:

X found processing metadata file ..\blah1\Microsoft.UI.Xaml.winmd, type already exists in file ..\blah\NewDLLProject\Microsoft.UI.Xaml.winmd.

原因Microsoft.UI.Xaml.winmd 本不该出现在你的输出目录,却被复制进去了,导致类型元数据重复。

修复:在 .vcxproj 顶部加入这个块,把所有引用的默认行为改为非私有(即"不要复制进我的输出目录"):

<ItemDefinitionGroup>
  <Reference>
    <Private>false</Private>
  </Reference>
</ItemDefinitionGroup>

仓库中的现成佐证:TerminalControlLib.vcxproj 第 177-179 行 就带着这一写法,与步骤 4 中对单个 <Reference> 手工设置 Private=false 互为表里——前者是全局兜底,后者是逐条声明。

4.2 报错:Class not Registered

可能原因:某个类没有被注册进 app manifest。

排查路径:检查构建产物 src/cascadia/CascadiaPackage/bin/x64/Debug/AppX/AppXManifest.xml(该文件为打包工程构建输出,不在源码树中),确认其中是否存在你新 DLL 对应类的条目。

修复:如果条目缺失,回头核对是否已在 WindowsTerminal.vcxprojTerminalApp.vcxproj 中都添加了 <ProjectReference> 块——正是引用树的断链让打包工程枚举不到你的 WinMD。

4.3 模糊报错 Error in the DLL,且日志显示新 DLL 加载后立刻被卸载

判断:这是清单定义缺失的典型征兆。按两条分支处理:

  • 若你的新 DLL 是作为某个汇入 CascadiaPackage 的工程被引用:先确认你真的创建了 .def 文件(缺 WINRT_GetActivationFactory 导出时,激活会立即失败,表现为 DLL 刚加载就被卸载);
  • 若你的新工程不经过自动填充 AppXManifest 引用信息的打包工程:那些引用定义需要你手动添加。

五、落地前检查清单

将全文收敛为可执行清单,新建 WinRT 组件 DLL 前逐项过一遍:

检查项 要求 依据
工程模板 复制现有 DLL 的 .vcxproj(如 TerminalControl.vcxproj),保留 DynamicLibraryOpenConsoleUniversalApp=trueCppWinRTNamespaceMergeDepth=3 原文档 + 源码注释
pre props common.openconsole.props + cppwinrt.build.pre.props 位于 .vcxproj 顶部 TerminalControlLib.vcxproj#L27-L29
post props cppwinrt.build.post.props 位于 .vcxproj 底部 TerminalControlLib.vcxproj#L182
引用树 WindowsTerminal.vcxprojTerminalApp.vcxproj 均添加 ProjectReference 原文档检查项
WinMD 引用 TerminalAppLib.vcxproj 中添加 IsWinMDFile=truePrivate=falseReference TerminalAppLib.vcxproj#L446-L451
.def 文件 导出 DllCanUnloadNowDllGetActivationFactory,命名为 $(ProjectName).def 以触发自动链接 cppwinrt.build.pre.props#L96-L103
清单 汇入 CascadiaPackage 则自动;否则手动写 AppXManifest.xml 类定义 原文档说明

需要说明的环境前提:整套构建链基于 MSBuild 属性系统,要求 Visual Studio 17.0(2022)及以上工具链(cppwinrt.build.pre.props 第 15 行声明了 MinimumVisualStudioVersion),完整的构建环境准备可参考 doc/building.md。按此流程走下来,你的新 C++/WinRT 组件即可像 Microsoft.Terminal.Control 一样,被主工程、设置面板乃至打包链路统一消费。

登录后查看全文
热门项目推荐
相关项目推荐

项目优选

收起
kernelkernel
deepin linux kernel
C
33
18
ops-transformerops-transformer
本项目是CANN提供的transformer类大模型算子库,实现网络在NPU上加速计算。
C++
1.12 K
2.72 K
kernelkernel
openEuler内核是openEuler操作系统的核心,既是系统性能与稳定性的基石,也是连接处理器、设备与服务的桥梁。
C
528
588
ops-nnops-nn
本项目是CANN提供的神经网络类计算算子库,实现网络在NPU上加速计算。
C++
906
1.82 K
pytorchpytorch
作为 Ascend for PyTorch 社区的核心组件,TorchNPU 是昇腾专为 PyTorch 打造的深度学习适配插件,使 PyTorch 框架能够直接调用昇腾 NPU,为开发者提供昇腾 AI 处理器的超强算力。
Python
854
1.34 K
docsdocs
暂无描述
Markdown
891
5.78 K
jiuwenswarmjiuwenswarm
JiuwenSwarm 是一款基于openJiuwen开发的智能AI Agent,它能够将大语言模型的强大能力,通过你日常使用的各类通讯应用,直接延伸至你的指尖。
Python
3.53 K
1.01 K
ops-mathops-math
本项目是CANN提供的数学类基础计算算子库,实现网络在NPU上加速计算。
C++
1.34 K
1.45 K
cann-learning-hubcann-learning-hub
CANN 学习中心仓,支持在线互动运行、边学边练,提供教程、示例与优化方案,一站式助力昇腾开发者快速上手。
Jupyter Notebook
987
504
AscendNPU-IRAscendNPU-IR
AscendNPU-IR是基于MLIR(Multi-Level Intermediate Representation)构建的,面向昇腾亲和算子编译时使用的中间表示,提供昇腾完备表达能力,通过编译优化提升昇腾AI处理器计算效率,支持通过生态框架使能昇腾AI处理器与深度调优
C++
540
384