Setembro de 2015

Volume 30 - Número 9

Windows com C++: Tipos de classe no tempo de execução do Windows

Kenny Kerr | Setembro de 2015

Kenny KerrO Tempo de Execução do Windows (WinRT) permite que os desenvolvedores de componentes apresentem um tipo de sistema de classe para desenvolvedores de aplicativos. Considerando que o Tempo de Execução do Windows é implementado inteiramente com interfaces COM, isso pode parecer absurdo para um desenvolvedor familiarizado com C++ e o COM clássico. Afinal de contas, o COM é um modelo de programação centrado em interfaces, e não em classes. O modelo de ativação COM permite que classes sejam construídas com CoCreateInstance e amigos, mas, nesse caso, só existe suporte plausível a algo semelhante a um construtor padrão. O WinRT RoActivateInstance não faz nada para alterar essa percepção. Então, como funciona?

Não é algo tão incongruente quanto possa parecer de início. O modelo de ativação COM já fornece as abstrações fundamentais necessárias para dar suporte a um sistema de tipo de classes. Ele simplesmente não tem os metadados necessários para descrever o processo a um consumidor. O modelo de ativação COM define instâncias e objetos de classe. A instância de determinada classe nunca é criada diretamente por um consumidor. Mesmo que um desenvolvedor de aplicativo chame CoCreateInstance para criar uma instância de classe, o sistema operacional ainda precisa obter o objeto de classe para recuperar a instância desejada.

O Tempo de Execução do Windows chama objetos de classe com um novo nome, mas a mecânica é a mesma. Um alocador de ativação é primeira porta de chamada de um consumidor em um componente. Em vez de implementar DllGetClassObject, um componente exporta uma função chamada DllGetActivationFactory. A maneira como as classes são identificadas foi alterada, mas o resultado é apenas um objeto que pode ser consultado depois em conjunto com a classe determinada. Um objeto de classe tradicionais, daqui em diante chamado de alocador de ativação, pode ter implementado a interface IClassFactory que permite que o chamador crie instâncias desta classe. Neste novo mundo, os alocadores de ativação no Tempo de Execução do Windows devem implementar a nova interface IActivationFactory — fornecendo efetivamente o mesmo serviço.

Como explorei nas duas últimas edições da minha coluna, uma classe de tempo de execução no Tempo de Execução do Windows que esteja decorada com o atributo ativável produz metadados que instruem uma projeção de linguagem para permitir a construção padrão. O pressuposto é que o alocador de ativação para a classe de atributos fornecerá um objeto construído padrão por meio do método ActivateInstance do IActivationFactory. Aqui temos um runtimeclass simples em IDL que representa uma classe com um construtor padrão:

[version(1)]
[activatable(1)]
runtimeclass Hen
{
  [default] interface IHen;
}

Construtores adicionais também podem ser descritos em IDL com – adivinhe! – interfaces que hospedam conjuntos de métodos que representam construtores adicionais com diferentes conjuntos de parâmetros. Assim como a interface IActivationFactory define a construção padrão, as interfaces específicas de classe e componente podem descrever construções parametrizadas. Eis uma interface simples de alocador para criar objetos Galinha com um número específico de cacarejos:

runtimeclass Hen;
[version(1)]
[uuid(4fa3a693-6284-4359-802c-5c05afa6e65d)]
interface IHenFactory : IInspectable
{
  HRESULT CreateHenWithClucks([in] int clucks,
                              [out,retval] Hen ** hen);
}

Como o IActivationFactory, a interface IHenFactory deve herdar diretamente de IInspectable sem motivo aparente. Cada um dos métodos de interface deste alocador são então projetados como um construtor adicional com os parâmetros definidos, e cada um deve terminar com um valor de retorno lógico cujo tipo é o mesmo da classe. Essa interface de alocador é explicitamente associada à classe de tempo de execução por meio de outro atributo ativável que indica o nome da interface:

