Titik ekstensi untuk jenis implementasi Anda

Templat struct winrt::implements adalah dasar dari mana implementasi C++/WinRT Anda sendiri (dari kelas runtime dan pabrik aktivasi) secara langsung atau tidak langsung berasal.

Topik ini membahas poin ekstensi winrt::implements di C++/WinRT 2.0. Anda dapat memilih untuk menerapkan titik perluasan ini pada tipe implementasi Anda, untuk menyesuaikan perilaku default objek inspectable (dalam pengertian antarmuka IInspectable).

Titik ekstensi ini memungkinkan Anda menunda penghancuran tipe implementasi Anda, melakukan kueri dengan aman selama penghancuran, dan mengaitkan titik masuk ke dan keluar dari metode yang diproyeksikan. Topik ini menjelaskan fitur-fitur tersebut dan menjelaskan lebih lanjut tentang kapan dan bagaimana Anda akan menggunakannya.

Penghancuran yang ditangguhkan

Pada topik Mendiagnosis alokasi langsung, kami menyebutkan bahwa jenis implementasi Anda tidak dapat memiliki destruktor privat.

Manfaat memiliki destruktor publik adalah memungkinkan penghancuran yang ditangguhkan, yang merupakan kemampuan untuk mendeteksi panggilan akhir IUnknown::Release pada objek Anda, dan kemudian untuk mengambil kepemilikan objek tersebut untuk menunda penghancurannya tanpa batas waktu.

Ingat bahwa objek COM klasik dihitung sebagai referensi intrinsik; jumlah referensi dikelola melalui fungsi IUnknown::AddRef dan IUnknown::Release . Dalam implementasi tradisional Rilis, destruktor C++ objek COM klasik dipanggil setelah jumlah referensi mencapai 0.

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

delete this; memanggil destruktor objek sebelum membebaskan memori yang ditempati objek. Ini bekerja cukup baik, asalkan Anda tidak perlu melakukan sesuatu yang menarik di destruktor Anda.

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

Apa yang kita maksud dengan menarik? Pertama, destruktor pada dasarnya bersifat sinkron. Anda tidak dapat berpindah antar-utas—misalnya untuk menghancurkan beberapa sumber daya khusus utas dalam konteks yang berbeda. Anda tidak dapat mengkueri objek dengan andal untuk beberapa antarmuka lain yang mungkin Anda butuhkan untuk membebaskan sumber daya tertentu. Daftarnya terus berlanjut. Jika proses penghancuran tidak sederhana, Anda memerlukan solusi yang lebih fleksibel. Di mana fungsi final_release C++/WinRT masuk.

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

Kami telah memperbarui implementasi C++/WinRT Rilis untuk memanggil final_release Anda tepat saat jumlah referensi objek Anda beralih ke 0. Dalam keadaan itu, objek dapat yakin bahwa tidak ada lagi referensi yang masih tersisa, dan kini objek tersebut memiliki dirinya sendiri secara eksklusif. Untuk alasan itu, ia dapat mentransfer kepemilikan dirinya sendiri ke fungsi final_release statis.

Dengan kata lain, objek telah mengubah dirinya sendiri dari objek yang mendukung kepemilikan bersama menjadi salah satu yang dimiliki secara eksklusif. Std::unique_ptr memiliki kepemilikan eksklusif atas objek, sehingga secara alami akan menghancurkan objek sebagai bagian dari semantiknya —oleh karena itu kebutuhan akan destruktor publik—ketika std::unique_ptr keluar dari cakupan (asalkan tidak dipindahkan ke tempat lain sebelum itu). Dan itu kuncinya. Anda dapat menggunakan objek tanpa batas waktu, asalkan std::unique_ptr menjaga objek tetap hidup. Berikut adalah ilustrasi tentang bagaimana Anda dapat memindahkan objek di tempat lain.

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));
    }
};

Kode ini menyimpan objek dalam koleksi bernama batch_cleanup salah satu pekerjaannya adalah membersihkan semua objek pada beberapa titik waktu mendatang dalam run-time aplikasi.

