winrt::implements 结构模板 是您自己的 C++/WinRT 实现(如运行时类和激活工厂)直接或间接派生而来的基础。
本主题讨论了 C++/WinRT 2.0 中 winrt::implements 的扩展点。 可以选择在实现类型上实现这些扩展点,以便自定义可检查对象的默认行为(可检查(IInspectable 接口)。
通过这些扩展点,可以延迟销毁实现类型、在销毁期间安全地进行查询,以及对预期方法的入口和出口进行挂钩。 本主题介绍这些功能,并详细介绍何时以及如何使用这些功能。
延迟销毁
在 诊断直接分配 主题中,我们提到你的实现类型不能具有专用析构函数。
具有公共析构函数的好处是,它允许延迟销毁,这意味着能够检测到对象上最终的 IUnknown::Release 调用,并随后获取那个对象的所有权,以便无限期地延迟其销毁。
回想一下,经典 COM 对象在内部引用计数;引用计数通过 IUnknown::AddRef 和 IUnknown::Release 函数进行管理。 在 Release的传统实现中,引用计数达到 0 后,将调用经典 COM 对象的C++析构函数。
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
delete this;
在释放对象占用的内存之前调用对象的析构函数。 这非常有效,前提是你不需要在析构函数中执行任何有趣的操作。
using namespace winrt::Windows::Foundation;
...
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
~Sample() noexcept
{
// Too late to do anything interesting.
}
};
有趣的意味着什么? 一方面,析构函数本质上是同步的。 无法切换线程 , 也许要在不同的上下文中销毁某些特定于线程的资源。 无法可靠地查询对象以获取可能需要的一些其他接口,以便释放某些资源。 该列表继续。 对于涉及复杂销毁的情况,您需要更灵活的解决方案。 这是C++/WinRT 的 final_release 函数出现的地方。
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static void final_release(std::unique_ptr<Sample> ptr) noexcept
{
// This is the first stop...
}
~Sample() noexcept
{
// ...And this happens only when *unique_ptr* finally deletes the object.
}
};
我们更新了 Release 的 C++/WinRT 实现,以便在您的对象的引用计数变为 0 时立即调用 final_release。 在该状态下,对象可以确信没有进一步未完成的引用,并且它现在拥有自身的独占所有权。 因此,它可以将自身所有权转让给静态 final_release 函数。
换句话说,这个对象已从支持共同所有权转变为独占所有权。 std::unique_ptr具有对象的独占所有权,因此当std::unique_ptr超出范围时(前提是它在此之前未被移至其他地方),它将自然地销毁对象作为其语义的一部分,因此需要一个公共析构函数。 这就是关键。 可以无限期地使用该对象,前提是 std::unique_ptr使对象保持活动状态。 下面是有关如何将对象移到其他位置的插图。
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static void final_release(std::unique_ptr<Sample> ptr) noexcept
{
batch_cleanup.push_back(std::move(ptr));
}
};
此代码将对象保存在名为 batch_cleanup 其作业之一的集合中,该集合将在应用运行时的某个将来点清理所有对象。
通常,当 std::unique_ptr 被销毁时,对象会被销毁;但你可以通过调用 std::unique_ptr::reset提前销毁它,或者,可以通过将 std::unique_ptr 保存到某个地方来推迟对象的销毁。
也许更切实和更有效地,你可以将 final_release 函数转换为协程,并在一个地方处理它的最终销毁,同时能够根据需要暂停和切换线程。
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
static winrt::fire_and_forget final_release(std::unique_ptr<Sample> ptr) noexcept
{
co_await winrt::resume_background(); // Unwind the calling thread.
// Safely perform complex teardown here.
}
};
挂起操作会使得最初调用 IUnknown::Release 函数的线程返回,从而通知调用方,该接口指针曾经持有的对象现在已经不可用了。 UI 框架通常需要确保对象在最初创建该对象的特定 UI 线程上销毁。 此功能使满足此类要求变得微不足道,因为销毁与释放对象分离。
请注意,传递给 final_release 的对象只是C++对象;它不再是 COM 对象。 例如,对该对象的现有 COM 弱引用不再解析。
销毁期间的安全查询
基于延迟销毁这一概念,还增加了在销毁期间安全查询接口的能力。
经典 COM 基于两个中心概念。 第一个方法是引用计数,第二个方法是接口查询。 除了 AddRef 和 Release外,IUnknown 接口还提供 QueryInterface。 某些 UI 框架(如 XAML)大量使用该方法来遍历 XAML 层次结构,因为它模拟其可组合类型系统。 请考虑一个简单的示例。
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
这可能 看起来 无害。 此 XAML 页面希望在析构函数中清除其数据上下文。 但是,DataContext 是 FrameworkElement 基类的属性,它位于不同的 IFrameworkElement 接口上。 因此,C++/WinRT 必须注入对 QueryInterface 的调用,才能查找正确的 vtable,然后才能调用 DataContext 属性。 但是,我们甚至在析构函数中的原因是引用计数已转换为 0。 在此处调用 QueryInterface 会暂时增加引用计数;当引用计数再次返回到 0 时,对象将再次析构。
C++/WinRT 2.0 已强化以支持此功能。 下面是 Release 的 C++/WinRT 2.0 实现,采用简化形式。
uint32_t Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
m_references = 1; // Debouncing!
T::final_release(...);
}
return remaining;
}
正如你可能预测的那样,它首先递减引用计数,然后仅在没有未完成的引用时才执行。 但是,在调用本主题前面所述的静态 final_release 函数之前,它会通过将引用计数设置为 1 来稳定引用计数。 我们称之为 取消(借用电气工程的术语)。 这对于防止最终引用发布至关重要。 发生这种情况后,引用计数不稳定,并且无法可靠地支持调用 QueryInterface。
在发布最终引用后,调用 QueryInterface 很危险,因为引用计数随后可以无限期增长。 你有责任只调用不会延长对象的生存期的已知代码路径。 C++/WinRT 通过确保可以可靠地进行 QueryInterface 调用来助你一臂之力。
它通过稳定引用计数来执行该操作。 释放最终引用后,实际引用计数要么为 0,要么为某个极其不可预测的值。 如果涉及弱引用,则后一种情况可能会发生。 无论哪种方式,如果后续调用 QueryInterface 发生,这将是不可持续的,因为这必然会导致引用计数暂时递增,类似于消抖过程。 将其设置为 1 可确保对 Release 的最终调用永远不会在此对象上再次发生。 这正是我们想要的,因为 std::unique_ptr 现在拥有对象,但对 QueryInterface/Release 对的绑定调用是安全的。
请考虑一个更有趣的示例。
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
static winrt::fire_and_forget final_release(std::unique_ptr<MainPage> ptr)
{
co_await 5s;
co_await winrt::resume_foreground(ptr->Dispatcher());
ptr = nullptr;
}
};
首先,调用 final_release 函数,通知实现是时候清理了。 在这里,final_release 恰好是协程。 若要模拟第一个挂起的点,开始时先等待线程池几秒钟。 然后,它会在页面的调度程序线程上恢复。 最后一步涉及查询,因为 Dispatcher 是 DependencyObject 基类的属性。 最后,通过将 nullptr
分配给 的 std::unique_ptr,页面实际上被删除。 这反过来又调用页面的析构函数。
在析构函数中,我们清除数据上下文;我们知道,这需要查询 FrameworkElement 基类。
这一切都得益于 C++/WinRT 2.0 提供的引用计数抖动消除(即引用计数稳定)。
方法进入和退出钩子
不太常用的扩展点包括 abi_guard 结构体,以及 abi_enter 和 abi_exit 函数。
如果您的实现类型定义了一个函数 abi_enter,则该函数将在每个投影接口方法的入口处被调用(不包括 IInspectable的方法)。
同样,如果定义 abi_exit,那么在退出每个此类方法时将调用它;但如果 abi_enter 引发异常,则不会调用它。 如果投影接口方法本身引发异常,仍会调用它。
例如,您可能会使用 abi_enter 引发一个假设的 invalid_state_error 异常,当客户端在对象被置于不可用状态后尝试使用该对象时(例如,在调用 ShutDown 或 Disconnect 方法之后)。 如果基础集合已更改,C++/WinRT 迭代器类使用此功能在 abi_enter 函数中引发无效状态异常。
在简单 abi_enter 和 abi_exit函数的上方,可以定义名为 abi_guard的嵌套类型。 在这种情况下,在投影接口方法的每个(非IInspectable)条目上创建 abi_guard 实例,并引用对象作为其构造函数参数。 在方法退出时,abi_guard 会被析构。 您可以将任意需要的额外状态放入 abi_guard 类型。
如果您未定义自己的 abi_guard,则会有一个默认的,在构造时调用 abi_enter,在销毁时调用 abi_exit。
仅当通过投影接口调用方法
下面是一个代码示例。
struct Sample : SampleT<Sample, IClosable>
{
void abi_enter();
void abi_exit();
void Close();
};
void example1()
{
auto sampleObj1{ winrt::make<Sample>() };
sampleObj1.Close(); // Calls abi_enter and abi_exit.
}
void example2()
{
auto sampleObj2{ winrt::make_self<Sample>() };
sampleObj2->Close(); // Doesn't call abi_enter nor abi_exit.
}
// A guard is used only for the duration of the method call.
// If the method is a coroutine, then the guard applies only until
// the IAsyncXxx is returned; not until the coroutine completes.
IAsyncAction CloseAsync()
{
// Guard is active here.
DoWork();
// Guard becomes inactive once DoOtherWorkAsync
// returns an IAsyncAction.
co_await DoOtherWorkAsync();
// Guard is not active here.
}