- 示例工程
【免费下载链接】Windows-universal-samples
API samples for the Universal Windows Platform.
本文围绕 Windows-universal-samples 仓库中的 WRLInProcessWinRTComponent 示例(Samples/WRLInProcessWinRTComponent),系统讲解如何使用 C++ 的 Windows Runtime C++ Template Library(WRL)编写一个进程内(in-process)Windows Runtime 组件,并分别从 C++(C++/CX 与纯 WRL 两种方式)、C# 与 JavaScript 中消费它。读完本文,你将掌握 WRL 组件编写中从 IDL 契约、运行时类实现、激活工厂、事件源到模块导出的完整链路,以及三种语言客户端调用同一组件时的投影差异与代码写法。
示例概览:一个"厨房"领域的进程内 WRL 组件
WRLInProcessWinRTComponent 的核心是一个名为Microsoft.SDKSamples.Kitchen的命名空间,内部定义了一套"烤箱"(Oven)领域模型:Oven烤箱、Bread面包、BreadBakedEventArgs烤面包完成事件参数,以及Dimensions结构体与OvenTemperature枚举。示例演示的正是"如何用 WRL 编写一个可被 C++、JS、C# 共同消费的进程内组件"这一 UWP 平台架构主题。
从仓库目录结构看,该示例分为服务器端与三个语言客户端:
| 目录 | 作用 |
|---|---|
| cpp/Server | WRL 组件本体:IDL 契约、OvenServer.cpp实现、module.cpp模块入口,以及对应的WRLInProcessWinRTComponent_server.vcxproj工程 |
| cpp/cpp | C++ 客户端,含 4 个场景:C++/CX 消费、纯 WRL 消费、两类自定义异常场景 |
| cpp/cs | C# 客户端,含烤箱消费与自定义异常两个场景 |
| cpp/js | JavaScript(WinJS)客户端,含烤箱消费与自定义异常两个场景 |
| WRLInProcessWinRTComponent.sln | 解决方案文件,组织 Server 与各语言客户端工程 |
同一份组件、三种语言客户端,是理解"WinRT 语言投影"的最佳入口:同样的Oven类,在 C++/CX 里是ref new,在 C# 里是new,在 JS 里是new SampleNamespace.Oven(...),而底层 ABI 完全一致。
进程内(in-process)组件与 WRL 的基础定位
"进程内"指组件 DLL 被直接加载到调用方进程地址空间中,无需跨进程调用,也没有额外的序列化/封送开销。与仓库中的 WRLOutOfProcessWinRTComponent(进程外组件)形成对照:后者通过代理/存根(proxy/stub)在独立进程中激活对象。
WRL 是 Windows 8 时代引入的模板库,提供了一套低层、无异常、基于 COM 接口的 WinRT 组件编写方式。它与 C++/CX(语言扩展投影)、C++/WinRT(标准 C++ 投影)的区别在于:WRL 直接面向 ABI 接口(ABI::命名空间下的IInspectable派生接口)编程,所有方法返回HRESULT,所有字符串使用HSTRING,开发者需要显式管理ComPtr生命周期。本示例恰好提供了 C++/CX 与纯 WRL 两种消费端实现(Scenario1 与 Scenario2),可直观对比同一组件的两种 C++ 使用风格。
IDL 契约:定义运行时类、结构体与枚举
WRL 组件编写的起点是 MIDL 文件(IDL),它声明了组件的公共类型契约。本示例的契约位于 cpp/Server/Microsoft.SDKSamples.Kitchen.idl,其中包含:
- 结构体与枚举:
Dimensions(Depth/Height/Width三个double字段)和OvenTemperature(Low/Medium/High三个值),均带[version(1.0)]版本特性。 - 运行时类:
Bread、BreadBakedEventArgs、Oven三个runtimeclass声明,其中Oven带有[activatable(1.0), activatable(IOvenFactory, 1.0)],表示它既支持无参默认激活,也支持通过自定义工厂接口IOvenFactory激活——即通过CreateOven(Dimensions)传入结构体构造。 - 接口定义:每个接口都带有显式
[uuid(...)]。值得注意的设计是IOven通过requires IAppliance声明继承关系(IDL 层面前向声明IAppliance后,Oven类在 ABI 层面同时暴露IOven与IAppliance两个接口);IOven还声明了BreadBaked事件([eventadd]/[eventremove]),事件参数类型为Windows.Foundation.TypedEventHandler<Oven*, BreadBakedEventArgs*>。 - exclusiveto 与 default 接口:
IBread用[exclusiveto(Bread)]限定只能由Bread实现;各运行时类通过[default] interface指定默认接口,供非反射调用场景使用。
这段 IDL 是整个示例的"骨架":OvenServer.cpp中的每个类、每个STDMETHODIMP方法都能在 IDL 中找到对应声明,三端客户端的语言投影行为也完全由这份契约驱动。
WRL 实现剖析:OvenServer.cpp
组件的核心实现在 cpp/Server/OvenServer.cpp,文件头部集中引用了<wrl.h>、<wrl\implements.h>、<wrl\event.h>、<windows.system.threading.h>以及由 IDL 生成的Microsoft.SDKSamples.Kitchen.h。
运行时类:RuntimeClass + InspectableClass
三个运行时类均派生自Microsoft::WRL::RuntimeClass<...>并配合WrlFinal(禁止继续派生),同时用InspectableClass宏注册类名与信任级别:
Bread实现RuntimeClass<IBread>,持有HString m_flavor,通过RuntimeClassInitialize(HSTRING flavor)注入初始化数据,get_Flavor用m_flavor.CopyTo(value)返回只读属性。Oven实现RuntimeClass<IOven, IAppliance>,内部用AgileEventSource<ITypedEventHandler<Oven*, BreadBakedEventArgs*>> _breadBaked存储事件订阅者,并维护_dims与_temperature两个私有状态。它提供了两个RuntimeClassInitialize重载:无参版本(默认Dimensions为 1×1×1、默认温度为Medium)与带Dimensions参数的版本。BreadBakedEventArgs实现RuntimeClass<IBreadBakedEventArgs>,持有ComPtr<Bread> m_bread,get_Bread返回面包对象。
属性get_Volume的语义很直观:Oven::get_Volume返回_dims.Height * _dims.Width * _dims.Depth,即烤箱容积——这正是客户端打印 "Oven volume is: 8" 的数据来源。
激活工厂与对象构造
Oven由于声明了自定义工厂接口IOvenFactory,因此实现侧对应定义OvenFactory : ActivationFactory<IOvenFactory>:
ActivateInstance(默认激活)内部调用MakeAndInitialize<Oven>(oven)走无参构造路径;CreateOven(Dimensions dimensions, ...)调用MakeAndInitialize<Oven>(oven, dimensions)走带参构造路径,把Dimensions结构体原样传入RuntimeClassInitialize。
MakeAndInitialize是 WRL 中"先分配对象、再调用RuntimeClassInitialize完成初始化"的组合模板:初始化失败时不会对外暴露半成品对象。类的最后通过ActivatableClassWithFactory(Oven, OvenFactory)宏把Oven类名与OvenFactory绑定,供DllGetActivationFactory查找。
事件源:AgileEventSource 与异步触发
Oven::BakeBread(HSTRING flavor)是理解 WRL 事件的钥匙,其流程为:
- 根据
_temperature计算预热时间:Low→ 100ms(TimeSpan.Duration = 1000000,单位为 100ns)、Medium→ 200ms、High→ 300ms; - 通过
GetActivationFactory(HStringReference(RuntimeClass_Windows_System_Threading_ThreadPoolTimer).Get(), &threadPoolTimer)获取线程池定时器工厂; - 用
MakeAndInitialize<Bread>创建面包、MakeAndInitialize<BreadBakedEventArgs>创建事件参数; - 用
Callback<ITimerElapsedHandler>把 lambda 包装成 COM 回调,回调体内调用thisRef->_breadBaked.InvokeAll(thisRef.Get(), eventArg.Get())触发所有已订阅的处理器; threadPoolTimer->CreateTimer(spTimerCallback.Get(), preheatTime, &timer)启动一次性定时器。
事件通过后台线程回调触发,因此三个语言客户端在事件处理函数中更新 UI 时都绕不开 Dispatcher(C#/C++ 用Dispatcher.RunAsync,JS 在 WinJS 场景下直接写 DOM)。事件注册/注销在 WRL 层面对应add_BreadBaked/remove_BreadBaked,返回与接收EventRegistrationToken。
错误处理:E_INVALIDARG 与 RoOriginateErrorW
ConfigurePreheatTemperature展示了 WRL 风格的错误处理:对OvenTemperature做范围校验(temperature >= Low && temperature <= High),非法输入返回E_INVALIDARG并调用::RoOriginateErrorW(hr, 0, L"Temperature is out of range. ...")登记错误信息。RoOriginateErrorW让错误在投影层以可读消息呈现,客户端捕获后能看到有意义的异常文本,而非裸 HRESULT。
模块入口:module.cpp 与 InProc 模块类型
cpp/Server/module.cpp 只有两个导出函数,却是组件能否被 WinRT 激活的关键:
DllGetActivationFactory:通过Microsoft::WRL::Module<Microsoft::WRL::InProc>::GetModule().GetActivationFactory(...)按类名返回IActivationFactory;DllCanUnloadNow:通过module.Terminate()判断 DLL 是否可卸载。
模板参数InProc正是"进程内"语义的落地:模块以进程内方式承载组件,对象在调用方进程中创建与使用。Server 工程目录下另有 OvenServer.def 等 .def 文件导出这两个入口,以及配套的代理/存根(PS)工程WRLInProcessWinRTComponentPS.vcxproj组成标准 WRL 组件工程骨架。
在 C++/CX 客户端中消费组件
Scenario1_OvenClient.xaml.cpp 采用 C++/CX 投影方式调用,语法最接近"原生语言":
// 通过 ref new 激活,工厂由投影层自动处理 _myOven = ref new Oven(dimensions); // 属性用属性语法访问 OvenClientOutput->Text += "Oven volume is: " + _myOven->Volume.ToString() + "\n"; // 事件用 += 注册、用 EventRegistrationToken 注销 _myOven->BreadBaked += ref new TypedEventHandler<Oven^, BreadBakedEventArgs^>(this, &OvenClient::BreadCompletedHandler1); auto eventRegistrationToken = (_myOven->BreadBaked += ref new TypedEventHandler<Oven^, BreadBakedEventArgs^>(this, &OvenClient::BreadCompletedHandler3)); _myOven->BreadBaked -= eventRegistrationToken; // 方法调用,触发 BreadBaked 事件 _myOven->BakeBread("Sourdough"); _myOven->ConfigurePreheatTemperature(OvenTemperature::High); _myOven->BakeBread("Wheat");注意BreadCompletedHandler1中通过OvenClientOutput->Dispatcher->RunAsync(CoreDispatcherPriority::Normal, ref new DispatchedHandler(...))把事件回调切回 UI 线程,这正是上一节所述"事件在后台线程触发"的应对。代码注释明确说明第 3 个处理器已被注销、不会执行。
用纯 WRL 客户端直接调用 ABI
Scenario2_OvenClientWRL.xaml.cpp 不借助 C++/CX 的组件投影(仅用 C++/CX 做 UI),而是以纯 WRL 方式直接调用 ABI 接口,展示了"投影之下的真实面貌":
// 1. 用类名字符串获取激活工厂 ComPtr<ABI::Microsoft::SDKSamples::Kitchen::IOvenFactory> ovenFactory; hr = GetActivationFactory(HStringReference(RuntimeClass_Microsoft_SDKSamples_Kitchen_Oven).Get(), &ovenFactory); // 2. 调用工厂方法构造对象 hr = ovenFactory->CreateOven(dimensions, &_myOven); // 3. 用 ComPtr::As 查询 IAppliance 接口 ComPtr<ABI::Microsoft::SDKSamples::Kitchen::IAppliance> myAppliance; hr = _myOven.As(&myAppliance); // 4. 属性通过 get_ 方法访问 hr = myAppliance->get_Volume(&volume); // 5. 事件用 Callback 包装 lambda 后用 add_/remove_ 注册注销 auto handler1 = Callback<ABI::Windows::Foundation::ITypedEventHandler<...>>(...); hr = _myOven->add_BreadBaked(handler1.Get(), &token1); hr = _myOven->remove_BreadBaked(token3); // 6. 字符串用 HStringReference 包装 hr = _myOven->BakeBread(HStringReference(L"Sourdough").Get());这段代码把"语言投影"完全剥开:RuntimeClass_Microsoft_SDKSamples_Kitchen_Oven是 IDL 生成的类名常量,事件参数手动用args->get_Bread(&bread)取出、HString flavor承接字符串,每个调用都要检查HRESULT并抛出/捕获Platform::COMException。与 Scenario1 对照,可以清晰看到 C++/CX 编译器替开发者完成了多少机械工作。
在 C# 客户端中消费组件
Scenario1_OvenClient.xaml.cs 展示了 C# 投影的消费方式,结构与 C++/CX 版完全同构:
_myOven = new Oven(dimensions); // 对象构造 OvenClientOutput.Text += "Oven volume is: " + _myOven.Volume; // 属性语法 _myOven.BreadBaked += new TypedEventHandler<Oven, BreadBakedEventArgs>(BreadCompletedHandler1); _myOven.BreadBaked -= new TypedEventHandler<Oven, BreadBakedEventArgs>(BreadCompletedHandler3); // -= 注销 _myOven.BakeBread("Sourdough"); // 触发事件 _myOven.ConfigurePreheatTemperature(OvenTemperature.High); _myOven.BakeBread("Wheat");事件处理器是async方法,内部用await OvenClientOutput.Dispatcher.RunAsync(CoreDispatcherPriority.Normal, new DispatchedHandler(updateOutputText))回到 UI 线程后追加文本。C# 端把EventRegistrationToken隐藏在了+=/-=运算符背后,注销语义与 C++ 端等价。
在 JavaScript 客户端中消费组件
scenario1-oven-client.js 揭示了 WinRT 对 JS 的投影规则——命名空间成为 JS 对象、PascalCase 转 camelCase、事件变成 DOM 风格监听器:
var SampleNamespace = Microsoft.SDKSamples.Kitchen; var myOven = new SampleNamespace.Oven({ width: 2, height: 2, depth: 2 }); // 结构体用字面量对象 printLn("Oven volume is: " + myOven.volume.toString()); // 属性转小写开头 myOven.addEventListener("breadbaked", breadCompleteHandler1); // 事件名全小写 myOven.removeEventListener("breadbaked", breadCompleteHandler3); // 像 DOM 事件一样注销 myOven.bakeBread("Sourdough"); // 方法转 camelCase myOven.configurePreheatTemperature(SampleNamespace.High); // 枚举值投影为命名空间成员在事件处理器中,事件的发送者(Oven)映射为evt.target,事件参数对象的Bread属性映射为evt.bread,evt.bread.flavor即面包口味。同一组件在 JS 端呈现的"轻量感"与 WRL 端的手工HRESULT处理形成鲜明对比。
自定义异常场景
除烤箱主流程外,示例还包含自定义异常演示:C++ 客户端目录下的 Scenario3_CustomException.xaml.cpp 与 Scenario4_CustomExceptionWRL.xaml.cpp、C# 端 Scenario2_CustomException.xaml.cs、JS 端 scenario2-custom-exception.js。从场景命名与分布可以推断,这些场景分别演示"如何从 WRL 组件中抛出自定义异常、并在 C++/CX、纯 WRL、C#、JS 四种投影下捕获与呈现"——与主场景一一对应的异常版对照实验,可结合 OvenServer.cpp 中RoOriginateErrorW的错误登记机制理解其投影路径。
构建与运行示例
按原 README 的说明,构建需 Visual Studio 与 Windows 10 环境(系统要求:客户端 Windows 10、服务器 Windows Server 2016 Technical Preview、手机 Windows 10):
- 若下载的是整个示例集合的 ZIP,务必解压整个压缩包,而不是只解压本示例文件夹——示例依赖
SharedContent等共享资源(见 README front matter 中extendedZipContent的SharedContent与LICENSE配置); - 启动 Visual Studio,选择File>Open>Project/Solution;
- 在解压目录下依次进入 Samples 子目录 → 本示例子目录 → 目标语言子目录(C++、C# 或 JavaScript),双击其中的解决方案(.sln)文件——C++ 客户端对应 cpp/cpp 下的
WRLInProcessWinRTComponent_client_cpp.vcxproj,C# 对应 cpp/cs 下的WRLInProcessWinRTComponent_client_cs.csproj,JS 对应 cpp/js 下的WRLInProcessWinRTComponent_client_js.jsproj; - 按
Ctrl+Shift+B或选择Build>Build Solution完成编译。
运行方式分两种:
- 仅部署:选择Build>Deploy Solution;
- 部署并运行:按
F5或选择Debug>Start Debugging调试运行;按Ctrl+F5或选择Debug>Start Without Debugging直接运行。
关联示例与延伸阅读
仓库中与本示例形成完整对照的相关内容:
- Samples/LinguisticServices:README 明确指出的相关示例,其中包含一个用 C++/WinRT 编写的进程内 Windows Runtime 组件——可作为"同一主题、不同技术栈(WRL vs C++/WinRT)"的对比资料;
- Samples/WRLOutOfProcessWinRTComponent:进程外 WRL 组件示例,与本文的 InProc 模块类型形成对照,帮助理解"进程内/进程外"两种组件承载方式的差异。
若需以 Git 方式获取本示例,可克隆本仓库(gh_mirrors/wi/Windows-universal-samples)后进入Samples/WRLInProcessWinRTComponent目录,或按上文步骤仅用该目录下的解决方案文件构建。
综上,WRLInProcessWinRTComponent 用一个"烤箱烤面包"的趣味领域模型,完整覆盖了 WRL 进程内组件从契约设计(IDL)、实现(RuntimeClass/ActivationFactory/AgileEventSource/Module)到三语种消费(C++/CX、C#、JS)的全部关键环节,是理解 WinRT ABI 与语言投影机制的经典教材级示例。
- 示例工程
【免费下载链接】Windows-universal-samples
API samples for the Universal Windows Platform.
相关推荐
Windows-universal-samples 中的 WRL Out-of-Process WinRT 组件:用 C++/WRL 编写跨进程组件并被 C++、C、JavaScript 消费
Windows universal samples 中的 WRL Out of Process WinRT 组件:用 C++/WRL 编写跨进程组件并被 C++
示例工程在 Rust 中调用 C++/WinRT 编译器:windows-rs 的 cppwinrt crate 完整指南
在 Rust 中调用 C++/WinRT 编译器:windows rs 的 cppwinrt crate 完整指南 本篇技术指南聚焦 windows rs 项目
开发工具Windows Terminal 测试实战:为 C++/WinRT XAML Islands 应用编写 TAEF 单元测试
Windows Terminal 测试实战:为 C++/WinRT XAML Islands 应用编写 TAEF 单元测试 当你用 C++/WinRT 与 XA
桌面应用
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考