news 2026/9/25 7:25:08

使用 WRL 编写进程内 WinRT 组件并在 C++/JS/C 中调用:Windows-universal-samples 实战剖析

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
使用 WRL 编写进程内 WinRT 组件并在 C++/JS/C 中调用:Windows-universal-samples 实战剖析
  • 示例工程

【免费下载链接】Windows-universal-samples

API samples for the Universal Windows Platform.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-universal-samples
点击查看免费下载

本文围绕 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/ServerWRL 组件本体:IDL 契约、OvenServer.cpp实现、module.cpp模块入口,以及对应的WRLInProcessWinRTComponent_server.vcxproj工程
cpp/cppC++ 客户端,含 4 个场景:C++/CX 消费、纯 WRL 消费、两类自定义异常场景
cpp/csC# 客户端,含烤箱消费与自定义异常两个场景
cpp/jsJavaScript(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 事件的钥匙,其流程为:

  1. 根据_temperature计算预热时间:Low→ 100ms(TimeSpan.Duration = 1000000,单位为 100ns)、Medium→ 200ms、High→ 300ms;
  2. 通过GetActivationFactory(HStringReference(RuntimeClass_Windows_System_Threading_ThreadPoolTimer).Get(), &threadPoolTimer)获取线程池定时器工厂;
  3. 用MakeAndInitialize<Bread>创建面包、MakeAndInitialize<BreadBakedEventArgs>创建事件参数;
  4. 用Callback<ITimerElapsedHandler>把 lambda 包装成 COM 回调,回调体内调用thisRef->_breadBaked.InvokeAll(thisRef.Get(), eventArg.Get())触发所有已订阅的处理器;
  5. 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):

  1. 若下载的是整个示例集合的 ZIP,务必解压整个压缩包,而不是只解压本示例文件夹——示例依赖SharedContent等共享资源(见 README front matter 中extendedZipContent的SharedContent与LICENSE配置);
  2. 启动 Visual Studio,选择File>Open>Project/Solution;
  3. 在解压目录下依次进入 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;
  4. 按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.

项目地址:https://gitcode.com/gh_mirrors/wi/Windows-universal-samples
点击查看免费下载
上一篇:Tab-rs高级技巧:自定义快捷键和自动补全设置
下一篇:技术视角:洛雪音乐助手架构解析与多平台音乐聚合实践

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/9/25 7:23:55

Atlas 300V实战:YOLO模型从环境搭建到推理部署全解析

很多人一听到Atlas&#xff0c;第一反应是“另一款显卡”&#xff0c;第二反应是“华为的AI芯片”。这两种说法都不算错&#xff0c;但都不够准确。刚接触这个生态时我一度也被文档绕晕&#xff0c;直到真的把YOLO模型在一个Atlas 300V加速卡上跑通推理&#xff0c;才把这块板子…

作者头像 李华
网站建设 2026/9/25 7:22:17

低功耗遥测终端机RTU选型指南:从功耗核算到Modbus RTU对接实战

/* MD / 富文本中的 .toc(含博客园搬家等嵌套结构);.toc-box 在侧栏,不受影响 */#content_views .toc,/* 编辑器常在目录前后插入空 p(:empty 仍占 20px),一并去掉避免顶空隙 */#content_views.markdown_views > p:empty:has(+ .toc),#content_views.markdown_views …

作者头像 李华
网站建设 2026/9/25 7:20:11

风电不确定性下的电力系统低碳经济调度优化

1. 项目背景与核心挑战风电作为清洁能源的代表&#xff0c;在电力系统中的占比逐年提升。但风电场出力具有显著的间歇性和波动性特征&#xff0c;这给电力系统的调度运行带来了新的挑战。传统确定性调度方法难以应对这种不确定性&#xff0c;可能导致系统备用容量不足或弃风率过…

作者头像 李华
网站建设 2026/9/25 7:18:47

Atlas 300V部署YOLOv5推理实战:从环境配置到性能调优

1. Atlas平台与项目背景1.1 Atlas 300V 24G到底是干什么的先回答那个被反复问到的问题&#xff1a;Atlas 300V 24G确实是运算加速卡&#xff0c;但准确说它是一张专门为AI推理场景设计的数据中心级加速卡&#xff0c;不是用来跑图形渲染的显卡。这颗卡的核心是华为昇腾AI处理器…

作者头像 李华