Pontos de extensão para seus tipos de implementação

O modelo de struct winrt::implements é a base da qual suas próprias implementações C++/WinRT (de classes de runtime 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ável no sentido da interface IInspectable ).

Esses pontos de extensão permitem adiar a destruição dos seus tipos de implementação, fazer consultas com segurança durante a destruição e interceptar a entrada e a saída dos 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 Diagnóstico de 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 a IUnknown::Release no seu objeto e, em seguida, assumir a propriedade desse objeto para adiar sua destruição indefinidamente.

Lembre-se de que objetos COM clássicos têm 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 Release, o destrutor em C++ de um objeto COM clássico é invocado assim que a contagem de referência chega a 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 destrutor 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 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 alguma outra interface que talvez seja necessário 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 de Release no C++/WinRT para chamar o seu final_release exatamente quando a contagem de referências 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 final_release estática.

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, uma de cujas funções será limpar todos os objetos em algum momento futuro da execução do aplicativo.

Normalmente, o objeto é destruído quando o std::unique_ptr é destruído, mas você pode apressar sua destruição chamando std::unique_ptr::reset; ou pode adiá-la armazenando 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 fará com que o thread de chamada , que iniciou originalmente a chamada para a função IUnknown::Release , retorne e, portanto, sinalize ao chamador que o objeto que ele já realizou não está mais disponível por meio desse ponteiro de interface. Frameworks de interface do usuário geralmente precisam garantir que os objetos sejam destruídos na thread específica da interface do usuário na qual foram originalmente criados. 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 fracas COM existentes ao objeto não podem mais ser resolvidas.

Consultas seguras durante a destruição

Com base na noção de destruição adiada, há a capacidade de consultar interfaces com segurança durante a destruição.

O COM clássico baseia-se em dois conceitos centrais. A primeira é a contagem de referência, e a segunda é a consulta de interfaces. Além de AddRef e Release, a interface IUnknown fornece QueryInterface. Esse método é muito utilizado por determinados frameworks de interface do usuário, como o XAML, para percorrer a hierarquia do XAML ao simular 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 deseja limpar seu contexto de dados em seu destruidor. Mas DataContext é uma propriedade da classe base FrameworkElement e reside na interface IFrameworkElement distinta. 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 destruidor é que a contagem de referências passou para 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 de Release no 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 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. Chamamos isso de debouncing (tomando emprestado 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 após o lançamento da referência final, pois a contagem de referências pode aumentar indefinidamente. É sua responsabilidade invocar apenas caminhos de código conhecidos que não prolonguem 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 liberada, a contagem de referências real é 0, ou algum valor totalmente 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 detém o objeto, mas as chamadas delimitadas aos 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 é hora de limpar. Aqui, final_release é uma coroutina. Para simular um primeiro ponto de suspensão, ele começa aguardando no pool de threads por alguns segundos. Em seguida, a execução é retomada na thread da fila do dispatcher da página. Essa última etapa envolve uma consulta, já que DispatcherQueue pode ser acessada a partir da classe base DependencyObject. Por fim, a página é de fato excluída ao atribuir nullptr ao std::unique_ptr. Isso, por sua vez, invoca o método 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 graças ao debouncing da contagem de referência (ou estabilização da contagem de referência) oferecido pelo C++/WinRT 2.0.

Ganchos de entrada e saída do método

Um ponto de extensão menos usado é o struct abi_guard e as funções abi_enter e abi_exit .

Se o tipo de implementação definir uma função abi_enter, essa função será chamada na entrada para cada um dos métodos de interface projetados (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 gerar uma hipotética exceção invalid_state_error se um cliente tentar usar um objeto depois que ele tiver sido colocado em um estado inutilizável — por exemplo, após uma chamada ao método Shut­Down 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 métodos projetados da sua interface que não sejam IInspectable, tendo uma referência ao objeto como parâmetro do construtor. A abi_guard é então destruída na saída do método. Você pode colocar qualquer estado adicional que quiser no seu tipo abi_guard.

Se você não definir seu próprio abi_guard, existe uma implementação padrão que chama abi_enter durante a construção e abi_exit durante a destruição.

Esses guardas são usados somente quando um método é invocado por meio 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.
}