用于你的实现类型的扩展点Extension points for your implementation types

winrt::implements 结构模板是直接或间接派生你自己的(运行时类和激活工厂的)C++/WinRT 实现的基础。The winrt::implements struct template is the base from which your own C++/WinRT implementations (of runtime classes and activation factories) directly or indirectly derive.

本主题讨论以 C++/WinRT 2.0 编写的 winrt::implements 的扩展点。This topic discusses the extension points of winrt::implements in C++/WinRT 2.0. 可以选择在你的实现类型上实现这些扩展点,以便自定义可检查对象的默认行为(从 IInspectable 接口的意义上说的“可检查” )。You can choose to implement these extension points on your implementation types, in order to customize the default behavior of inspectable objects (inspectable in the sense of the IInspectable interface).

使用这些扩展点可以延迟实现类型的析构、在析构期间安全地查询,以及与投影方法的入口和出口挂钩。These extension points allow you to defer destruction of your implementation types, to safely query during destruction, and to hook the entry into and exit from your projected methods. 本主题介绍了这些功能,并详细说明了应何时以及如何使用这些功能。This topic describes those features and explains more about when and how you would use them.

延迟析构Deferred destruction

Diagnosing direct allocations(诊断直接分配)主题中,我们提到实现类型不能有专用析构函数。In the Diagnosing direct allocations topic, we mentioned that your implementation type can't have a private destructor.

使用公共析构函数的好处是,它支持延迟析构。此功能可以检测在对象上进行的最终 IUnknown::Release 调用,然后通过获取该对象的所有权来无限期延迟其析构。The benefit of having a public destructor is that it enables deferred destruction, which is the ability to detect the final IUnknown::Release call on your object, and then to take ownership of that object to defer its destruction indefinitely.

回忆一下,经典 COM 对象进行内部引用计数;引用计数通过 IUnknown::AddRefIUnknown::Release 函数来管理。Recall that classic COM objects are intrinsically reference counted; the reference count is managed via the IUnknown::AddRef and IUnknown::Release functions. Release 的传统实现中,一旦引用计数为 0,就会调用经典 COM 对象的 C++ 析构函数。In a traditional implementation of Release, a classic COM object's C++ destructor is invoked once the reference count reaches 0.

uint32_t WINRT_CALL Release() noexcept
{
    uint32_t const remaining{ subtract_reference() };
 
    if (remaining == 0)
    {
        delete this;
    }
 
    return remaining;
}

delete this; 在释放对象占用的内存之前调用对象的析构函数。The delete this; calls the object's destructor before freeing the memory occupied by the object. 如果你不需要在析构函数中执行某些微妙的操作,那么这没有任何问题。This works well enough, provided you don't need to do anything interesting in your destructor.

using namespace winrt::Windows::Foundation;
... 
struct Sample : implements<Sample, IStringable>
{
    winrt::hstring ToString() const;
 
    ~Sample() noexcept
    {
        // Too late to do anything interesting.
    }
};

这里的“微妙”是指什么呢? What do we mean by interesting? 首先,析构函数本质上是同步的。For one thing, a destructor is inherently synchronous. 不能通过切换线程来执行某些操作—例如,不能析构另一上下文中某些特定于线程的资源。You can't switch threads—perhaps to destroy some thread-specific resources in a different context. 不能通过可靠方式查询对象来获取某个其他的接口,而该接口可能是释放某些资源所必需的。You can't reliably query the object for some other interface that you might need in order to free certain resources. 此外还有其他例子,这里不一一例举。The list goes on. 如果析构影响重大,则需要更灵活的解决方案。For the cases where your destruction is non-trivial, you need a more flexible solution. 这种情况下,需要使用 C++/WinRT 的 final_release 函数。Which is where C++/WinRT's final_release function comes in.

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_releaseWe've updated the C++/WinRT implementation of Release to call your final_release right when your object's reference count transitions to 0. 在该状态下,对象可以确信不会有更多待处理引用,因此其现在对自己有独占性的所有权。In that state, the object can be confident that there are no further outstanding references, and it now has exclusive ownership of itself. 由于这个原因,它可以将自身的所有权移交给静态 final_release 函数。For that reason, it can transfer ownership of itself to the static final_release function.

