Agosto de 2015

Número 8 do Volume 30

Windows com C++ - Componentes do Tempo de Execução do Windows com MIDL

Por Kenny Kerr | Agosto de 2015

Kenny KerrNa minha coluna de julho de 2015 (msdn.microsoft.com/magazine/mt238401), apresentei o conceito de componentes do Tempo de Execução do Windows (WinRT) como a evolução do paradigma de programação COM. Enquanto o Win32 coloca COM ao lado, o Tempo de Execução do Windows coloca COM na frente e no centro. O Tempo de Execução do Windows é o sucessor do Win32, o último sendo um termo abrangente para a API do Windows, na medida que ele abrange várias tecnologias e modelos de programação diferentes. O Tempo de Execução do Windows fornece um modelo de programação consistente e unificado, mas para terem êxito, os desenvolvedores dentro e fora da Microsoft precisam de melhores ferramentas para desenvolver componentes do WinRT e usar esses componentes de dentro dos aplicativos.

A principal ferramenta fornecida pelo SDK do Windows para atender a essa necessidade foi o compilador MIDL. Na coluna de julho, mostrei como o compilador MIDL pode produzir o arquivo de Metadados de Tempo de Execução do Windows (WINMD) na maioria das linguagens que exigem projeções para consumir componentes do WinRT. Obviamente, qualquer desenvolvedor na plataforma Windows estará ciente de que o compilador MIDL também produz código que um compilador C ou C++ pode consumir diretamente. Na verdade, o MIDL em si não sabe nada sobre o formato de arquivo WINMD. Trata-se principalmente sobre analisar arquivos IDL e código de produção para compiladores C e C++ para oferecer suporte ao desenvolvimento de COM e chamada de procedimento remoto (RPC) e a produção de DLLs do proxy. O compilador MIDL é uma parte historicamente crítica das máquinas que os engenheiros que desenvolveram o Tempo de Execução do Windows optaram por não correr o risco de dividi-la e em vez disso, desenvolveram um “subcompilador” responsável apenas pelo Tempo de Execução do Windows. Os desenvolvedores não reconhecem normalmente esse artificio, e não precisam, mas ele ajuda a explicar a maneira como o compilador MIDL funciona na prática.

Vamos examinar alguns códigos-fonte do IDL e ver o que realmente está acontecendo com o compilador MIDL. Aqui está um arquivo de origem de IDL que define uma interface COM clássica:

C:\Sample>type Sample.idl
import "unknwn.idl";
[uuid(e21df825-937d-4b0b-862e-e411b57e280e)]
interface IHen : IUnknown
{
  HRESULT Cluck();
}

A COM clássica não tem uma noção forte de namespaces de forma que a interface IHen simplesmente é definida no escopo do arquivo. A definição do IUnknown também deve ser importada antes do uso. Então posso passar logo esse arquivo pelo compilador MIDL para produzir inúmeros artefatos:

C:\Sample>midl Sample.idl
C:\Sample>dir /b
dlldata.c
Sample.h
Sample.idl
Sample_i.c
Sample_p.c

O arquivo de origem dlldata.c contém algumas macros que implementam as exportações necessárias para DLL do proxy. O Sample_i.c contém a GUID da interface IHen, se você estiver usando um compilador de 25 anos que não tem suporte para o uuid _declspec que anexa as GUIDs aos tipos. Em seguida, existe o Sample_p.c, que contém as instruções de empacotamento para a DLL do proxy. Vou ignorar esses por enquanto e me concentrar no Sample.h, que contém algo bastante útil. Se você ignorar todas as macros terríveis que se destinam a ajudar os desenvolvedores de C a usar COM (o horror!), você encontrará isso:

MIDL_INTERFACE("e21df825-937d-4b0b-862e-e411b57e280e")
IHen : public IUnknown
{
public:
  virtual HRESULT STDMETHODCALLTYPE Cluck( void) = 0;
};

Não é C++ elegante, mas, após o pré-processamento, isso equivale a uma classe C++ que herda de IUnknown e adiciona sua própria função essencialmente virtual. Isso é útil porque você não precisa escrever isso manualmente, possivelmente, apresentando uma incompatibilidade entre a definição de C++ da interface e a definição do IDL original que outras ferramentas e linguagens podem consumir. Isso é a essência do que o compilador MIDL oferece para desenvolvedores de C++, produzir uma tradução do código de origem IDL de tal forma que um compilador de C++ possa consumir esses tipos diretamente.

Agora vamos retornar para o Tempo de Execução do Windows. Vou atualizar o código de origem do IDL um pouco para estar de acordo com as exigências mais rigorosas dos tipos do WinRT:

C:\Sample>type Sample.idl
import "inspectable.idl";
namespace Sample
{
  [uuid(e21df825-937d-4b0b-862e-e411b57e280e)]
  [version(1)]
  interface IHen : IInspectable
  {
    HRESULT Cluck();
  }
}

Interfaces do WinRT devem ser herdadas diretamente de IInspectable, e um namespace é usado em parte para associar os tipos com o componente de implementação. Se tentasse compilar isso como antes, encontraria um problema:

