Programação COM em .Net – parte III
Nível: Iniciado/Médio
Conteúdo: Introdução à programação COM em .Net
Ferramentas: Visual Studio 2003/Visual C++ 2005 Express
Linguagem:Managed C++
Apesar de ter prometido no último artigo abordar a construção de componentes .Net para serem utilizados em código unmanaged, não consegui resistir a desviar-me ligeiramente desse objectivo e a apresentar um exemplo da utilização de componentes COM em Managed C++. Continuem a ler e já vão ver porquê...
Porquê o Managed C++?
A questão que se coloca é: porquê utilizar o Managed C++ em vez do C# ou do VB.Net? A resposta é bastante rápida e simples: performance! Apesar de tudo, o Managed C++ continua a ser a linguagem mais eficiente da plataforma .Net. Os amantes das outras linguagens não me devem interpretar mal (afinal de contas eu também gosto muito do C#!)...a verdade é que, se pretendermos utilizar apenas managed code, a utilização de uma linguagem é (praticamente) uma questão de gosto. Contudo, a partir da altura em que pretendemos utilizar Interop (e, sejamos honestos: cerca de 90% das nossas aplicações necessitam de Interop!) e necessitamos de ter o máximo de performance, não nos resta outro caminho senão utilizar o Managed C++ ( e, claro, como o objectivo desta série de artigos é apresentar vários tópicos sobre Interop, então tinha de cá aparecer pelo menos um artigo deste tipo).
Quais as vantagens decorrentes da utilização do Managed C++
A obtenção de maior performance é, como referi no parágrafo anterior, a grande vantagem decorrente da utilização de Managed C++. Para tal contribuem os seguintes factores:
- simplificação da gestão do tempo de vida dos objectos COM;
- diminuição dos chamados context switches;
- maior controlo sobre operações de marshalling;
Vamos lá então analisar cada um destes itens individualmente.
Simplificação da gestão do tempo de vida dos objectos COM
Como o leitor deve estar recordado dos artigos anteriores, existe uma pequena (grande) diferença entre a forma como os componentes COM e os componentes .Net funcionam. Ou seja, o programador não detém o controlo completo sobre o tempo de vida de um objecto .Net enquanto que, no caso dos objectos COM, o scope desses objectos é da responsabilidade do programador.
Devido a esta diferença, existem alguns problemas em aplicações mais complexas que necessitam de maximizar a performance. Como é sabido, a interacção entre um componente .Net e um componente COM é, tradicionalmente, feita através do chamado RCW (Runtime Callable Wrapper). Um dos problema destes componentes resume-se ao facto destes não implementarem o tradicional padrão IDisposable, fazendo assim com que, se quisermos ter controlo total sobre o objecto, tenhamos de:
- Escrever um wrapper à volta desse componente que também implementa o interface IDisposable;
- Evocar o método ReleaseComObject (da classe Marshal) fazendo assim com o “contador interno” seja decrementado.
Ambas as soluções anteriores dependem da evocação do método ReleaseComObject. O único problema dessas soluções reside no facto de, por vezes, ser necessário evocar várias vezes o método até que o objecto seja realmente destruído. O artigo anterior da série apresenta uma explicação para este facto ( e por isso não a vou repetir aqui).
Uma vez que o Managed C++ permite a utilização de ambos os tipos de objectos (managed e unmanaged), então podemos recorrer aos smart pointers para encapsular os objectos COM no interior de uma função. Por exemplo, se nos concentrarmos no objecto COM utilizado na sample que acompanha este artigo (e que reaproveita o objecto Car do artigo anterior), poderíamos, dentro de um método (managed ou unmanaged) utilizar o seguinte código para criar e utilizar uma instância de um carro:
void Init()
{
TestProjectLib::ICarDualPtr aux( __uuidof( TestProjectLib::CarDual ) );
if( aux != NULL )
{
aux->TopSpeed = Convert::ToInt32( _topSpeed->Text );
//necessario efectuar o marshalling
_bstr_t brand( static_cast( Marshal::StringToBSTR( _brand->Text ).ToPointer() ) );
_car->Brand = brand;
}
//contador do objecto decrementado e automaticamente destruido pois nao existe
//nenhuma referencia activa para alem do smart pointer
}
Mesmo para aqueles que não gostam de C++, esta parece-me uma boa forma de ter controlo total sobre o tempo de vida do objecto sem ter de andar a espalhar os tais métodos ReleaseComObject pelo código. E reparem que este código funciona em métodos managed e unmanaged. Repare-se também que, neste caso, para inicializarmos a propriedade Brand temos de converter o valor de System::String para _bstr_t uma vez que o tipo System::String não é um tipo blittable. Mais informações sobre este tipo de conversões no tópico “Maior controlo sobre operações de marshalling”.
Diminuição dos chamados Context Switches
Para apreciarmos a utilidade do Managed C++ nesta área convém ter uma ideia sobre o tipo de acções que são efectuadas quando uma método managed evoca um método unmanaged. Ao que consta, o compilador necessita de executar um conjunto de acções por forma a efectuar a transição entre um contexto managed e um contexto unmanaged. Para tal introduz um thunk no código. Por outras palavras, quando a partir do nosso código managed acedemos a um RCW, o compilador tem de inserir código que permite a comunicação entres estes dois tipos de contexto. O problema aqui resume-se ao facto desta transição poder introduzir um custo um pouco elevado a nível de performance (especialmente se essas transições forem constantes).
Devido a isso podemos criar uma classe unmanaged que contém vários wrappers que minimizam esse tipo de transições. Uma vez que iríamos possuir um método unmanaged que funciona como uma facade em torno de uma determinada operação que necessitava da evocação de vários métodos unmanaged, estaríamos apenas a incorrer numa transição de contexto, aumentando assim a performance da aplicação. Infelizmente a sample que acompanha este artigo não permite demonstrar este tipo de operações. Contudo presumo que a explicação anterior foi suficiente para transmitir os ganhos principais obtidos através da utilização destas técnicas.
Maior controlo sobre eventuais operações de marshalling
Bem, cá está uma área em que irá (de certeza) haver opiniões divergentes! Mantendo a tradição do C++, o Managed C++ continua a ser a linguagem mais flexível da framework. Ora bem, como sempre aconteceu, esta grande flexibilidade traduz-se também numa maior responsabilidade por parte do programador. Devido a isto, todos os tipos que não se inserem nos chamados tipos blittable têm de ser transformados explicitamente de forma a poderem transitar de um contexto para outro (ou seja, o processo de marshalling tem de ser explicitamente definido pelo programador).
Felizmente para nós, a MSDN inclui uma secção muito vasta com vários exemplos que demonstram como podemos efectuar o marshalling de vários tipos não blittable. Já agora, se ainda existem dúvidas em relação aos tipos blittable e não blittable, então sugere-se a consulta desta página na MSDN.
O seguinte excerto é retirado do sample que acompanha este artigo:
_bstr_t aux( static_cast( Marshal::StringToBSTR( _brand->Text ).ToPointer() ) );
_car->Brand = aux;
Repare-se ainda que a utilização do tipo _bstr_t encapsula o tipo BSTR, libertando assim o programador da responsabilidade de libertar a memória associada à variável (recorde-se que a utilização dos tipos BSTR obriga a que o utilizador recorra a API para reservar e libertar memória para ser utilizada nos BSTRs).
Pormenores interessantes relativos ao sample
O sample que acompanha este artigo reutiliza o objecto COM apresentado no artigo anterior. Como é possível verificar pelo código, a utilização de código .Net é muito simples a partir do Managed C++. A interacção com o formulário é muito semelhante ao código C# do artigo anterior.
Como referi anteriormente, este projecto não explora metade das potencialidades do C# devido à simplicidade do objecto COM utilizado. Contudo, esta linguagem é ideal em projectos complexos em que é necessário interagir com vários objectos de forma eficiente.
Apesar da simplicidade, este projecto permite ter em atenção alguns pormenores importantes relativos ao Interop. Como o leitor pode constatar, a classe Form1 contém um apontador ( _car )que é utilizado para aceder ao objecto COM. Este apontador é especial! Tradicionalmente os apontadores estão sempre na mesma posição e apontam para uma determinada posição de memória que não muda dinamicamente. Como é do conhecimento geral, não é possível utilizar este tipo de apontadores em managed code devido ao facto do GC poder redefinir as posições de memória em que se encontram as instâncias de determinadas classes de forma a aumentar a performance. Portanto, neste caso o elemento é armazenado através de um apontador designado de interior pointer (representado através do tipo interior_ptr<>).
Então coloca-se aqui um problema: como vamos inicializar este apontador? Para os mais esquecidos, a inicialização de um apontador para um interface COM é feita através do método CoCreateInstance:
TestProjectLib::ICarDual* pcar = NULL;
HRESULT hr = ::CoCreateInstance( TestProjectLib::CLSID_CarDual,
NULL,
CLSCTX_INPROC_SERVER,
TestProjectLib::IID_ICarDual,
(void**)&pcar );
if (SUCCEEDED(hr))
{
//apontador inicializado com sucesso
//ja nao e necessario, entao libertar
pcar->Release();
}
Só uma nota relativa ao processo de inicialização do interface: podemos utilizar a directiva __uuidof e os smart pointers para criar a instância de forma mais simples (aliás, foi este o processo seguido no código que acompanha o artigo). Como o leitor deve estar a pensar, não é possível passar o nosso apontador (do tipo interior_ptr<> ) como argumento para o método CoCreateInstance pois o GC pode modificar a posição desse apontador. A solução: efectuar o chamado pinning garantindo assim que o apontador interno da classe não é mudado de posição durante a evocação do método.
Se não quisermos utilizar o pinning, então podemos utilizar a mesma estratégia apresentada no código que acompanha artigo:
void Init()
{
TestProjectLib::ICarDualPtr aux( __uuidof( TestProjectLib::CarDual ) );
if( aux != NULL )
{
_car = aux.Detach();
_car->TopSpeed = Convert::ToInt32( _topSpeed->Text );
//necessario efectuar o marshalling
_bstr_t aux( static_cast( Marshal::StringToBSTR( _brand->Text ).ToPointer()));
_car->Brand = aux;
}
}
Neste caso recorri aos smart pointers e à directiva __uuidof para criar o objecto COM de forma rápida e em seguida armazenamos o apontador na variável _car. Repare-se que esta atribuição é feita ao mesmo tempo em que libertamos o smart pointer do encapsulamento do apontador para o objecto COM através do método Detach(). Se não evocássemos este método, o smart pointer iria decrementar o contador interno no fim do bloco onde foi criado e acabaria por destruir o objecto COM (isto porque quando efectuámos a atribuição não evocámos o método AddRef).
Agora a nossa classe tem de ser responsável pela libertação do objecto apontado por _car. É essa a função do método ReleaseComPtr:
void ReleaseComPtr()
{
if( _car != nullptr )
{
_car->Release();
_car = nullptr;
}
}
Refira-se que este método é evocado a partir do event handler associado ao botão Destruir e no método Dispose do formulário. Para terminar, é importante ter alguma atenção quando evocamos métodos/propriedades do nosso apontador. Isto deve-se ao facto de ser necessário respeitar as necessidades do objecto a nível de apartments (é preciso não esquecer tudo o que foi referido relativamente às classes managed e à sua falta de afinidade à threads).
Conclusões finais
Apesar de tudo o C++ (na sua vertente Managed) continua a ser a linguagem principal sempre que se pensa em eficiência e performance. Devido a isto desempenha um papel muito importante no desenvolvimento de aplicações que necessitam de Interop. Isto deve-se (essencialmente) à fácil interacção entre managed e unmanaged code.
A verdade é que o novo C++ é uma linguagem poderosa e flexível que consegue ombrear com as linguagens C# e VB.Net a nível de managed code e ultrapassá-las a nível de Interop.
Onde estamos?
Bem, por hoje é tudo. Espero que este artigo tenha despertado a curiosidade do leitor para o Managed C++ que, na minha opinião, é a linguagem mais poderosa da framework.
Por favor enviem-me as vossas opiniões/sugestões/críticas/correcções para progC@netmadeira.com.
Fiquem bem e boa programação! Até à próxima.
Leiam o meu blog em: http://weblogs.pontonetpt.com/luisabreu