换而言之,此对象已将自身从支持共享所有权的对象转换成所有权被独占的对象。In other words, the object has transformed itself from one that supports shared ownership into one that is exclusively owned. std::unique_ptr 独占此对象的所有权,自然会通过其语义来析构此对象,因此需要在 std::unique_ptr 超出作用域时使用公共析构函数(前提是没有在此之前将其移到其他位置)。The std::unique_ptr has exclusive ownership of the object, and so it'll naturally destroy the object as part of its semantics—hence the need for a public destructor—when the std::unique_ptr goes out of scope (provided that it isn't moved elsewhere before that). 这很关键。And that's the key. 可以无限期地使用此对象,前提是 std::unique_ptr 让此对象保持活动状态。You can use the object indefinitely, provided that the std::unique_ptr keeps the object alive. 下面显示了如何将对象移到别处。Here's an illustration of how you might move the object elsewhere.

struct Sample : implements<Sample, IStringable>
{
    winrt::hstring ToString() const;
 
    static void final_release(std::unique_ptr<Sample> ptr) noexcept
    {
        gc.push_back(std::move(ptr));
    }
};

可以将它视为更具确定性的垃圾回收器。Think of this as a more deterministic garbage collector.

通常,该对象在  std::unique_ptr 析构时析构,但你可以通过调用  std::unique_ptr::reset 来加快其析构;或者可以通过将  std::unique_ptr 保存到某处来推迟其析构。Normally, the object destructs when the std::unique_ptr destructs, but you can hasten its destruction by calling std::unique_ptr::reset; or you can postpone it by saving the std::unique_ptr somewhere.

可以将 final_release 函数转换成协同程序,集中处理其最终析构,同时可以根据需要暂停和切换线程,这样也许更实际且功能更强大。Perhaps more practically and more powerfully, you can turn the final_release function into a coroutine, and handle its eventual destruction in one place while being able to suspend and switch threads as needed.

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 函数的调用)返回,向调用方表明它曾经拥有的对象不再通过该接口指针提供。A suspension will point causes the calling thread—which originally initiated the call to the IUnknown::Release function—to return, and thus signal to the caller that the object it once held is no longer available through that interface pointer. UI 框架通常需确保在最初创建对象的特定 UI 线程上将对象析构。UI frameworks often need to ensure that objects are destroyed on the specific UI thread that originally created the object. 此功能使得那样的要求履行起来很简单,因为析构独立于对象的释放。This feature makes fulfilling such a requirement trivial, because destruction is separated from releasing the object.

在析构期间进行安全的查询Safe queries during destruction

基于延迟析构概念之上的是在析构期间安全查询接口的功能。Building on the notion of deferred destruction is the ability to safely query for interfaces during destruction.

经典 COM 基于两个核心概念。Classic COM is based on two central concepts. 第一个是引用计数,第二个是查询接口。The first is reference counting, and the second is querying for interfaces. 除了 AddRefRelease 外,IUnknown 接口还提供 QueryInterfaceIn addition to AddRef and Release, the IUnknown interface provides QueryInterface. 该方法由特定的 UI 框架(例如 XAML)在模拟其可组合类型系统时频繁用来遍历 XAML 层次结构。That method is heavily used by certain UI frameworks—such as XAML, to traverse the XAML hierarchy as it simulates its composable type system. 让我们考虑一个简单示例。Consider a simple example.

struct MainPage : PageT<MainPage>
{
    ~MainPage()
    {
        DataContext(nullptr);
    }
};