Biasanya, objek akan dihancurkan saat std::unique_ptr dihancurkan, tetapi Anda dapat mempercepat penghancurannya dengan memanggil std::unique_ptr::reset; atau Anda dapat menundanya dengan menyimpan std::unique_ptr di tempat lain.

Mungkin yang lebih praktis dan lebih canggih, Anda dapat mengubah fungsi final_release menjadi korutin, dan menangani proses penghancurannya pada akhirnya di satu tempat, sembari tetap dapat menangguhkan eksekusi dan berpindah utas sesuai kebutuhan.

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

Penangguhan akan menyebabkan utas panggilan—yang awalnya memulai panggilan ke fungsi IUnknown::Release —untuk kembali, dan dengan demikian memberi sinyal kepada pemanggil bahwa objek yang pernah dipegangnya tidak lagi tersedia melalui penunjuk antarmuka tersebut. Kerangka kerja UI sering kali perlu memastikan bahwa objek dihancurkan pada utas UI tertentu yang awalnya membuat objek. Fitur ini membuat pemenuhan persyaratan seperti itu sepele, karena penghancuran dipisahkan dari melepaskan objek.

Perhatikan bahwa objek yang diteruskan ke final_release hanyalah objek C++; ini bukan lagi objek COM. Misalnya, referensi lemah COM ke objek tersebut tidak lagi dapat merujuk dengan benar.

Kueri aman selama penghancuran

Berdasarkan konsep penghancuran yang ditunda, tersedia kemampuan untuk melakukan kueri terhadap antarmuka dengan aman selama proses penghancuran.

COM klasik didasarkan pada dua konsep pusat. Yang pertama adalah penghitungan referensi, dan yang kedua adalah melakukan kueri untuk antarmuka. Selain AddRef dan Release, antarmuka IUnknown menyediakan QueryInterface. Metode tersebut banyak digunakan oleh kerangka kerja UI tertentu—seperti XAML—untuk menelusuri hierarki XAML saat mensimulasikan sistem jenis yang dapat disusun. Pertimbangkan contoh sederhana.

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

Itu mungkin tampak tidak berbahaya. Halaman XAML ini ingin menghapus konteks datanya dalam destruktornya. Tetapi DataContext adalah properti dari kelas dasar FrameworkElement , dan hidup di antarmuka IFrameworkElement yang berbeda. Akibatnya, C++/WinRT harus menyuntikkan panggilan ke QueryInterface untuk mencari vtable yang benar sebelum dapat memanggil properti DataContext . Tapi alasan kita sampai berada dalam destruktor adalah karena hitungan referensi telah turun menjadi 0. Memanggil QueryInterface di sini untuk sementara menaikkan jumlah referensi tersebut; dan ketika jumlah itu kembali ke 0 lagi, objek akan dihancurkan lagi.

C++/WinRT 2.0 telah diperkuat untuk mendukung hal ini. Berikut adalah implementasi Rilis C++/WinRT 2.0, dalam bentuk yang disederhanakan.

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

Seperti yang mungkin sudah Anda perkirakan, metode ini pertama-tama mengurangi jumlah referensi, lalu bertindak hanya jika tidak ada referensi yang masih tersisa. Namun, sebelum memanggil fungsi final_release statis yang kami jelaskan sebelumnya dalam topik ini, fungsi ini menstabilkan jumlah referensi dengan mengaturnya ke 1. Kami menyebutnya sebagai debouncing (meminjam istilah dari rekayasa listrik). Ini sangat penting untuk mencegah referensi akhir dirilis. Setelah itu terjadi, jumlah referensi tidak stabil, dan tidak dapat mendukung panggilan ke QueryInterface dengan andal.

Memanggil QueryInterface berbahaya setelah referensi akhir dirilis, karena jumlah referensi kemudian dapat tumbuh tanpa batas waktu. Anda bertanggung jawab untuk hanya memanggil jalur kode yang diketahui yang tidak akan memperpanjang masa pakai objek. C++/WinRT menemui Anda di tengah jalan dengan memastikan bahwa panggilan QueryInterface tersebut dapat dilakukan dengan andal.