[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
runtimeclass Hen
{
  [default] interface IHen;
}

Se um construtor padrão não faz sentido para a sua classe, basta omitir o atributo ativável original, e as projeções de idioma de sua classe também não terão um construtor padrão. E se eu enviar uma versão do componente aves e perceber que ele não tem capacidade de criar galinhas com cristas grandes? Com certeza eu não consigo alterar a interface IHenFactory agora que já a enviei aos clientes, porque as interfaces COM são necessariamente contratos binários e semânticos imutáveis. Sem problemas. Eu posso simplesmente definir outra interface que abrigue os construtores adicionais:

[version(2)]
[uuid(9fc40b45-784b-4961-bc6b-0f5802a4a86d)]
interface IHenFactory2 : IInspectable
{
  HRESULT CreateHenWithLargeComb([in] float width,
                                 [in] float height,
                                 [out, retval] Hen ** hen);
}

Então posso associar essa segunda interface de alocador com a classe Galinha como antes:

[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
[activatable(IHenFactory2, 2)]
runtimeclass Hen
{
  [default] interface IHen;
}

As projeções de idioma cuidam do amálgama e o desenvolvedor do aplicativo vê apenas algumas sobrecargas do construtor, conforme mostrado na Figura 1.

Métodos de alocador definidos por IDL em C#
Figure 1 Métodos de alocador definidos por IDL em C#

O desenvolvedor de componentes é cuidadoso ao implementar essas interfaces de alocador, juntamente com o pré-requisito de interface IActivationFactory:

struct HenFactory : Implements<IActivationFactory,
                               ABI::Sample::IHenFactory,
                               ABI::Sample::IHenFactory2>
{
};

Aqui novamente estou contando com o modelo de classe Implements, que descrevi na minha coluna de dezembro de 2014 (msdn.microsoft.com/magazine/dn879357). Mesmo que opte por proibir a construção padrão, o alocador de ativação de galinhas ainda precisaria implementar a interface IActivationFactory porque a função DllGetActivationFactory que os componentes devem implementar está codificada para retornar uma interface IActivationFactory. Isso significa que uma projeção de idioma deve primeiro chamar QueryInterface no ponteiro resultante se o aplicativo precisar de algo diferente da construção padrão. Ainda assim, uma implementação de função virtual do ActivateInstance do IActivationFactory é necessária, mas nenhuma não-operacional como essa será suficiente:

virtual HRESULT __stdcall ActivateInstance(IInspectable ** instance) noexcept override
{
  *instance = nullptr;
  return E_NOTIMPL;
}

Os métodos de construção adicionais podem ser implementados da maneira que fizer mais sentido para a implementação, mas uma solução simples seria simplesmente encaminhar os argumentos para a própria classe interna. Algo parecido com isto:

virtual HRESULT __stdcall CreateHenWithClucks(int clucks,
                                              ABI::Sample::IHen ** hen) noexcept override
{
  *hen = new (std::nothrow) Hen(clucks);
  return *hen ? S_OK : E_OUTOFMEMORY;
}

Isso pressupõe que a classe C++ Galinha não tem um construtor de exceção. Talvez eu precise alocar um vetor de C++ de cacarejos no construtor. Nesse caso, um simples manipulador de exceção será suficiente:

try
{
  *hen = new Hen(clucks);
  return S_OK;
}
catch (std::bad_alloc const &)
{
  *hen = nullptr;
  return E_OUTOFMEMORY;
}

E os membros da classe estática? Você não deve se surpreender ao saber que estes também são implementados com interfaces COM no alocador de ativação. De volta ao IDL, eu posso definir uma interface para abrigar todos os membros estáticos. Que tal algo para relatar o número de camadas de galinhas no quintal:

[version(1)]
[uuid(60086441-fcbb-4c42-b775-88832cb19954)]
interface IHenStatics : IInspectable
{
  [propget] HRESULT Layers([out, retval] int * count);
}

Em seguida, eu preciso associar essa interface com a minha classe de tempo de execução com o atributo estático:

[version(1)]
[activatable(1)]
[activatable(IHenFactory, 1)]
[activatable(IHenFactory2, 2)]
[static(IHenStatics, 1)]
runtimeclass Hen
{
  [default] interface IHen;
}

A implementação em C++ é igualmente simples. Vou atualizar o modelo variadic Implements da seguinte maneira:

struct HenFactory : Implements<IActivationFactory,
                               ABI::Sample::IHenFactory,
                               ABI::Sample::IHenFactory2,
                               ABI::Sample::IHenStatics>
{
};

E fornecer uma implementação adequada da função virtual get_Layers:

virtual HRESULT __stdcall get_Layers(int * count) noexcept override
{
  *count = 123;
  return S_OK;
}

Fica a seu cargo fornecer uma contagem de cabeças mais precisa... Quero dizer, contagem de galinhas.

Do lado do consumidor, dentro do aplicativo, tudo parece muito simples e conciso. Conforme ilustrado na Figura 1, a experiência do IntelliSense é muito útil. Eu posso usar o construtor CreateHenWithClucks como se fosse apenas outro construtor em uma classe C#:

Sample.Hen hen = new Sample.Hen(123); // Clucks

Obviamente, o compilador C# e o Common Language Runtime (CLR) têm uma quantidade razoável de trabalho a fazer para que esta que tudo seja executado. Em tempo de execução, o CLR chama RoGetActivationFactory, que chama LoadLibrary seguido por DllGetActivationFactory para recuperar o alocador de ativação Sample.Hen. Em seguida, o QueryInterface é chamado para recuperar a implementação da interface IHenFactory do alocador e só então ele vai chamar a função virtual CreateHenWithClucks para criar o objeto Galinha. Isso para não mencionar as muitas, muitas chamadas adicionais para QueryInterface que o CLR insiste em fazer entanto produz todos os objetos que entram no CLR para descobrir vários atributos e semânticas desses objetos.

E chamar um membro estático aparece igualmente simples:

int layers = Sample.Hen.Layers;

No entanto, aqui, novamente, o CLR deve chamar RoGetActivationFactory para obter o alocador de ativação, seguido por QueryInterface para recuperar o ponteiro de interface IHenStatics antes que ele possa chamar a função virtual get_Layers para recuperar o valor da propriedade estática. O CLR, no entanto, armazena, em cache os alocadores de ativação para que o custo seja um pouco amortizado em muitas dessas chamadas. Por outro lado, ele também faz diversas tentativas no cache do alocador dentro de um componente bastante inútil e desnecessariamente complexo. Mas esse é um assunto para outro dia. Junte-se a mim no próximo mês enquanto continuamos a explorar o Tempo de Execução do Windows em C++.


Kenny Kerr é programador de computador, autor da Pluralsight e Microsoft MVP, e mora no Canadá. Ele mantém um blog em kennykerr.ca e pode ser seguido no Twitter em twitter.com/kennykerr.


Agradecemos ao seguinte especialista técnico da Microsoft pela revisão deste artigo: Larry Osterman