Примечание.
Для доступа к этой странице требуется авторизация. Вы можете попробовать войти или изменить каталоги.
Для доступа к этой странице требуется авторизация. Вы можете попробовать изменить каталоги.
Шаблон структуры winrt::implements — это базовый класс, от которого ваши собственные реализации C++/WinRT (классов среды выполнения и фабрик активации) наследуются напрямую или косвенно.
В этом разделе рассматриваются точки расширения winrt::implements в C++/WinRT 2.0. Вы можете при желании реализовать эти точки расширения в ваших типах реализации, чтобы настроить поведение по умолчанию для инспектируемых объектов (inspectable в смысле интерфейса IInspectable).
Эти точки расширения позволяют отложить уничтожение типов реализации, безопасно выполнять запросы в процессе уничтожения, а также подключаться к входу в ваши проецируемые методы и выходу из них. В этом разделе описываются эти функции и объясняется больше о том, когда и как их использовать.
Отложенное уничтожение
В теме Диагностика прямых выделений мы упоминали, что ваш тип реализации не может иметь закрытый деструктор.
Преимущество общедоступного деструктора заключается в том, что он позволяет отложить уничтожение: то есть обнаружить последний вызов 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.
}
};
Мы обновили реализацию Release в C++/WinRT, чтобы вызывать ваш 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 , чтобы найти правильную 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. Мы называем это 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 оказывается сопрограммой. Чтобы имитировать первую точку приостановки, она начинается с ожидания в пуле потоков в течение нескольких секунд. Затем выполнение возобновляется в потоке очереди диспетчера страницы. Последний шаг включает запрос, так как DispatcherQueue доступен из базового класса 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.
}
Windows developer