.\Sample.idl(3) : error MIDL2025 : syntax error : expecting an interface name or DispatchInterfaceName or CoclassName or ModuleName or LibraryName or ContractName or a type specification near "namespace"

O compilador MIDL não reconhece a palavra-chave do namespace e desiste. É para isso que existe a opção da linha de comando /winrt. Ela informa ao compilador MIDL para passar a linha de comando diretamente para o compilador MIDLRT para pré-processar o arquivo de origem IDL. É esse segundo compilador, MIDLRT, que espera a opção de linha de comando /metadata_dir que mencionei na coluna de julho:

C:\Sample>midl /winrt Sample.idl /metadata_dir
  "C:\Program Files (x86)\Windows Kits ..."

Como uma evidência adicional disso, olhe com mais atenção a saída do compilador MIDL e verá o que quero dizer:

C:\Sample>midl /winrt Sample.idl /metadata_dir "..."
Microsoft (R) 32b/64b MIDLRT Compiler Engine Version 8.00.0168
Copyright (c) Microsoft Corporation. All rights reserved.
MIDLRT Processing .\Sample.idl
.
.
.
Microsoft (R) 32b/64b MIDL Compiler Version 8.00.0603
Copyright (c) Microsoft Corporation. All rights reserved.
Processing C:\Users\Kenny\AppData\Local\Temp\Sample.idl-34587aaa
.
.
.

Removi algumas partes do processamento de dependências para realçar os pontos-chave. Chamar o executável do MIDL com as opções /winrt passa cegamente a linha de comando para o executável do MIDLRT antes de sair. O MIDLRT analisa primeiro o IDL para gerar o arquivo WINMD, mas ele também produz outro arquivo IDL temporário. O arquivo IDL temporário é uma tradução do original com todas as palavras-chave específicas do WinRT, como namespaces, substituídas de forma que o compilador MIDL original as aceitará. Em seguida, o MIDLRT chama o executável do MIDL novamente, mas sem a opção /winrt e com o local do arquivo IDL temporário para que ele possa produzir o conjunto original de cabeçalhos e arquivos de origem do C e C++ como antes.

O namespace no arquivo IDL original é removido e o nome da interface IHen é decorado no arquivo IDL temporário da seguinte maneira:

interface __x_Sample_CIHen : IInspectable
.
.
.

Isso é realmente uma forma codificada do nome do tipo que é interpretado pelo compilador MIDL dada a opção de linha de comando /gen_namespace que o MIDLRT usa ao chamar MIDL com a saída pré-processada. O compilador MIDL original pode processar isso diretamente sem conhecimento específico do Tempo de Execução do Windows. Isso é apenas um exemplo, mas dá uma ideia de como as novas ferramentas fazem o máximo da tecnologia existente para concluir o trabalho. Se você estiver curioso para ver como isso funciona, você pode examinar a pasta temporária que o compilador MIDL rastreia, apenas para descobrir que esses arquivos, como Sample.idl-34587aaa no exemplo anterior, estão faltando. O executável do MIDLRT é cuidadoso ao apagar-se depois, mas se você incluir a opção de linha de comando /savePP, o MIDL não excluirá esses arquivos temporários do pré-processador. Mesmo assim, um pouco mais de pré-processamento é lançado e a Sample.h resultante agora tem algo que até mesmo um compilador C++ reconhecerá como um namespace:

namespace Sample {
  MIDL_INTERFACE("e21df825-937d-4b0b-862e-e411b57e280e")
  IHen : public IInspectable
  {
  public:
    virtual HRESULT STDMETHODCALLTYPE Cluck( void) = 0;
  };
}

Posso implementar essa interface como antes, confiante de que o compilador pegará qualquer discrepância entre a minha implementação e as definições originais codificadas em IDL. Por outro lado, se você só precisa do MIDL para produzir o arquivo WINMD e não precisa de todos os arquivos de origem para um compilador C ou C++, você pode evitar todos os artefatos de compilação adicionais com a opção de linha de comando /nomidl. Essa opção é passada, junto com o resto, pelo executável do MIDL para o executável do MIDLRT. O MIDLRT pula a última etapa da MIDL chamada novamente depois de terminar de criar o arquivo WINMD. Também é comum, quando usar uma ABI do Tempo de Execução do Windows produzida pelo MIDL, incluir a opção de linha de comando /ns_prefix para que os namespaces e tipos resultantes sejam colocados dentro do namespace “ABI” da seguinte maneira:

namespace ABI {
  namespace Sample {
    MIDL_INTERFACE("e21df825-937d-4b0b-862e-e411b57e280e")
    IHen : public IInspectable
    {
    public:
      virtual HRESULT STDMETHODCALLTYPE Cluck( void) = 0;
    };
  }
}

Por fim, devo mencionar que nem o MIDL ou nem o MIDLRT é suficiente para produzir um arquivo WINMD independente que descreva suficientemente os tipos do componente. Se você referenciar tipos externos, geralmente outros tipos definidos pelo sistema operacional, o arquivo WINMD, produzido pelo processo descrito até agora, deve ainda ser mesclado com o arquivo principal de metadados para a versão do Windows que você deseja. Deixe-me ilustrar o problema.