该示例看起来似乎无害。 That may appear harmless. 此 XAML 页需清除其析构函数中的数据上下文。This XAML page wants to clear its data context in its destructor. 但是,DataContextFrameworkElement 基类的属性,它生存在独特的 IFrameworkElement 接口上。But DataContext is a property of the FrameworkElement base class, and it lives on the distinct IFrameworkElement interface. 因此,C++/WinRT 必须注入一个针对 QueryInterface 的调用来查找正确的 vtable,然后才能调用 DataContext 属性。As a result, C++/WinRT must inject a call to QueryInterface to look up the correct vtable before being able to call the DataContext property. 但是,之所以我们位于析构器中,是因为引用计数已经为 0。But the reason we're even in the destructor is that the reference count has transitioned to 0. 在这里调用 QueryInterface 会暂时影响该引用计数;当它再次为 0 时,对象会再次析构。Calling QueryInterface here temporarily bumps that reference count; and when it again returns to 0, the object destructs again.

C++/WinRT 2.0 在经过强化后支持此功能。C++/WinRT 2.0 has been hardened to support this. 下面是 C++/WinRT 2.0 对 Release 的实现,采用简化的形式。Here's the C++/WinRT 2.0 implementation of Release, in a simplified form.

uint32_t Release() noexcept
{
    uint32_t const remaining{ subtract_reference() };
 
    if (remaining == 0)
    {
        m_references = 1; // Debouncing!
        T::final_release(...);
    }
 
    return remaining;
}

你可能已经预测到,它首先将引用计数递减,然后,只有在没有待处理引用的情况下才进行操作。As you might have predicted, it first decrements the reference count, and then acts only if there are no outstanding references. 但是,在调用我们此前在本主题中介绍的静态 final_release 函数之前,它会将引用计数设置为 1,使之保持稳定。However, before calling the static final_release function that we described earlier in this topic, it stabilizes the reference count by setting it to 1. 我们称之为“去抖”(借用电气工程的术语)。 We refer to this as debouncing (borrowing a term from electrical engineering). 这对于防止最终引用的释放很重要。This is critical to prevent the final reference from being release. 一旦发生释放,引用计数将变得不稳定,无法可靠地支持对 QueryInterface 的调用。Once that happens, the reference count is unstable, and isn't able to reliably support a call to QueryInterface.

在释放最终引用以后,调用 QueryInterface 是很危险的,因为引用计数随后可能会无限增加。Calling QueryInterface is dangerous after the final reference has been released, because the reference count can then conceivably grow indefinitely. 你有责任确保只调用已知的不会延长对象生存期的代码路径。It's your responsibility to call only known code paths that won't prolong the life of the object. C++/WinRT 可确保这些 QueryInterface 调用的可靠性,因此符合你的要求。C++/WinRT meets you halfway by ensuring that those QueryInterface calls can be made reliably.

其实现这一点的方式是稳定引用计数。It does that by stabilizing the reference count. 释放最终引用以后,实际的引用计数为 0,或者为某个根本无法预测的值。When the final reference has been released, the actual reference count is either 0, or some wildly unpredictable value. 如果涉及弱引用,则可能会发生后面这种情况。The latter case may occur if weak references are involved. 不管什么情况,如果随后调用 QueryInterface,则这会变得不可持续,因为这必然会导致引用计数临时性递增—因此需要让引用去抖。Either way, this is unsustainable if a subsequent call to QueryInterface occurs; because that will necessarily cause the reference count to increment temporarily—hence the reference to debouncing. 将它设置为 1 可确保永远不会在该对象上再次对 Release 进行最终调用。Setting it to 1 ensures that a final call to Release will never again occur on this object. 这正是我们所需要的,因为 std::unique_ptr 现在拥有该对象,但是对 QueryInterface/Release 对进行绑定调用将会是安全的。That's precisely what we want, since the std::unique_ptr now owns the object, but bounded calls to QueryInterface/Release pairs will be safe.

