Nota
O acesso a esta página requer autorização. Pode tentar iniciar sessão ou alterar os diretórios.
O acesso a esta página requer autorização. Pode tentar alterar os diretórios.
O modelo de estrutura winrt::implements é a base de que derivam, direta ou indiretamente, as suas próprias implementações C++/WinRT (de classes de tempo de execução e fábricas de ativação).
Este tópico discute os pontos de extensão do winrt::implements em C++/WinRT 2.0. Pode escolher implementar estes pontos de extensão nos seus tipos de implementação, para personalizar o comportamento predefinido dos objetos inspecionáveis (inspectable no sentido da interface IInspectable).
Estes pontos de extensão permitem-lhe adiar a destruição dos seus tipos de implementação, consultar em segurança durante a destruição e ligar a entrada e saída dos seus métodos projetados. Este tópico descreve essas funcionalidades e explica melhor quando e como as utilizar.
Destruição adiada
No tópico Diagnosticar alocações diretas, mencionámos que o seu tipo de implementação não pode ter um destruidor privado.
A vantagem de ter um destruidor público é que este permite a destruição diferida, ou seja, a capacidade de detetar a chamada final a IUnknown::Release no seu objeto e, em seguida, tomar posse desse objeto para adiar indefinidamente a sua destruição.
Recorde-se que os objetos clássicos COM são intrinsecamente contados por referência; a contagem de referências é gerida através das funções IUnknown::AddRef e IUnknown::Release . Numa implementação tradicional do Release, o destruidor C++ de um objeto COM clássico é invocado assim que a contagem de referências atinge 0.
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
O delete this; invoca o destrutor do objeto antes de libertar a memória ocupada por este. Isto funciona bastante bem, desde que não seja necessário fazer nada de interessante no destrutor.
using namespace winrt::Windows::Foundation;
...
struct Sample : implements<Sample, IStringable>
{
winrt::hstring ToString() const;
~Sample() noexcept
{
// Too late to do anything interesting.
}
};
O que queremos dizer com interessante? Por um lado, um destruidor é inerentemente síncrono. Não podes mudar de thread — talvez para destruir alguns recursos específicos de threads num contexto diferente. Não podes consultar de forma fiável o objeto para outra interface que possas precisar para libertar certos recursos. A lista continua. Nos casos em que a destruição de um objeto não é trivial, é necessária uma solução mais flexível. É aqui que entra a função final_release do 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.
}
};
Atualizámos a implementação C++/WinRT do Release para chamar o seu final_release exatamente quando a contagem de referências do seu objeto transita para 0. Nesse estado, o objeto pode ter a certeza de que não existem mais referências pendentes, e agora tem propriedade exclusiva sobre si próprio. Por essa razão, pode transferir o controlo de si mesmo para a função estática final_release.
Por outras palavras, o objeto transformou-se de um objeto que apoia a propriedade partilhada para um que é exclusivamente possuído. O std::unique_ptr tem propriedade exclusiva do objeto, e por isso irá naturalmente destruí-lo como parte da sua semântica — daí a necessidade de um destruidor público — quando o std::unique_ptr sair do âmbito (desde que não seja movido para outro local antes disso). E essa é a chave. Podes usar o objeto indefinidamente, desde que o std::unique_ptr mantenha o objeto vivo. Aqui está uma ilustração de como pode mover o objeto para outro local.
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 o objeto numa coleção chamada batch_cleanup cujo trabalho será limpar todos os objetos em algum momento futuro do tempo de execução da aplicação.
Normalmente, o objeto destrói-se quando o std::unique_ptr destrói, mas podes acelerar a sua destruição chamando std::unique_ptr::reset; Ou podes adiar guardando o STD::unique_ptr algures.
Talvez de forma mais prática e poderosa, podes transformar a função final_release numa corrotina e gerir a sua destruição eventual num só lugar, podendo suspender e trocar de threads conforme necessário.
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.
}
};
Um ponto de suspensão faz com que o thread que chamava—que originalmente iniciou a chamada para a função IUnknown::Release —regresse, sinalizando assim ao chamador que o objeto que antes detinha já não está disponível através desse ponteiro de interface. Os frameworks de UI muitas vezes precisam de garantir que os objetos são destruídos no thread específico que originalmente criou o objeto. Esta funcionalidade torna o cumprimento desse requisito trivial, porque a destruição está separada da libertação do objeto.
Note que o objeto passado para final_release é apenas um objeto C++; já não é um objeto COM. Por exemplo, as referências fracas COM existentes ao objeto deixam de poder ser resolvidas.
Consultas seguras durante a destruição
Partindo da noção de destruição diferida, existe a capacidade de consultar interfaces em segurança durante a destruição.
Classic COM baseia-se em dois conceitos centrais. A primeira é a contagem de referências, e a segunda é a consulta de interfaces. Além do AddRef e do Release, a interface IUnknown fornece o QueryInterface. Esse método é amplamente utilizado por certos frameworks de interface — como o XAML — para percorrer a hierarquia XAML enquanto simula o seu sistema de tipos componível. Considere um exemplo simples.
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
Isso pode parecer inofensivo. Esta página XAML quer limpar o seu contexto de dados no seu destruidor. Mas DataContext é uma propriedade da classe base FrameworkElement, e existe na interface separada IFrameworkElement. Como resultado, o C++/WinRT tem de injetar uma chamada ao QueryInterface para procurar o vtable correto antes de poder chamar a propriedade DataContext . Mas a razão pela qual estamos mesmo no destruidor é que a contagem de referências passou para 0. Chamar QueryInterface aqui aumenta temporariamente essa contagem de referências; e quando volta a 0, o objeto destrói-se novamente.
O C++/WinRT 2.0 foi reforçado para suportar isto. Aqui está a implementação C++/WinRT 2.0 do Release, numa 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 poderá ter previsto, primeiro diminui a contagem de referências e só atua se não houver referências pendentes. No entanto, antes de chamar a função de final_release estática que descrevemos anteriormente neste tópico, estabiliza a contagem de referências ao defini-la para 1. Chamamos a isto debouncing (termo emprestado da engenharia eletrotécnica). Isto é fundamental para evitar que a referência final seja divulgada. Quando isso acontece, a contagem de referências torna-se instável e não consegue suportar de forma fiável uma chamada para o QueryInterface.
Chamar o QueryInterface é perigoso após a última referência ter sido libertada, porque a contagem de referências pode, em teoria, crescer indefinidamente. É da tua responsabilidade chamar apenas caminhos de código conhecidos que não prolonguem a vida do objeto. O C++/WinRT vai a meio caminho, assegurando que essas chamadas QueryInterface podem ser feitas de forma fiável.
Faz isso estabilizando a contagem de referências. Quando a referência final é divulgada, a contagem real de referências é ou 0, ou um valor extremamente imprevisível. Este último caso pode ocorrer se estiverem envolvidas referências fracas. De qualquer forma, isto é insustentável se ocorrer uma chamada subsequente ao QueryInterface ; porque isso fará necessariamente com que a contagem de referências aumente temporariamente — daí a referência ao debouncing. Defini-lo para 1 garante que uma chamada final para Libertar nunca mais ocorrerá neste objeto. É exatamente isso que queremos, já que o std::unique_ptr agora é dono do objeto, mas chamadas limitadas para pares QueryInterface/Release serão seguras.
Considere um exemplo mais interessante.
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;
}
};
Primeiro, a função final_release é chamada, notificando a implementação de que é altura de limpar. Aqui, final_release é uma corrotina. Para simular um primeiro ponto de suspensão, começa por esperar alguns segundos na piscina de rosca. Em seguida, retoma na thread da fila do dispatcher da página. Esse último passo envolve uma consulta, uma vez que o DispatcherQueue é acessível a partir da classe base DependencyObject . Finalmente, a página é efetivamente eliminada ao atribuir-se nullptr ao std::unique_ptr. Isso, por sua vez, invoca o destrutor da página.
No destrutor, limpamos o contexto de dados, o que, como sabemos, requer uma consulta à classe FrameworkElement base.
Tudo isto é possível graças à atenuação da contagem de referências (ou estabilização da contagem de referências) proporcionada pelo C++/WinRT 2.0.
Anzóis de entrada e saída do método
Um ponto de extensão menos utilizado é a estrutura abi_guard e as funções abi_enter e abi_exit.
Se o seu tipo de implementação definir uma função abi_enter, então essa função é chamada à entrada de todos os métodos projetados da sua interface (sem contar com os métodos de IInspectable).
De forma semelhante, se definir abi_exit, então este será chamado à saída de cada um desses métodos; mas não será chamado se o seu abi_enter lançar uma exceção. Continuará a ser chamada mesmo que seja lançada uma exceção pelo próprio método projetado da interface.
Por exemplo, pode usar abi_enter para lançar uma exceção hipotética invalid_state_error se um cliente tentar usar um objeto depois de este ter sido colocado num estado inutilizável — por exemplo, após uma chamada ao método ShutDown ou Disconnect. As classes iteradoras C++/WinRT usam esta funcionalidade para lançar uma exceção de estado inválida na função abi_enter se a coleção subjacente tiver sido alterada.
Para além das funções simples abi_enter e abi_exit, pode definir um tipo encapsulado chamado abi_guard. Nesse caso, é criada uma instância de abi_guard à entrada de cada um dos métodos da interface projetada que não sejam IInspectable, com uma referência ao objeto como parâmetro do construtor. O abi_guard é então destruído ao sair do método. Pode colocar qualquer estado adicional que quiser no seu tipo abi_guard.
Se não definires a tua própria abi_guard, existe uma predefinida que chama abi_enter na construção e abi_exit na destruição.
Estas guardas são usadas apenas quando um método é invocado através da interface projetada. Se invocar métodos diretamente no objeto de implementação, essas chamadas vão diretamente para a implementação, sem quaisquer guardas.
Aqui está um exemplo 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.
}
Windows developer