Nota:
El acceso a esta página requiere autorización. Puede intentar iniciar sesión o cambiar directorios.
El acceso a esta página requiere autorización. Puede intentar cambiar los directorios.
La plantilla de estructura winrt::implements es la base de la que se derivan sus propias implementaciones de C++/WinRT (de clases en tiempo de ejecución y generadores de activación) directa o indirectamente.
En este tema se describen los puntos de extensión de winrt::implements en C++/WinRT 2.0. Puede optar por implementar estos puntos de extensión en sus tipos de implementación para personalizar el comportamiento predeterminado de los objetos (inspectable en el sentido de la interfaz IInspectable).
Estos puntos de extensión permiten posponer la destrucción de sus tipos de implementación, realizar consultas de forma segura durante la destrucción e interceptar la entrada y la salida de sus métodos proyectados. En este tema se describen esas características y se explica más sobre cuándo y cómo se usarían.
Destrucción diferida
En el tema Diagnóstico de asignaciones directas , hemos mencionado que el tipo de implementación no puede tener un destructor privado.
La ventaja de tener un destructor público es que permite la destrucción diferida, es decir, la capacidad de detectar la llamada final a IUnknown::Release en su objeto y, a continuación, asumir el control de ese objeto para aplazar su destrucción indefinidamente.
Recuerde que los objetos COM clásicos llevan intrínsecamente un recuento de referencias; dicho recuento se gestiona mediante las funciones IUnknown::AddRef e IUnknown::Release. En una implementación tradicional de Release, se invoca un destructor de C++ de un objeto COM clásico una vez que el recuento de referencias alcanza 0.
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
El delete this; llama al destructor del objeto antes de liberar la memoria ocupada por el objeto. Esto funciona bastante bien, siempre que no necesites hacer nada interesante en el destructor.
using namespace winrt::Windows::Foundation;
...
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
~Sample() noexcept
{
// Too late to do anything interesting.
}
};
¿Qué queremos decir por interesante? Por un lado, un destructor es inherentemente síncrono. No se pueden cambiar los subprocesos, quizás para destruir algunos recursos específicos del subproceso en un contexto diferente. No se puede consultar de forma confiable el objeto para alguna otra interfaz que pueda necesitar para liberar determinados recursos. La lista continúa. En los casos en que la destrucción no sea trivial, se necesita una solución más flexible. Donde entra en funcionamiento la función final_release de 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.
}
};
Hemos actualizado la implementación de C++/WinRT de Release para llamar a final_release justo cuando el recuento de referencias de su objeto se reduce a 0. En ese estado, el objeto puede estar seguro de que no hay más referencias pendientes y ahora tiene propiedad exclusiva de sí mismo. Por ese motivo, puede transferir la propiedad de sí misma a la función estática final_release .
En otras palabras, el objeto se ha transformado de uno que admite la propiedad compartida en uno que es propiedad exclusiva. El std::unique_ptr tiene la propiedad exclusiva del objeto y, por tanto, destruirá de forma natural el objeto como parte de su semántica(por lo tanto, la necesidad de un destructor público) cuando std::unique_ptr salga del ámbito (siempre que no se mueva en otro lugar antes de eso). Y esa es la clave. Puede usar el objeto indefinidamente, siempre que std::unique_ptr mantenga activo el objeto. Esta es una ilustración de cómo se puede mover el objeto en otro lugar.
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));
}
};
Este código guarda el objeto en una colección denominada batch_cleanup, una de cuyas funciones será eliminar todos los objetos en algún momento futuro durante el tiempo de ejecución de la aplicación.
Normalmente, el objeto se destruye cuando se destruye el std::unique_ptr, pero puede adelantar su destrucción llamando a std::unique_ptr::reset; o puede posponerla guardando el std::unique_ptr en algún lugar.
Quizás, de forma más práctica y potente, puedes convertir la función final_release en una corrutina y gestionar su destrucción final en un solo lugar, con la posibilidad de suspender la ejecución y cambiar de hilo según sea necesario.
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.
}
};
Un punto de suspensión hará que el hilo llamador —que originalmente inició la llamada a la función IUnknown::Release— retorne y, por tanto, indique al llamador que el objeto al que antes hacía referencia ya no está disponible a través de ese puntero de interfaz. A menudo, los marcos de interfaz de usuario necesitan asegurarse de que los objetos se destruyen en el subproceso de interfaz de usuario específico que creó originalmente el objeto. Esta característica hace que el cumplimiento de este requisito sea trivial, ya que la destrucción se separa de liberar el objeto.
Tenga en cuenta que el objeto pasado a final_release es simplemente un objeto de C++; ya no es un objeto COM. Por ejemplo, las referencias débiles COM existentes al objeto ya no se resuelven.
Consultas seguras durante la destrucción
Partiendo de la noción de destrucción diferida, existe la posibilidad de consultar interfaces de forma segura durante la destrucción.
COM clásico se basa en dos conceptos centrales. La primera es el recuento de referencias y la segunda es la consulta de interfaces. Además de AddRef y Release, la interfaz IUnknown proporciona QueryInterface. Ese método se utiliza ampliamente en determinados marcos de interfaz de usuario, como XAML, para recorrer la jerarquía de XAML mientras simula su sistema de tipos componible. Considere un ejemplo sencillo.
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
Eso puede parecer inofensivo. Esta página XAML quiere borrar su contexto de datos en su destructor. Pero DataContext es una propiedad de la clase base FrameworkElement y reside en la interfaz IFrameworkElement distinta. Como resultado, C++/WinRT debe insertar una llamada a QueryInterface para buscar la tabla virtual correcta antes de poder llamar a la propiedad DataContext . Pero la razón por la que estamos incluso en el destructor es que el recuento de referencias ha pasado a 0. Llamar a QueryInterface aquí aumenta temporalmente ese recuento de referencias; y cuando vuelve a volver a 0, el objeto se destruce de nuevo.
C++/WinRT 2.0 se ha reforzado para soportar esto. Esta es la implementación de Release de C++/WinRT 2.0, de forma simplificada.
uint32_t Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
m_references = 1; // Debouncing!
T::final_release(...);
}
return remaining;
}
Como podría haber previsto, primero disminuye el recuento de referencias y, a continuación, actúa solo si no hay referencias pendientes. Sin embargo, antes de llamar a la función estática final_release que se describió anteriormente en este tema, estabiliza el recuento de referencias estableciendo en 1. Nos referimos a esto como debouncing (tomando prestado un término de la ingeniería eléctrica). Esto es fundamental para evitar que la referencia final se publique. Una vez que esto sucede, el recuento de referencias es inestable y no es capaz de admitir de forma confiable una llamada a QueryInterface.
Llamar a QueryInterface es peligroso después de que se haya liberado la referencia final, ya que el recuento de referencias puede crecer indefinidamente. Es su responsabilidad invocar solo rutas de código conocidas que no prolonguen la vida del objeto. C++/WinRT te lo pone más fácil al garantizar que esas llamadas QueryInterface puedan hacerse de manera fiable.
Para ello, estabiliza el recuento de referencias. Cuando se ha liberado la última referencia, el recuento real de referencias es 0 o algún valor totalmente impredecible. Este último caso puede producirse si hay referencias débiles implicadas. En cualquier caso, esto no es viable si posteriormente se realiza una llamada a QueryInterface, porque eso hará que el recuento de referencias aumente temporalmente; de ahí la referencia a la estabilización. Si se establece en 1, se garantiza que nunca se volverá a producir una llamada final a Release en este objeto. Eso es exactamente lo que queremos, ya que std::unique_ptr ahora es propietario del objeto, pero las llamadas acotadas a pares QueryInterface/Release serán seguras.
Considere un ejemplo más interesante.
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;
}
};
En primer lugar, se llama a la función final_release , notificando a la implementación que es el momento de limpiar. Aquí, final_release resulta ser una corrutina. Para simular un primer punto de suspensión, comienza esperando en el grupo de subprocesos durante unos segundos. A continuación, se reanuda en el subproceso de cola del distribuidor de la página. Este último paso implica una consulta, ya que DispatcherQueue es accesible desde la clase base DependencyObject . Por último, la página se elimina efectivamente al asignar nullptr a std::unique_ptr. A su vez, llama al destructor de la página.
Dentro del destructor, borramos el contexto de datos; que, como sabemos, requiere una consulta para la clase base FrameworkElement .
Todo esto es posible gracias al antirrebote del recuento de referencias (o estabilización del recuento de referencias) proporcionado por C++/WinRT 2.0.
Enlaces de entrada y salida del método
Un punto de extensión menos usado es la estructura abi_guard y las funciones abi_enter y abi_exit .
Si el tipo de implementación define una función abi_enter, esa función se invoca al entrar en cada uno de los métodos proyectados de interfaz (sin contar los métodos de IInspectable).
Del mismo modo, si define abi_exit, se llamará al salir de cada uno de esos métodos; pero no se llamará si abi_enter lanza una excepción. Se seguirá llamando incluso si el propio método de la interfaz proyectada genera una excepción.
Por ejemplo, puede usar abi_enter para generar la excepción hipotética invalid_state_error si un cliente intenta usar un objeto después de que el objeto haya quedado en un estado inutilizable; pongamos por caso, después de una llamada al método ShutDown o Disconnect. Las clases de iterador de C++/WinRT usan esta característica para iniciar una excepción de estado no válida en la función abi_enter si la colección subyacente ha cambiado.
Por encima de las funciones de abi_enter y abi_exitsimples, puede definir un tipo anidado denominado abi_guard. En ese caso, se crea una instancia de abi_guard en cada entrada (no IInspectable) de los métodos de interfaz proyectados, con una referencia al objeto como parámetro de constructor. A continuación, el abi_guard se destruye al salir del método . Puede incluir cualquier estado adicional que desee en su tipo abi_guard.
Si no define su propio abi_guard, existe uno predeterminado que llama a abi_enter durante la construcción y a abi_exit durante la destrucción.
Estas guardias solo se usan cuando se invoca un método a través de la interfaz proyectada. Si invoca métodos directamente en el objeto de implementación, esas llamadas van directamente a la implementación, sin protección.
Este es un ejemplo de código.
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.
}