让我们考虑一个更有趣的示例。Consider a more interesting example.

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 函数,通知实现是时候进行清理了。First, the final_release function is called, notifying the implementation that it's time to clean up. 这里的 final_release 刚好是一个协同程序。Here, final_release happens to be a coroutine. 它一开始会在线程池上等待几秒钟,模拟第一个暂停点。To simulate a first suspension point, it begins by waiting on the thread pool for a few seconds. 然后,它会在页面的调度程序线程上继续。It then resumes on the page's dispatcher thread. 这最后一步涉及一个查询,因为 DispatcherDependencyObject 基类的一个属性。That last step involves a query, since Dispatcher is a property of the DependencyObject base class. 最后,通过将 nullptr 分配给 std::unique_ptr,页面被实际删除。Finally, the page is actually deleted by virtue of assigning nullptr to the std::unique_ptr. 接着就会调用页面的析构函数。That in turn calls the page's destructor.

我们在析构函数中清除数据上下文;我们知道,这需要查询 FrameworkElement 基类。Inside the destructor, we clear the data context; which, as we know, requires a query for the FrameworkElement base class.

这一切之所以能够实现,是因为 C++/WinRT 2.0 提供的引用计数去抖功能(也称为引用计数稳定功能)。All of this possible because of the reference count debouncing (or reference count stabilization) provided by C++/WinRT 2.0.

方法入口和出口挂钩Method entry and exit hooks

不太常用的扩展点为  abi_guard 结构以及 abi_enter 和  abi_exit 函数。A less commonly-used extension point is the abi_guard struct, and the abi_enter and abi_exit functions.

如果实现类型定义了一个函数 abi_enter,则将在你的每个投影接口方法的入口调用该函数(不计算  IInspectable 的方法的数量)。If your implementation type defines a function abi_enter, then that function is called at the entry to every one of your projected interface methods (not counting the methods of IInspectable).

同样,如果定义了 abi_exit,则将在每个此类方法的出口调用此函数;但如果 abi_enter 引发异常,则不会调用此函数。Similarly, if you define abi_exit, then that will be called at the exit from every such method; but it will not be called if your abi_enter throws an exception. 如果你的投影接口方法本身引发了异常,则仍将调用此函数 。It will still be called if an exception is thrown by your projected interface method itself.

例如,如果客户端尝试在某个对象已置于不可用状态后(例如,在  Shut­Down 或  Disconnect 方法调用后)使用该对象,你可能会使用 abi_enter 引发有条件的  invalid_state_error 异常。As an example, you might use abi_enter to throw a hypothetical invalid_state_error exception if a client tries to use an object after the object has been put into an unusable state—say after a Shut­Down or Disconnect method call. 如果基础集合已发生更改,则 C++/WinRT 迭代器类会使用此功能在  abi_enter 函数中引发无效状态异常。The C++/WinRT iterator classes use this feature to throw an invalid state exception in the abi_enter function if the underlying collection has changed.

除了简单的 abi_enter 和  abi_exit 函数之外,还可以定义名为  abi_guard 的嵌套类型。Over and above the simple abi_enter and abi_exitfunctions, you can define a nested type named abi_guard. 在这种情况下,将在你的每个(非 IInspectable)投影接口方法的入口创建 abi_guard 的实例,并将对该对象的引用作为其构造函数参数。In that case, an instance of abi_guard is created on entry to each (non-IInspectable) of your projected interface methods, with a reference to the object as its constructor parameter. 然后, abi_guard 将在此方法的出口析构。The abi_guard is then destructed on exit from the method. 可以将所需的任何额外状态置于 abi_guard 类型中。You can put whatever extra state you like into your abi_guard type.

如果未定义自己的 abi_guard,则会有一个默认 abi_guard,它将在构造时调用 abi_enter,并在析构时调用  abi_exitIf you don't define your own abi_guard, then there's a default one that calls abi_enter at construction, and abi_exit at destruction.

仅当通过投影接口调用方法时,才会使用这些防护。These guards are used only when a method is invoked via the projected interface. 如果直接在实现对象上调用方法,则这些调用会直接转到该实现,而不会有任何防护。If you invoke methods directly on the implementation object, then those calls go straight to the implementation, without any guards.

下面是代码示例。Here's a code example.

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.
}