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.
El plantilla de estructura winrt::implements es la base de la que se derivan directamente o indirectamente sus propias implementaciones de de C++/WinRT (de clases en tiempo de ejecución y generadores de activación).
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 los tipos de implementación para personalizar el comportamiento predeterminado de los objetos inspeccionables ( inspeccionables en el sentido de la interfaz IInspectable).
Estos puntos de extensión permiten aplazar la destrucción de sus tipos de implementación, consultar de forma segura durante la destrucción, y enlazar con la entrada y 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, mencionamos que tu 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, que es la capacidad de detectar la final IUnknown::Release llamar a en el objeto y, a continuación, tomar posesión de ese objeto para aplazar su destrucción indefinidamente.
Recuerde que los objetos COM clásicos son intrínsecamente contados por referencia, el recuento de referencias se administra a través de 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 suficientemente bien, siempre que no necesiten hacer nada interesante en su 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 con interesante? Para empezar, un destructor es intrínsecamente sincrónico. 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. Para los casos en los que su destrucción no es trivial, requiere una solución más flexible. Donde entra en juego la función de 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 tu final_release justo cuando el recuento de referencias del objeto pasa 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, por lo que destruirá automáticamente el objeto como parte de su semántica; de ahí la necesidad de un destructor público, cuando el std::unique_ptr salga del ámbito (si no se ha movido a otro lugar antes de eso). Y esa es la clave. Puede usar el objeto indefinidamente, siempre que el 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, cuyo trabajo será limpiar todos los objetos en algún momento durante el tiempo de ejecución de la aplicación.
Normalmente, el objeto se destruye cuando el std::unique_ptr se destruye, pero puede acelerar su destrucción llamando a std::unique_ptr::reset, o puede posponerlo guardando el std::unique_ptr en algún lugar.
Quizás de manera más práctica y efectiva, puede convertir la función de final_release en una coroutine y gestionar su eventual destrucción en un solo lugar, mientras se pueden suspender y cambiar los hilos 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.
}
};
Una suspensión provocará que el subproceso que hizo la llamada a la función IUnknown::Release regrese, y por lo tanto indique al autor de la llamada que el objeto que antes poseía ya no está disponible a través de dicho 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
A partir de la noción de destrucción diferida se encuentra la capacidad de consultar de forma segura las interfaces 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 usa ampliamente en determinados marcos de interfaz de usuario, como XAML, para atravesar la jerarquía XAML a medida que 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
C++/WinRT 2.0 se ha fortalecido para soportar esto. Esta es la implementación de la función Release de C++/WinRT 2.0, en una 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 eliminación de (préstamo de un término de 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 tu responsabilidad llamar solo a rutas de código conocidas que no alarguen la vida del objeto. C++/WinRT facilita el proceso asegurando que esas llamadas QueryInterface se puedan realizar de manera fiable.
Para ello, estabiliza el recuento de referencias. Cuando se ha publicado la referencia final, el recuento actual de referencias es 0 o un valor sumamente impredecible. Este último caso puede producirse si hay referencias débiles implicadas. En cualquier caso, esto no es sostenible si se produce una llamada posterior a QueryInterface; porque eso hará necesariamente que el recuento de referencias se incremente temporalmente, por lo tanto, la referencia a la desbouncing. Si se establece en 1, se garantiza que una llamada final a Release nunca se volverá a producir en este objeto. Eso es precisamente lo que queremos, ya que el std::unique_ptr ahora posee el objeto, pero las llamadas delimitadas a QueryInterface/pares de 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->Dispatcher());
ptr = nullptr;
}
};
En primer lugar, se llama a la función final_release, notificando a la implementación que es el momento de realizar la limpieza. Aquí, final_release resulta ser una corutina. Para simular un primer punto de suspensión, comienza esperando en el pool de hilos durante unos segundos. A continuación, se reanuda en el hilo del controlador de la página. Este último paso implica una consulta, ya que Dispatcher es una propiedad de la clase base DependencyObject. Por último, la página se elimina realmente mediante la asignación de nullptr
a la std::unique_ptr. A su vez, llama al destructor de la página.
Dentro del destructor, limpiamos el contexto de datos, lo cual, como sabemos, requiere una consulta para la clase base FrameworkElement.
Todo esto es posible debido al anti-rebote (o estabilización del contaje de referencias) proporcionada por C++/WinRT 2.0.
Ganchos 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 su tipo de implementación define una función abi_enter, se llama a esa función a la entrada de cada uno de sus métodos de interfaz proyectados (sin contar los métodos de IInspectable).
Del mismo modo, si define abi_exit, se llamará al salir de cada método de este tipo; pero no se llamará si el abi_enter lanza una excepción. Se seguirá llamando a si tu método de interfaz proyectada lanza una excepción.
Por ejemplo, puede usar abi_enter para producir una excepción hipotética de invalid_state_error si un cliente intenta usar un objeto después de que el objeto se haya puesto en un estado inutilizable, por ejemplo, después de una shutDown o llamada al método 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.
Además de las funciones simples de abi_enter y abi_exit, puede definir un tipo anidado denominado abi_guard. En ese caso, al inicio de cada uno de los métodos de interfaz proyectados (que no seaIInspectable), se crea una instancia de abi_guard, usando una referencia al objeto como parámetro de su constructor. A continuación, el abi_guard es destruido al salir del método. Puedes poner dentro del tipo abi_guard cualquier estado adicional que prefieras.
Si no define su propio abi_guard, hay uno predeterminado que llama a abi_enter en construcción y a abi_exit en destrucción.
Estas protecciones 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 se dirigen directamente a la implementación, sin ninguna guardia.
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.
}