Nota
O acesso a esta página requer autorização. Podes tentar iniciar sessão ou mudar de diretório.
O acesso a esta página requer autorização. Podes tentar mudar de diretório.
O
Este tópico discute os pontos de extensão do winrt::implements em C++/WinRT 2.0. Você pode optar por implementar esses pontos de extensão nos seus tipos de implementação, a fim de personalizar o comportamento padrão de objetos inspecionáveis (inspecionáveis conforme a interface IInspectable).
Esses pontos de extensão permitem que você adie a destruição de seus tipos de implementação, consulte com segurança durante a destruição e conecte a entrada e a saída dos métodos projetados. Este tópico descreve esses recursos e explica mais sobre quando e como você os usaria.
Destruição diferida
No tópico Diagnóstico de alocações diretas, mencionamos que o tipo da sua implementação não pode ter um destrutor privado.
O benefício de ter um destruidor público é que ele permite a destruição diferida, que é a capacidade de detetar o final IUnknown::Release chamar seu objeto e, em seguida, tomar posse desse objeto para adiar sua destruição indefinidamente.
Lembre-se de que os objetos COM clássicos são intrinsecamente contados como referências; a contagem de referência é gerida através das funções IUnknown::AddRef e IUnknown::Release . Em uma implementação tradicional do Release, 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 não precises fazer nada interessante no teu 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 mudar de threads — talvez para destruir alguns recursos específicos das threads em um contexto diferente. Você não pode consultar de forma confiável o objeto para alguma outra interface que você possa precisar 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.
}
};
Atualizámos a implementação C++/WinRT do Release para invocar o seu final_release precisamente quando a contagem de referências do objeto passa para 0. Nesse estado, o objeto pode estar confiante de que não há mais referências pendentes, e agora tem propriedade exclusiva de si mesmo. Por essa razão, ele pode transferir a propriedade de si mesmo para a função final_release estática.
Em outras palavras, o objeto transformou-se de um que suporta a propriedade compartilhada para um que é de 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 vivo. 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 um de cujos trabalhos será limpar todos os objetos em algum momento futuro no tempo de execução do aplicativo.
Normalmente, o objeto destrói-se quando o std::unique_ptr destrói, mas pode acelerar a sua destruição chamando o std::unique_ptr::reset; ou pode adiá-la salvando o std::unique_ptr em algum lugar.
Talvez de forma mais prática e mais poderosa, pode-se transformar a função final_release em uma corrotina e lidar com a sua eventual destruição num único local, podendo 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 fará com que o thread que fez a chamada — que originalmente iniciou a chamada para a função IUnknown::Release — retorne e, assim, sinalize ao chamador que o objeto que ele possuía antes não está mais disponível através desse ponteiro de interface. As estruturas de interface do usuário geralmente precisam garantir que os objetos sejam destruídos no thread específico da interface do usuário que originalmente criou o objeto. Esta característica torna o cumprimento de tal requisito trivial, porque a destruição é separada da liberação do objeto.
Observe que o objeto passado para final_release é meramente um objeto C++; não é mais um objeto COM. Por exemplo, as referências fracas COM existentes ao objeto não são mais resolvidas.
Consultas seguras durante a destruição
A capacidade de consultar interfaces com segurança durante a destruição baseia-se na noção de destruição diferida.
A OCM clássica baseia-se em dois conceitos centrais. O primeiro é a contagem de referências, e o segundo é a consulta de interfaces. Para além de AddRef e Release, a interface IUnknown fornece QueryInterface. Esse método é muito usado por determinadas estruturas de interface do usuário, como XAML, para percorrer a hierarquia XAML enquanto simula seu sistema de tipos compostáveis. 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 destruidor. Mas
C++/WinRT 2.0 foi reforçada para suportar isso. Aqui está a implementação C++/WinRT 2.0 do Release, em um formato simplificado.
uint32_t Release() noexcept
{
uint32_t const remaining{ subtract_reference() };
if (remaining == 0)
{
m_references = 1; // Debouncing!
T::final_release(...);
}
return remaining;
}
Como você pode ter previsto, ele primeiro diminui a contagem de referência e, em seguida, age apenas se não houver referências pendentes. No entanto, antes de chamar a função final_release estática que descrevemos anteriormente neste tópico, ela estabiliza a contagem de referência definindo-a como 1. Referimo-nos a isto como debounce (tomando emprestado um termo da engenharia elétrica). Isso é fundamental para evitar que a referência final seja liberada. Quando isso acontece, a contagem de referências torna-se instável e não consegue suportar de forma confiável uma chamada para QueryInterface.
Chamar QueryInterface é perigoso depois que a referência final foi libertada, porque a contagem de referências poderá crescer indefinidamente. É da sua responsabilidade invocar apenas caminhos de código conhecidos que não prolonguem a vida útil do objeto. C++/WinRT atende você no meio do caminho, garantindo que essas chamadas QueryInterface possam ser feitas de forma confiável.
Fá-lo estabilizando a contagem de referência. Quando a referência final é lançada, a contagem de referência real é 0 ou algum valor extremamente imprevisível. Este último caso pode ocorrer se estiverem envolvidas referências fracas. De qualquer das formas, isto torna-se insustentável se uma chamada subsequente para QueryInterface ocorrer; porque isso irá provocar um aumento temporário da contagem de referências – daí a referência ao debouncing. Defini-lo como 1 garante que uma chamada final para Release nunca mais ocorrerá neste objeto. É exatamente isso que queremos, já que o std::unique_ptr agora possui o objeto, mas pares de chamadas controladas para QueryInterface/Release terão segurança garantida.
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 passa a ser uma co-rotina. Para simular um primeiro ponto de suspensão, ele começa por aguardar no pool de threads por alguns segundos. Em seguida, o processo é retomado no thread do despachante da página. Essa última etapa envolve uma consulta, já que Dispatcher é uma propriedade do DependencyObject classe base. Finalmente, a página é efetivamente eliminada ao atribuir nullptr ao std::unique_ptr . Isso, por sua vez, chama o destrutor da página.
Dentro do destruidor, limpamos o contexto dos dados; que, como sabemos, requer uma consulta para o FrameworkElement classe base.
Tudo isso é possível devido à estabilização da contagem de referência (ou "debouncing" da contagem de referência) fornecida pelo C++/WinRT 2.0.
Ganchos 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 define uma função abi_enter, então essa função é chamada na entrada para cada um dos seus métodos de interface projetados (sem contar os métodos de IInspectable).
Da mesma forma, se você definir abi_exit, então isso será chamado na saída de cada um desses métodos; mas não será chamado se o seu abi_enter lançar uma exceção. Ele ainda será chamado se uma exceção for lançada pelo próprio método de interface projetado.
Como exemplo, pode-se usar abi_enter para lançar uma exceção hipotética de invalid_state_error se um cliente tentar usar um objeto depois que o objeto tiver sido colocado em um estado inutilizável—digamos, após uma chamada de método ShutDown ou Disconnect. As classes de iterador C++/WinRT usam esse recurso 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 de abi_enter e abi_exit, poder-se-á definir um tipo aninhado denominado abi_guard. Nesse caso, uma instância de abi_guard é criada na entrada para cada um dos seus métodos de interface projetados (não-IInspectable), com uma referência ao objeto como parâmetro do construtor. O abi_guard é então destruído na saída do método. Você pode colocar o estado extra que quiser em seu tipo de abi_guard.
Se você não define seu próprio abi_guard, então há um padrão que chama abi_enter na construção e abi_exit na destruição.
Esses protetores 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 irão diretamente para a implementação, sem nenhum protetor.
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.
}