Bagikan melalui


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 ekstensi ini pada jenis implementasi Anda, untuk menyesuaikan perilaku default objek yang dapat diperiksa (dapat diperiksa dalam arti antarmuka IInspectable ).

Titik ekstensi ini memungkinkan Anda menugasi penghancuran jenis implementasi Anda, untuk mengkueri dengan aman selama penghancuran, dan untuk menghubungkan entri ke dalam 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

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

Memanggil delete this; destruktor objek sebelum membebaskan memori yang ditempati oleh 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? Untuk satu hal, destruktor secara inheren sinkron. Anda tidak dapat beralih utas—mungkin 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. Untuk kasus di mana penghancuran Anda tidak sepele, 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 referensi yang luar biasa lebih lanjut, dan sekarang memiliki kepemilikan eksklusif itu sendiri. 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 di beberapa titik waktu mendatang dalam run-time aplikasi.

Biasanya, objek merusak ketika 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 suatu tempat.

Mungkin lebih praktis dan lebih kuat, Anda dapat mengubah fungsi final_release menjadi koroutin, dan menangani penghancuran akhirnya di satu tempat sambil dapat menangguhkan dan beralih 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++; objek tersebut bukan lagi objek COM. Misalnya, referensi LEMAH COM yang ada ke objek tidak lagi diselesaikan.

Kueri aman selama penghancuran

Membangun gagasan penghancuran yang ditangguhkan adalah kemampuan untuk mengkueri antarmuka dengan aman selama penghancuran.

COM klasik didasarkan pada dua konsep pusat. Yang pertama adalah penghitungan referensi, dan yang kedua adalah mengkueri antarmuka. Selain AddRef dan Release, antarmuka IUnknown menyediakan QueryInterface. Metode itu sangat digunakan oleh kerangka kerja UI tertentu—seperti XAML, untuk melintasi hierarki XAML saat mensimulasikan sistem jenis yang dapat disusulkan. 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 bahkan berada di destruktor adalah bahwa jumlah referensi telah beralih ke 0. Memanggil QueryInterface di sini untuk sementara menabrak jumlah referensi; dan ketika kembali kembali ke 0, 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 telah Anda prediksi, pertama-tama mengurangi jumlah referensi, dan kemudian bertindak hanya jika tidak ada referensi yang luar biasa. 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 dilepaskan. 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 akhir telah dirilis, jumlah referensi aktual adalah 0, atau beberapa nilai yang sangat tidak dapat diprediksi. Kasus terakhir dapat terjadi jika referensi yang lemah terlibat. Bagaimanapun, ini tidak dapat dipertahankan jika panggilan berikutnya ke QueryInterface terjadi; karena itu akan selalu menyebabkan jumlah referensi bertambah sementara—oleh karena itu referensi untuk mendebouncing. 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 pasangan Rilis QueryInterface/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->Dispatcher());
        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, itu dimulai dengan menunggu di kumpulan utas selama beberapa detik. Kemudian dilanjutkan pada utas dispatcher halaman. Langkah terakhir itu melibatkan kueri, karena Dispatcher adalah properti kelas dasar DependencyObject. Akhirnya, halaman benar-benar dihapus oleh kebajikan nullptr menetapkan ke std::unique_ptr. Itu pada gilirannya memanggil destruktor halaman.

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

Semua ini dimungkinkan karena jumlah referensi mendeboun (atau stabilisasi jumlah 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 menentukan abi_exit, maka itu akan dipanggil di pintu keluar dari setiap metode tersebut; tetapi tidak akan dipanggil jika abi_enter Anda melemparkan pengecualian. Ini masih akan dipanggil jika pengecualian dilemparkan oleh metode antarmuka yang diproyeksikan itu sendiri.

Sebagai contoh, Anda dapat 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 hal ini, instans abi_guard dibuat pada entri ke masing-masing (non-IInspectable) dari metode antarmuka yang diproyeksikan, dengan referensi ke objek sebagai parameter konstruktornya. abi_guard kemudian dimatikan saat keluar dari metode . Anda dapat menempatkan status tambahan apa pun yang Anda sukai ke dalam jenis 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.
}