Поделиться через


Точки расширения для типов реализации

Шаблон структуры winrt::реализует шаблон структуры является базой, из которой собственные реализации C++/WinRT (классы среды выполнения и фабрики активации) напрямую или косвенно наследуется.

В этом разделе рассматриваются точки расширения winrt::implements в C++/WinRT 2.0. Вы можете выбрать реализацию этих точек расширения в типах ваших реализаций, чтобы настроить поведение объектов, подлежащих инспекции, по умолчанию (подлежащие инспекции в смысле интерфейса IInspectable).

Эти точки расширения позволяют отложить уничтожение типов реализации, безопасно выполнять запросы в процессе уничтожения, а также внедрять точки входа и выхода предполагаемых методов. В этом разделе описываются эти функции и объясняется больше о том, когда и как их использовать.

Отложенное уничтожение

В теме Диагностика прямых распределений мы упомянули, что ваш тип реализации не может иметь закрытый деструктор.

Преимуществом общедоступного деструктора является то, что он позволяет отложенное уничтожение, которое дает возможность обнаружить окончательный вызов IUnknown::Release на вашем объекте, а затем взять его в собственность, чтобы отложить его уничтожение на неопределенный срок.

Помните, что классические COM-объекты являются внутренне ссылочными; Счетчик ссылок управляется с помощью функций IUnknown::AddRef и IUnknown::Release . В традиционной реализации выпускаклассический деструктор COM-объекта C++ вызывается, когда счётчик ссылок достигает 0.

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

Что мы подразумеваем под интересным? Во-первых, деструктор по сути является синхронным. Вы не можете переключать потоки — например, чтобы уничтожить некоторые ресурсы, связанные с потоком, в другом контексте. Вы не можете надежно запрашивать объект для другого интерфейса, который может потребоваться для освобождения определенных ресурсов. Список продолжается. В тех случаях, когда уничтожение не является тривиальным, вам потребуется более гибкое решение. Именно поэтому функция final_release в C++/WinRT оказывается полезной.

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 для выпуска , чтобы вызвать вашу функцию 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, вернётся и, таким образом, сигнализирует вызывающему потоку о том, что объект, который он когда-то удерживал, больше недоступен через этот указатель интерфейса. Платформы пользовательского интерфейса часто должны гарантировать, что объекты уничтожаются в определенном потоке пользовательского интерфейса, который изначально создал объект. Эта функция делает выполнение такого требования тривиальным, так как уничтожение отделяется от освобождения объекта.

Обратите внимание, что объект, переданный в final_release , является просто объектом C++; он больше не является COM-объектом. Например, существующие слабые ссылки COM на объект больше не разрешаются.

Безопасные запросы при уничтожении объектов

Опираясь на понятие отложенного уничтожения, можно безопасно запрашивать интерфейсы во время уничтожения.

Классический COM основан на двух центральных концепциях. Первое — подсчет ссылок, а второй — запрос на интерфейсы. Помимо AddRef и Releaseинтерфейс IUnknown предоставляет QueryInterface. Этот метод используется в некоторых платформах пользовательского интерфейса, таких как 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 оказывается корутиной. Чтобы имитировать первую точку приостановки, она начинается с ожидания в пуле потоков в течение нескольких секунд. Затем он возобновляется в потоке диспетчера страницы. Этот последний шаг включает запрос, так как диспетчер является свойством базового класса 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. В этом случае экземпляр abi_guard создается при входе для каждого (неIInspectable) из методов вашего проецируемого интерфейса, со ссылкой на объект в качестве параметра конструктора. Затем 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.
}