Hal itu dilakukan dengan menstabilkan jumlah referensi. Ketika referensi terakhir telah dilepas, jumlah referensi sebenarnya adalah 0 atau suatu nilai acak yang sama sekali tidak dapat diprediksi. Kasus terakhir dapat terjadi jika referensi yang lemah terlibat. Bagaimanapun, ini tidak dapat dipertahankan jika kemudian ada pemanggilan ke QueryInterface; karena hal itu pasti akan menyebabkan jumlah referensi meningkat untuk sementara—itulah sebabnya ada rujukan pada debouncing. Mengaturnya ke 1 memastikan bahwa panggilan akhir ke Rilis tidak akan pernah terjadi lagi pada objek ini. Itulah yang kita inginkan, karena std::unique_ptr sekarang memiliki objek, tetapi panggilan terikat ke pasanganRilis/ akan aman.

Pertimbangkan contoh yang lebih menarik.

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

Pertama, fungsi final_release dipanggil, memberi tahu implementasi bahwa saatnya untuk membersihkan. Di sini, final_release kebetulan koroutine. Untuk mensimulasikan titik suspensi pertama, proses ini dimulai dengan menunggu di thread pool selama beberapa detik. Kemudian prosesnya dilanjutkan kembali pada utas antrean dispatcher milik halaman tersebut. Langkah terakhir itu melibatkan kueri, karena DispatcherQueue dapat diakses dari kelas dasar DependencyObject . Akhirnya, halaman benar-benar dihapus dengan menetapkan nullptr ke std::unique_ptr. Hal itu kemudian memanggil destruktor halaman tersebut.

Di dalam destruktor, kami menghapus konteks data; yang, seperti yang kita ketahui, memerlukan kueri untuk kelas dasar FrameworkElement .

Semua ini dimungkinkan karena peredaman hitungan referensi (atau stabilisasi hitungan referensi) yang disediakan oleh C++/WinRT 2.0.

Entri metode dan kait keluar

Titik ekstensi yang kurang umum digunakan adalah struct abi_guard , dan fungsi abi_enter dan abi_exit .

Jika jenis implementasi Anda mendefinisikan fungsi abi_enter, maka fungsi tersebut dipanggil pada entri ke setiap metode antarmuka yang diproyeksikan (tidak menghitung metode IInspectable).

Demikian pula, jika Anda mendefinisikan abi_exit, maka fungsi itu akan dipanggil saat keluar dari setiap metode tersebut; tetapi fungsi itu tidak akan dipanggil jika abi_enter Anda melempar pengecualian. Ini tetap akan dipanggil jika pengecualian terjadi pada metode antarmuka yang Anda proyeksikan itu sendiri.

Sebagai contoh, Anda mungkin menggunakan abi_enter untuk melemparkan pengecualian invalid_state_error hipotetis jika klien mencoba menggunakan objek setelah objek dimasukkan ke dalam status yang tidak dapat digunakan—katakanlah setelah panggilan metode ShutDown atau Putuskan sambungan . Kelas iterator C++/WinRT menggunakan fitur ini untuk melemparkan pengecualian status yang tidak valid dalam fungsi abi_enter jika koleksi yang mendasar telah berubah.

Di atas fungsi abi_enter dan abi_exitsederhana, Anda dapat menentukan jenis berlapis bernama abi_guard. Dalam kasus tersebut, sebuah instans abi_guard dibuat saat memasuki setiap metode antarmuka terproyeksi Anda yang bukan IInspectable, dengan referensi ke objek sebagai parameter konstruktor. abi_guard kemudian dihancurkan saat keluar dari metode. Anda dapat menambahkan keadaan tambahan apa pun yang Anda inginkan ke tipe abi_guard Anda.

Jika Anda tidak menentukan abi_guard Anda sendiri, maka ada yang default yang memanggil abi_enter saat konstruksi, dan abi_exit saat penghancuran.

Penjaga ini hanya digunakan ketika metode dipanggil melalui antarmuka yang diproyeksikan. Jika Anda memanggil metode langsung pada objek implementasi, maka panggilan tersebut langsung menuju implementasi, tanpa penjaga.

Berikut adalah contoh kode.

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