實作型別的擴充點

winrt::implements struct 模板是你自己 C++/WinRT 實作(執行時類別與啟用工廠)直接或間接衍生的基礎。

本主題討論 C++/WinRT 2.0 中 winrt::implements 的擴充點。 你可以選擇在您的實作型別上實作這些擴充點,以自訂可檢查的物件的預設行為(此處的 inspectable 是採用 IInspectable 介面的意義)。

這些擴充點可讓您延後銷毀您的實作類型、在銷毀期間安全地進行查詢,並在您的投影方法進入和退出時掛接處理。 本主題將描述這些功能,並說明何時以及如何使用它們。

延遲毀滅

「診斷直接配置」 主題中,我們提到你的實作類型不能有私有解構器。

擁有公開毀滅者的好處是它能實現延遲毀滅,也就是能偵測到你物件最後的 IUnknown::Release 呼叫,然後取得該物件的所有權,無限期延後其毀滅。

回想經典 COM 物件本質上是參照計數的;參考計數則透過 IUnknown::AddRefIUnknown::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.
    }
};

我們所說的 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.
    }
};

我們已更新 C++/WinRT 的 Release 實作,當你物件的參考計數轉換為 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基於兩個核心概念。 第一是引用計數,第二是查詢介面。 除了 AddRefRelease,IUnknown 介面還提供 QueryInterface 這種方法被某些 UI 框架廣泛使用,例如 XAML,用來在模擬其可組合型別系統時穿越 XAML 階層。 舉個簡單的例子。

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

看起來 可能無害。 這個 XAML 頁面想在它的解構器中清除其資料上下文。 但 DataContextFrameworkElement 基底類別的屬性,且存在於獨立的 IFrameworkElement 介面上。 因此,C++/WinRT 必須先注入 QueryInterface 的呼叫,以查找正確的 vtable,才能呼叫 DataContext 屬性。 但我們之所以還在毀滅器裡,是因為參考計數已經變成 0。 在此呼叫 QueryInterface 會暫時增加該參考數量;當它再次回到 0 時,物體再次被破壞。

C++/WinRT 2.0 已經加強以支援此功能。 以下是 C++/WinRT 2.0 版本的 Release 實作,以簡化版呈現。

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 來穩定參考計數。 我們將其稱為 去彈跳(debouncing)(借用電機工程中的術語)。 這對於防止最終參考資料被釋放至關重要。 一旦發生這種情況,參考數量就不穩定,無法可靠地支援呼叫 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->DispatcherQueue());
        ptr = nullptr;
    }
};

首先,呼叫 final_release 函式,通知實作該清理了。 在這裡, final_release 恰好是一個協程。 為了模擬第一個暫止點,會先在線程池中等待幾秒鐘。 接著會在該頁面的 Dispatcher 佇列執行緒上繼續執行。 最後一步涉及查詢,因為 DispatcherQueue 可從 DependencyObject 基底類別存取。 最後,將 nullptr 指派給 std::unique_ptr 之後,該頁面實際上就會被刪除。 而那又會呼叫頁面的解構函式。

在解散器內部,我們會清除資料上下文;如我們所知,這需要查詢 FrameworkElement 基底類別。

這一切之所以成為可能,都要歸功於 C++/WinRT 2.0 所提供的參考計數去彈跳(或參考計數穩定化)。

方法的進入與退出鉤子

較少使用的擴充點是 abi_guard struct,以及 abi_enterabi_exit 函式。

如果你的實作型別定義了一個函式 abi_enter,那麼該函式會在你所有投影介面方法的入口處被呼叫(不包括 IInspectable 的方法)。

同樣地,如果你定義 abi_exit,則每個此類方法的出口都會呼叫它;但如果你的 abi_enter 拋出例外,則不會被叫回。 即使你的投影介面方法本身拋出例外,仍然被呼叫。

舉例來說,你可以用 abi_enter 拋出假設性的 invalid_state_error 例外,當客戶端在物件處於無法使用的狀態後嘗試使用該物件時——例如在 ShutDownDisconnect 方法呼叫後。 C++/WinRT 迭代器類別利用此功能,若底層集合有變更,則會在 abi_enter 函式中拋出無效狀態異常。

除了簡單的 abi_enterabi_exit函式外,你還可以定義一個名為 abi_guard 的巢狀型別。 在這種情況下,每個(不可檢視的)投影介面方法的入口都會建立一個 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.
}