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

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

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

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

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

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

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

Напомним, что классические объекты модели COM изначально имеют счетчики ссылок, которыми управляют функции IUnknown::AddRef и IUnknown::Release. В традиционной реализации метода Release деструктор на C++ объекта классической модели COM вызывается, когда значение счетчика ссылок достигает 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.
    }
};

Что подразумевается под особыми действиями? Начнем с того, что деструктор изначально является синхронным. Вы не можете переходить по потокам, например, если вам нужно удалить ресурсы определенных потоков в другом контексте. Вы не можете отправлять объекту надежные запросы на какой-нибудь другой интерфейс, который вам может потребоваться для освобождения определенных ресурсов. И это далеко не все. В случаях, когда удаление приводит к серьезным последствиям, вам потребуется более гибкое решение. Именно его и предоставляет функция 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, который теперь вызывает вашу функцию final_release сразу же, когда счетчик ссылок объекта достигает значения 0. В таком состоянии объект не имеет ожидающих обработки ссылок и получает эксклюзивные права владения собой. Это позволяет ему передавать права владения собой статической функции 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, чтобы найти корректную виртуальную таблицу перед вызовом свойства 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 выступает в роли сопрограммы. Для моделирования первой точки приостановки в течение первых нескольких секунд сопрограмма ожидает получение пула потоков. Затем она возобновляет работу в потоке диспетчера страницы. Последний шаг включает выполнение запроса, так как Dispatcher является свойством базового класса 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, если клиент пытается использовать объект после того, как объект был переведен в непригодное состояние, например, после вызова метода Shut­Down или 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.
}