Windows Terminal 新组件开发指南:创建 C++/WinRT 组件 DLL 并完成工程引用链接入
本文基于 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 组件为例,它被拆成两个工程:
- TerminalControlLib.vcxproj:
StaticLibrary,承载全部.idl、.xaml、.cpp源文件(工程显示名为Microsoft.Terminal.Control.Lib); - dll/TerminalControl.vcxproj:
ConfigurationType为DynamicLibrary,最终产出Microsoft.Terminal.Control.dll及其 WinMD。它几乎不放源文件(项目注释明确要求"源文件一律放 TerminalControlLib"),而是通过ProjectReference引用 Lib 工程,利用 Lib 的 winmd 作为自己的 winmd,并在 第 58 行 以<None Include="$(ProjectName).def" />挂入模块定义文件。
这一结构由两条公共 props 链驱动:
- common.openconsole.props:因为
wapproj项目中$(SolutionDir)永远无法被正确求值,仓库改用该文件定义$(OpenConsoleDir)来替代$(SolutionDir)(第 9-11 行); - src/cppwinrt.build.pre.props:核心前置配置。它设置
OpenConsoleCppWinRTProject=true、CppWinRTEnabled=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>
- src/cppwinrt.build.post.props:后置配置,内容是导入
common.build.post.props完成收尾处理。
二、创建新 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.vcxproj 和 TerminalApp.vcxproj 中添加 ProjectReference
原文档第二条检查项:新 DLL 工程创建后,必须同时在 WindowsTerminal.vcxproj 和 TerminalApp.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=false 与 CopyLocalSatelliteAssemblies=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 导出(CreateTerminal、TerminalSendCharEvent 等,供 C 接口宿主直接调用):
EXPORTS
; WinRT ABI
DllCanUnloadNow = WINRT_CanUnloadNow PRIVATE
DllGetActivationFactory = WINRT_GetActivationFactory PRIVATE
; Flat C ABI
AvoidBuggyTSFConsoleFlags
CreateTerminal
...
也就是说:两行 WinRT ABI 导出是必备项,其余导出按需追加。得益于 src/cppwinrt.build.pre.props 中 ModuleDefinitionFile 的自动探测逻辑,只要文件命名为 $(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.vcxproj 和 TerminalApp.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.vcxproj 和 TerminalApp.vcxproj 中都添加了 <ProjectReference> 块——正是引用树的断链让打包工程枚举不到你的 WinMD。
4.3 模糊报错 Error in the DLL,且日志显示新 DLL 加载后立刻被卸载
判断:这是清单定义缺失的典型征兆。按两条分支处理:
- 若你的新 DLL 是作为某个汇入
CascadiaPackage的工程被引用:先确认你真的创建了.def文件(缺WINRT_GetActivationFactory导出时,激活会立即失败,表现为 DLL 刚加载就被卸载); - 若你的新工程不经过自动填充
AppXManifest引用信息的打包工程:那些引用定义需要你手动添加。
五、落地前检查清单
将全文收敛为可执行清单,新建 WinRT 组件 DLL 前逐项过一遍:
| 检查项 | 要求 | 依据 |
|---|---|---|
| 工程模板 | 复制现有 DLL 的 .vcxproj(如 TerminalControl.vcxproj),保留 DynamicLibrary、OpenConsoleUniversalApp=true、CppWinRTNamespaceMergeDepth=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.vcxproj 与 TerminalApp.vcxproj 均添加 ProjectReference |
原文档检查项 |
| WinMD 引用 | TerminalAppLib.vcxproj 中添加 IsWinMDFile=true、Private=false 的 Reference |
TerminalAppLib.vcxproj#L446-L451 |
.def 文件 |
导出 DllCanUnloadNow 与 DllGetActivationFactory,命名为 $(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 一样,被主工程、设置面板乃至打包链路统一消费。
atomcodeClaude Code 的开源替代方案。连接任意大模型,编辑代码,运行命令,自动验证 — 全自动执行。用 Rust 构建,极致性能。 | An open-source alternative to Claude Code. Connect any LLM, edit code, run commands, and verify changes — autonomously. Built in Rust for speed. Get StartedRust0623
Hy4-previewHy4 preview 是由腾讯混元团队研发的新一代混合专家(MoE)旗舰模型。模型总参数量 770B,每个 token 激活 49B,主干共包含78层,第一层采用标准 FFN,其余 77 层均为 MoE 结构,每层包含 256 个路由专家与 1 个共享专家,每个 token 激活 top-8 路由专家及共享专家。主干之外原生内置 1 层 MTP(总参数量 10B,激活 0.7B)以支持投机解码。Python00
GLM-5.3GLM-5.3 与 GLM-5.2 使用相同的基座模型——所有提升均来自后训练。与 GLM-5.2 相比,它在复杂编程和长程任务上的表现显著提升。Jinja00
GLM-5.3-FlashGLM-5.3-Flash (320B-A18B),是GLM-5系列的首个原生多模态模型。320B总参数,能力超过GLM-5.2Jinja00
Spark-X2.5-4BSpark-X2.5-4B 旨在让强大的 AI 更实用、更高效、更易获得。在广泛日常任务中表现强劲,涵盖对话、写作、翻译、推理、编码、工具调用以及智能体工作流,并在同等规模的开源模型中取得领先成绩。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00
Spark-X2.5-1.7BSpark-X2.5-1.7B 旨在让强大的 AI 更加实用、高效且易于获取。这些模型在广泛的日常任务中表现出色,涵盖对话、写作、翻译、推理、编程、工具调用和智能体工作流,并在同等规模的开源模型中取得领先结果。Spark-X2.5 将面向效率的架构与最高 1M tokens 的原生上下文窗口相结合,并支持 200 多种语言。Python00