Observação
O acesso a essa página exige autorização. Você pode tentar entrar ou alterar diretórios.
O acesso a essa página exige autorização. Você pode tentar alterar os diretórios.
O modelo de struct winrt::implements é a base da qual suas próprias implementações C++/WinRT (de classes de tempo de execução e fábricas de ativação) derivam, direta ou indiretamente.
Este tópico discute os pontos de extensão de winrt::implements no C++/WinRT 2.0. Você pode optar por implementar esses pontos de extensão em seus tipos de implementação, a fim de personalizar o comportamento padrão de objetos inspecionáveis ( inspecionáveis no sentido da interface de IInspectable).
Esses pontos de extensão permitem adiar a destruição de seus tipos de implementação, consultar com segurança durante a destruição e interceptar a entrada e saída de seus métodos projetados. Este tópico descreve esses recursos e explica mais sobre quando e como você os usaria.
Destruição adiada
No tópico Diagnosticando alocações diretas, mencionamos que seu tipo de implementação não pode ter um destruidor privado.
O benefício de ter um destruidor público é que ele permite a destruição adiada, que é a capacidade de detectar a chamada final IUnknown::Release no seu objeto e, em seguida, assumir a propriedade desse objeto para adiar sua destruição indefinidamente.
Lembre-se de que os objetos COM clássicos possuem contagem de referência intrínseca; a contagem de referência é gerenciada por meio das funções IUnknown::AddRef e IUnknown::Release. Em uma implementação tradicional de versão, o destruidor C++ de um objeto COM clássico é invocado quando a contagem de referência atinge 0.
uint32_t WINRT_CALL Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
delete this;
}
return remaining;
}
O delete this; chama o destruidor do objeto antes de liberar a memória ocupada pelo objeto. Isso funciona bem o suficiente, desde que você não precise fazer nada interessante no seu 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. Você não pode alternar threads, talvez para destruir alguns recursos específicos do thread em um contexto diferente. Você não pode consultar de forma confiável o objeto para outra interface que talvez seja necessária para liberar determinados recursos. A lista continua. Para os casos em que sua destruição não é trivial, você precisa de uma solução mais flexível. É aí 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.
}
};
Atualizamos a implementação do C++/WinRT de versão para chamar seu final_release imediatamente assim que a contagem de referência do seu objeto passar para 0. Nesse estado, o objeto pode ter certeza de que não há mais referências pendentes e agora tem propriedade exclusiva de si mesmo. Por esse motivo, ele pode transferir a propriedade de si mesmo para a função estática final_release.
Em outras palavras, o objeto se transformou de um que dá suporte à propriedade compartilhada em uma propriedade exclusiva. O std::unique_ptr tem propriedade exclusiva do objeto e, portanto, destruirá naturalmente o objeto como parte de sua semântica, daí a necessidade de um destruidor público, quando o std::unique_ptr sair do escopo (desde que não seja movido para outro lugar antes disso). E essa é a chave. Você pode usar o objeto indefinidamente, desde que o std::unique_ptr mantenha o objeto ativo. Aqui está uma ilustração de como você pode mover o objeto para outro 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));
}
};
Esse código salva o objeto em uma coleção chamada batch_cleanup, que tem a função de limpar todos os objetos em um momento futuro durante a execução do aplicativo.
Normalmente, o objeto é destruído quando o std::unique_ptr se destrói, mas você pode acelerar sua destruição chamando std::unique_ptr::resetou pode adiá-la salvando o std::unique_ptr em algum lugar.
Talvez de forma mais prática e eficiente, você possa transformar a função final_release em uma coroutina e lidar com sua destruição eventual em um só lugar, enquanto pode suspender e alternar 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.
}
};
Uma suspensão indicará à thread de chamada que originalmente iniciou a chamada para a função IUnknown::Release que ela retorne e, assim, indique ao chamador que o objeto que ele antes possuía não está mais disponível por meio desse ponteiro de interface. As estruturas de interface do usuário geralmente precisam garantir que os objetos sejam destruídos na thread de interface específica que criou originalmente o objeto. Esse recurso torna trivial o cumprimento desse requisito, pois a destruição é separada da liberação do objeto.
Observe que o objeto passado para final_release é apenas um objeto C++; ele não é mais um objeto COM. Por exemplo, as referências COM fracas existentes ao objeto não são mais resolvidas.
Consultas seguras durante a desmontagem
A ideia de destruição adiada inclui a capacidade de consultar interfaces de forma segura durante o processo de destruição.
O COM clássico baseia-se em dois conceitos centrais. O primeiro é a contagem de referências e o segundo é a consulta de interfaces. Além de AddRef e Release, a interface IUnknown fornece QueryInterface. Esse método é amplamente usado por certos frameworks de interface de usuário, como o XAML, para percorrer a hierarquia XAML enquanto simula seu sistema de tipos composável. Considere um exemplo simples.
struct MainPage : PageT<MainPage>
{
~MainPage()
{
DataContext(nullptr);
}
};
Isso pode parecer inofensivo. Esta página XAML deseja limpar seu contexto de dados em seu destrutor. Mas DataContext é uma propriedade da classe base FrameworkElement e reside na interface distinta IFrameworkElement. Como resultado, C++/WinRT deve injetar uma chamada para QueryInterface para pesquisar a vtable correta antes de poder chamar a propriedade DataContext. Mas a razão pela qual estamos no destrutor é que o contador de referências foi reduzido a 0. Chamar QueryInterface aqui aumenta temporariamente essa contagem de referência; e quando ele retorna novamente para 0, o objeto é destruído novamente.
O C++/WinRT 2.0 foi protegido para dar suporte a isso. Aqui está a implementação do C++/WinRT 2.0 de Release, 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 você deve ter previsto, ele primeiro diminui a contagem de referências e, em seguida, atua somente 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, ela estabiliza a contagem de referência definindo-a como 1. Referimos-nos a isso como debouncing (emprestando um termo da engenharia elétrica). Isso é fundamental para impedir que a referência final seja liberada. Quando isso acontece, a contagem de referência é instável e não é capaz de dar suporte confiável a uma chamada para QueryInterface.
Chamar QueryInterface é perigoso depois que a última referência foi liberada, pois a contagem de referências pode, então, aumentar indefinidamente. É sua responsabilidade chamar apenas caminhos de código conhecidos que não prolongarão a vida útil do objeto. O C++/WinRT atende a você no meio do caminho, garantindo que essas chamadas queryInterface possam ser feitas de forma confiável.
Ele faz isso estabilizando a contagem de referência. Quando a referência final tiver sido lançada, a contagem de referência real será 0 ou algum valor extremamente imprevisível. O último caso poderá ocorrer se houver referências fracas envolvidas. De qualquer forma, isso será insustentável se ocorrer uma chamada subsequente para QueryInterface, porque isso necessariamente fará com que a contagem de referência seja incrementada temporariamente—daí a referência ao debouncing. Defini-lo como 1 garante que uma chamada final para Release nunca mais ocorra neste objeto. É exatamente o que queremos, já que o std::unique_ptr agora possui o objeto, mas as 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->Dispatcher());
ptr = nullptr;
}
};
Primeiro, a função final_release é chamada, notificando a implementação de que é hora de limpar. Aqui, final_release é uma coroutine. Para simular um primeiro ponto de suspensão, ele começa aguardando no pool de threads por alguns segundos. Em seguida, ele é retomado no thread do dispatcher da página. Essa última etapa envolve uma consulta, já que Dispatcher é uma propriedade da classe base DependencyObject . Por fim, a página é definitivamente excluída ao atribuir nullptr ao std::unique_ptr. Isso, por sua vez, chama o destrutor da página.
Dentro do destruidor, limpamos o contexto de dados; que, como sabemos, requer uma consulta para a classe base FrameworkElement.
Tudo isso é possível devido ao uso de debouncing de contagem de referência (ou estabilização da contagem de referência) fornecido por C++/WinRT 2.0.
Ganchos de entrada e saída do método
Um ponto de extensão menos comumente usado é a struct abi_guard, juntamente com as funções abi_enter e abi_exit.
Se seu tipo de implementação definir uma função abi_enter, então essa função será invocada sempre que um dos seus métodos de interface projetados for acessado (sem contar os métodos de IInspectable).
Da mesma forma, se você definir abi_exit, isso será chamado na saída de todos esses métodos; mas não será chamado se o abi_enter gerar uma exceção. Ele ainda será chamado se uma exceção for gerada pelo próprio método de interface projetado.
Por exemplo, você pode usar abi_enter para lançar uma exceção hipotética de invalid_state_error se um cliente tentar usar um objeto depois de o objeto ter sido colocado em um estado inutilizável, digamos, após uma chamada do método ShutDown ou Disconnect. As classes de iterador C++/WinRT usam esse recurso para gerar uma exceção de estado inválida na função abi_enter se a coleção subjacente tiver sido alterada.
Além das funções abi_enter e abi_exitsimples, você pode definir um tipo aninhado chamado abi_guard. Nesse caso, uma instância de abi_guard é criada na entrada de cada um dos seus métodos de interface projetados que não sejamIInspectable, com uma referência ao objeto como seu parâmetro construtor. O abi_guard é então destruído ao sair do método. Você pode colocar qualquer estado extra que quiser em seu tipo de abi_guard.
Se você não definir seu próprio abi_guard, haverá um padrão que chama abi_enter durante o processo de construção e abi_exit durante o processo de destruição.
Esses guardas são usados somente quando um método é invocado através da interface projetada. Se você invocar métodos diretamente no objeto de implementação, essas chamadas vão diretamente para a implementação, sem 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.
}