Vou começar com um namespace IDL descrevendo uma interface IHen e uma classe Hen ativável que implementa essa interface, conforme mostrado na Figura 1.

Figura 1 A classe Hen no IDL

namespace Sample
{
  [uuid(e21df825-937d-4b0b-862e-e411b57e280e)]
  [version(1)]
  interface IHen : IInspectable
  {
    HRESULT Cluck();
  }
  [version(1)]
  [activatable(1)]
  runtimeclass Hen
  {
    [default] interface IHen;
  }
}

Em seguida, implementarei usando a mesma técnica que descrevi na coluna de julho, exceto que agora eu posso contar com a definição de IHen conforme fornecida pelo compilador MIDL. Agora, dentro de um aplicativo do WinRT, posso simplesmente criar um objeto Hen e chamar o método Cluck. Usarei C# para ilustrar o lado do aplicativo da equação:

public void SetWindow(CoreWindow window)   
{
  Sample.IHen hen = new Sample.Hen();
  hen.Cluck();
}

O método SetWindow é parte da implementação do IFrameworkView fornecido pelo aplicativo em C#. (Descrevi o IFrameworkView na minha coluna de agosto de 2013, que você pode ler em msdn.microsoft.com/magazine/jj883951.) E, claro, isso funciona. C# depende totalmente dos metadados do WINMD que descrevem o componente. Por outro lado, ele certamente facilita o compartilhamento do código em C++ nativo com clientes em C#. A maioria das vezes, em qualquer caso. Um problema que surge é se você referenciar tipos externos, como fiz alusão há pouco. Vamos atualizar o método Cluck para exigir um CoreWindow como um argumento. Um CoreWindow é definido pelo sistema operacional para que eu simplesmente não possa defini-lo dentro do meu arquivo de origem IDL.

Primeiro, atualizarei o IDL para usar uma dependência na interface do ICoreWindow. Simplesmente vou importar a definição da seguinte maneira:

import "windows.ui.core.idl";

E vou adicionar um parâmetro do ICoreWindow para o método Cluck:

HRESULT Cluck([in] Windows.UI.Core.ICoreWindow * window);

O compilador MIDL tornará essa “importação” em um #include do windows.ui.core.h dentro do cabeçalho gerado para que tudo o que preciso fazer seja atualizar minha implementação de classe Hen:

virtual HRESULT __stdcall Cluck(ABI::Windows::UI::Core::ICoreWindow *) 
  noexcept override
{
  return S_OK;
}

Agora posso compilar o componente como antes e transferi-lo para o desenvolvedor do aplicativo. O desenvolvedor do aplicativo C# obedientemente atualiza a chamada do método Cluck com uma referência para o CoreWindow do aplicativo da seguinte maneira:

public void SetWindow(CoreWindow window)
{
  Sample.IHen hen = new Sample.Hen();
  hen.Cluck(window);
}

Infelizmente, o compilador C# reclama agora:

error CS0012: The type 'ICoreWindow' is defined in an assembly
  that is not referenced.

Você pode ver, o compilador C# não reconhece as interfaces como sendo as mesmas. O compilador c# não está satisfeito com apenas um nome de tipo correspondente e não é possível fazer a conexão com o tipo do Windows de mesmo nome. Ao contrário do C++, o C# é muito dependente das informações do tipo binary para ligar os pontos. Para solucionar esse problema, posso usar outra ferramenta fornecida pelo SDK do Windows que vai compor ou mesclar os metadados do sistema operacional Windows com os metadados do meu componente, solucionando corretamente o ICoreWindow para o arquivo principal de metadados para o sistema operacional. Essa ferramenta é denominada MDMERGE:

c:\Sample>mdmerge /i . /o output /partial /metadata_dir "..."

Os executáveis do MIDLRT e do MDMERGE são bastante específicos sobre seus argumentos de linha de comando. Você precisa obtê-los na ordem correta para que ele funcione. Nesse caso, eu simplesmente não posso atualizar o Sample.winmd in-loco apontando as opções /i (entrada) e /o (saída) para a mesma pasta, porque o MDMERGE realmente excluirá o arquivo de entrada WINMD após a conclusão. A opção /partial informa ao MDMERGE para procurar pela interface do ICoreWindow não solucionada nos metadados fornecidos pela opção /metadata_dir. Isso é chamado de metadados de referência. O MDMERGE, portanto, pode ser usado para mesclar vários arquivos WINMD, mas nesse caso, estou apenas usando-o para solucionar referências para tipos do sistema operacional.

Neste ponto, o Sample.winmd resultante corretamente aponta para os metadados do sistema operacional Windows, quando nos referimos à interface ICoreWindow e o compilador C# está satisfeito e compilará o aplicativo como gravado. Junte-se a mim no próximo mês, enquanto continuo a explorar o Tempo de Execução do Windows em C++.


Kenny Kerr é programador de computador, conhecido como 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