Programação COM em .Net - parte IV
Autor: Luís Abreu
Conteúdo: Apresenta várias considerações sobre construção de componentes .Net para serem consumidos por unmanaged code
Ferramentas: Visual Studio 2003
Hoje tive um sonho: Vivia num mundo ideal, e, tendo em atenção o nosso "amor" pela plataforma .Net, apenas construíamos aplicações em .Net. Contudo, o mundo em que vivemos é capitalista (em vez de ser ideal :) ). Devido a isso é impensável pegar em aplicações já feitas e deitá-las fora (especialmente se pensarmos que estas custaram muito dinheiro). hum...então como é que conseguimos obter a nossa satisfação pessoal(derivada da utilização da plataforma .Net) e, ao mesmo tempo, a satisfação daqueles que nos pagam? O leitor atento já sabe a resposta: através do reaproveitamento daquilo que foi feito nos tempos "antigos" (ou seja, pré-.Net) e construção de novas funcionalidades através da nova plataforma. Por outras palavras: Interop.
Infelizmente esta filosofia professada no último parágrafo nem sempre é aplicável (especialmente se pretendermos performance máxima). Por outro lado, se essa diminuição de performance for aceitável, então nada melhor do que "arregaçar as mangas" e começar a programar utilizando Interop. O leitor atento (que tem acompanhado a "saga") já tem conhecimentos suficientes para reaproveitar componentes/objectos COM unmanaged. A questão que se coloca é: como é que construo componentes .Net para serem consumidos por aplicações unmanaged através de COM? O objectivo deste artigo passa por responder a esta pergunta.
Interacção entre componentes .Net e componentes COM
Surpresa: as aplicações unmanaged acedem a um componente .Net através de um wrapper designado de COM Callable Wraper (CCW) . Aposto que ninguém era capaz de advinhar isto! A utilização dum wrapper é obrigatória pois, como vimos anteriormente, existe uma diferença a nível de funcionamento entre estes dois mundos. O CCW tem a responsabilidade de gerir o objecto .Net de forma a que este respeite as regras definidas pelo COM (ou seja, o CCW irá gerir o objecto tendo em atenção todas as particularidades relativas à identidade , scope e interfaces dos objectos).
Os CCWs são criados na unmanaged heap, garantindo assim que o código unmanaged consegue aceder directamente aos interfaces expostos por este tipo de componentes. Por outro lado, e como sempre acontece, o objecto .Net será sempre criado na managed heap, usufruindo assim da gestão automática de memória. Muito interessante também é o facto do CCW conseguir (simultaneamente) manter a contagem relativa ao número de clientes unmanaged actuais (garantindo assim o respeito pelas regras do COM) e de manter o apontador para a managed heap onde foi alocado o objecto .Net que está a ser exposto a clientes unamanged. O CCW limita-se a libertar a referência ao objecto .Net quando o contador relativo ao número de clientes unmanaged atingir o valor zero.
Convém referir que os CCWs suportam vários interfaces standard definidos pela especificação COM (isto, claro, para além dos interfaces/métodos definidos pelo objecto .Net):
- IUnknown: como foi referido préviamente, é o interface fundamental de toda a programação COM.
- IDispatch: responsável pelo chamado late binding; é utilizado essencialmente pelos clientes script.
- IProvideClassInfo: permite obter interface ITypeInfo, de forma a obter informação sobre o objecto.
- ISupportErrorInfo: interface que permite a um cliente determinar se o objecto COM fornece informação adicional sobre um eventual erro.
- IErrorInfo: Fornece informação relativa a um determinado erro.
- ITypeInfo: Permite obter informação sobre a classe;
- IDispatchEx: este interface só será exposto pelo CCW se o nosso objecto .Net implementar o interface IExpando. O interface IDispatchEx consistiu numa evolução do interface IDispatch de forma a satisfazer as necessidades das linguagens de scripts. É devido a este interface que as linguagens de scripts conseguem construir "objectos dinâmicos", definindo assim eventuais novos membros de um objecto, eliminando membros existentes, etc. A MSDN fornece alguma informação interessante sobre este assunto. Do ponto de vista do .Net, podemos considerar o interface IExpando como sendo o equivalente deste interface em .Net.
- IConnectionPointContainer: este interface será implementado apenas se a classe .Net contiver eventos. Este interface permite a um cliente unmanaged obter informação sobre um determinado Connection Point.
- IConnectionPoint: define um connection point e, tal como no caso anterior, apenas irá ser implementado pelo CCW se a classe .Net apresentar eventos públicos.
Geração dos CCWs
Para um objecto .Net ser utilizado por clientes unamanged devem ser seguidos os seguintes passos:
- Construir o componente .Net;
- Criar uma type library (opcional);
- Atribuir um strong name ao assembly;
- Introduzir as entradas correctas no registry;
Bom, vamos lá analisar cada um destes itens de forma a percebermos melhor todo este processo.
Construir o componente .Net
Bem, apesar de todo esta conversa "agradável" acerca dos eventuais interfaces expostos pelo CCW, o leitor mais curioso deverá estar a pensar na eventual transformação que irá ocorrer a nível da classe .Net que irá ser exposta. Como foi referido num dos artigos anteriores, a definição de um objecto COM é feita através de interfaces, sendo através destes que os clientes efectuam as operações desejadas. Esta informação não é nova...então a questão é: se eu construir uma classe .Net, como é que esta vai ser exposta aos clientes unmanaged? A resposta é: depende de vários factores (que irão ser analizados em seguida).
Para tornar esta discussão mais simples, vamos introduzir uma nova classe que, para variar, vai ser designada de CarNet :).
namespace CarNetLib
{
public class CarNet
{
private int _topSpeed;
private string _brand;
private int _currentSpeed;
public CarNet()
{
_topSpeed = _currentSpeed = 0;
_brand = "";
}
public int TopSpeed
{
get
{
return _topSpeed;
}
set
{
_topSpeed = value;
}
}
public string Brand
{
get
{
return _brand;
}
set
{
_brand = value;
}
}
public int CurrentSpeed
{
get
{
return _currentSpeed;
}
set
{
_currentSpeed = value;
}
}
}
}
Esta classe é muito simples e vai servir de ponto de partida para analisarmos a transformação existente. Como foi referido, irá ser necessário adicionar informação sobre o assembly ao registry e também será necessário adicionar o assembly ao GAC (de forma a que seja mais fácil encontra o assembly. A única novidade aqui reside na forma como iremos gerar a type library e adicionar o item ao registry. Existem duas ferramentas que permitem efectuar essas operações:
- TlbExp: este utilitário (que é utilizado a partir da linha de comandos) permite criar uma type library a partir de um assembly .Net. Para criarmos a type library basta escrevermos tlbexp nome_assembly [/opcoes].
- RegAsm: por outro lado, o regasm permite registar um componente .Net como componente COM, gerando também opcionalmente a type library. Na maior parte dos casos, é normal apenas utilizar este utilitário. A sua utilização também é bastante simples. Por exemplo, se quisermos registar o componente e obter a type lib podemos recorrer ao seguinte: regasm nome_assembly.dll /tlb:nome_typelib.tlb.
Nota: Para obter mais informação sobre estas ferramentas pode consultar a ajuda online disponível na MSDN.
Apesar de geralmente não utilizar essa opção, o Visual Studio permite-nos adicionar a informação automaticamente ao Registry. Para tal basta modificar as propriedades do projecto de forma a que a opção Register For Com Interop (Configuration Properties -> Build) esteja seleccionada.
Continuando com o que importa, vamos começar por efectuar o build do projecto. Em seguida, vamos adicionar o componente ao GacUtil e também vamos registar o componente para ser utilizado através de COM. Para tal são necessárias as seguintes instruções:
gacutil -i carlib.dll
regasmg carlib.dll /tlb:carlib.tlb
Então vamos lá ver o aspecto da nossa classe .Net da perspectiva dos objectos COM. Para tal, podemos utilizar o Ole/Com Object Viewer. Ao abrirmos a type library, obtemos o seguinte:
// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: carlib.tlb
[
uuid(E91D3614-D2AB-3CFC-92E7-9F149DA973BF),
version(1.0),
custom(90883F05-3D28-11D2-8F17-00A0C9A6186D, CarLib, Version=1.0.1695.34651, Culture=neutral, PublicKeyToken=c8820e5c7f53a54e)
]
library CarLib
{
// TLib : // TLib : Common Language Runtime Library : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
importlib("mscorlib.tlb");
// TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");
// Forward declare all types defined in this typelib
interface _CarNet;
[
uuid(8DB6394B-C7C7-3DD6-9071-BE2D0C13E7AF),
version(1.0),
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, CarNetLib.CarNet)
]
coclass CarNet {
[default] interface _CarNet;
interface _Object;
};
[
odl,
uuid(28788515-411D-300B-828E-24658BCAF4C0),
hidden,
dual,
oleautomation,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, CarNetLib.CarNet)
]
interface _CarNet : IDispatch {
};
};
A grande dúvida do momento está desfeita! Foi criado automaticamente um interface (_CarNet) que irá permitir aos clientes COM acederem às funcionalidades do objecto. Quando um objecto .Net não implementa nenhum interface, então o processo de exportação gera um automaticamente cujo nome é obtido através da combinação do underscore (_) e do nome da classe. Para além disso, repare-se que o processo de exportação também gerou uma CoClass com o mesmo nome da nossa classe .Net que, como é óbvio, implementa o interface _CarNet e o interface _Object. Muito importante também é reparar que o interface é um IDispatch puro (o que não é o ideal).
Neste casos o interface gerado automaticamente é designado de class interface. A utilização deste tipo de interfaces traz alguns inconvenientes, pelo que esta estratégia não deverá ser utilizada em código de produção. A primeira desvantagem reside no facto podermos modificar o interface exposto se modificarmos o layout da nossa classe (para tal basta, por exemplo, adicionarmos uma nova propriedade). Se isto acontecer, estamos a quebrar uma das regras fundamentais do COM (após ser publicado, um interface nunca deve ser modificado!). Por outro lado, como foi indicado no parágrafo anterior, estamos a gerar (por defeito) um interface IDispatch puro, o que está longe de ser ideal para todas as situações. Todas estas situações podem ser facilmente contornadas através da utilização de atributos e da definição de interfaces como iremos ver em seguida.
Personalização do componente COM exportado
A framework permite-nos personalizar os interfaces e as classes exportadas através de um conjunto de atributos que irão ser apresentados agora. Vamos começar por vermos as hipóteses disponíveis para controlarmos a geração do interface COM.
Definir do interface COM
Como vimos, uma classe .Net gera (por defeito) um interface IDispatch puro. Contudo, podemos controlar essa definição através do atributo ClassInterfaceAttribute. Este atributo suporta três valores:
- AutoDispatch: Neste caso será gerado um interface IDispatch puro (ou seja, na prática é como se a nossa classe .Net apresentada nos parágrafos anteriores tivesse sido definido com este atributo);
- AutoDual: neste caso é gerado um interface de acordo com as regras dos dual interfaces. Este interface é útil pois permite satisfazer os clientes a nível de early e late binding.
- None: é provavelmente o valor adequado à maior parte das situações. Neste caso o processo de exportaçõa não gera nenhum interface para classe. Deve ser utilizado quando a classe implementa um interface.
Para testarmos este atributo, vamos modificar a nossa clase de forma a que ela seja anotada com o atributo ClassInterfaceAttribute contendo o valor AutoDual:
[ClassInterface(ClassInterfaceType.AutoDual)]
public class CarNet
{
....
Ao compilarmos o código e ao utilizarmos o regasm obtemos a seguinte type library:
// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: carlib.tlb
[
uuid(EB4316C8-D437-39EC-9B10-68FCB58E82F9),
version(1.0),
custom(90883F05-3D28-11D2-8F17-00A0C9A6186D, CarLib, Version=1.0.1695.38896, Culture=neutral, PublicKeyToken=c8820e5c7f53a54e)
]
library CarLib
{
// TLib : // TLib : Common Language Runtime Library : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
importlib("mscorlib.tlb");
// TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");
// Forward declare all types defined in this typelib
interface _CarNet;
[
uuid(C650CFBF-B190-3C2E-917D-BDBF2CCAB05C),
version(1.0),
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, CarNetLib.CarNet)
]
coclass CarNet {
[default] interface _CarNet;
interface _Object;
};
[
odl,
uuid(DD0B73B3-067F-35C8-8403-7FC0DCFC94BF),
hidden,
dual,
nonextensible,
oleautomation,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, CarNetLib.CarNet)
]
interface _CarNet : IDispatch {
[id(00000000), propget,
custom(54FC8F55-38DE-4703-9C4E-250351302B1C, 1)]
HRESULT ToString([out, retval] BSTR* pRetVal);
[id(0x60020001)]
HRESULT Equals(
[in] VARIANT obj,
[out, retval] VARIANT_BOOL* pRetVal);
[id(0x60020002)]
HRESULT GetHashCode([out, retval] long* pRetVal);
[id(0x60020003)]
HRESULT GetType([out, retval] _Type** pRetVal);
[id(0x60020004), propget]
HRESULT TopSpeed([out, retval] long* pRetVal);
[id(0x60020004), propput]
HRESULT TopSpeed([in] long pRetVal);
[id(0x60020006), propget]
HRESULT Brand([out, retval] BSTR* pRetVal);
[id(0x60020006), propput]
HRESULT Brand([in] BSTR pRetVal);
[id(0x60020008), propget]
HRESULT CurrentSpeed([out, retval] long* pRetVal);
[id(0x60020008), propput]
HRESULT CurrentSpeed([in] long pRetVal);
};
};
Como é possível verificar, ao contrário do que acontecia no primeiro exemplo, a definição do interface jé define um conjunto de métodos de forma a optimizar o código relatico aos clientes que suportam early binding. Contudo, continuamos ainda sem conseguir controlar a definição do nome do interface. Bom, a melhor solução para este caso consiste em definir explicitamente o interface (definindo também o tipo de interface COM) e anotar a classe que implementa esse interface de forma a que esta não gere o interface que normalmente gera.
Definir do interface COM de forma explicita
O primeiro passo consiste em construirmos um interface com os métodos/propriedades que desejamos. Em seguida, convém definir o tipo de interface. Para tal podemos recorrer ao atributo InterfaceTypeAttribute, que suporta os seguintes valores:
- InterfaceIsDual: neste caso o interface será exportado como sendo um dual interface;
- InterfaceIsDispatch: o interface será exposto aos clientes COM como sendo um IDispatch puro;
- InterfaceIsUnknown: interface é apresentado como sendo um interface COM personalizado (e que, devido a isso, apenas pode ser consumido através de early binding - ou seja, não está disponível para ser consumido pelos clientes script).
Modifiquemos então nosso código .Net para o seguinte e vejamos os resultados:
[InterfaceType(ComInterfaceType.InterfaceIsIDual)]
public interface ICarNet
{
int TopSpeed{get;set;}
string Brand{get;set;}
}
[ClassInterface(ClassInterfaceType.None)]
public class CarNet:ICarNet
{
...
Esta é a situação ideal pois através deste tipo de definições conseguimos controlar por completo o aspecto do nosso interface COM. Se o leitor tem vindo a efectuar os passos todos apresentados no artigo, já deve ter reparado que o registry está a ficar com várias versões do mesmo componente. Isto deve-se ao facto de não ter sido definido e associado um GUID à classe .Net. Devido a isto, cada vez que é registado o componente (através de regasm) é adicionada uma nova entrada no registry.
Atributos Guid e ProgId
Para resolvermos essa situação, podemos recorrer aos atributos GuidAttribute e ProgIdAttribute. O atributo GuidAttribute permite associar (de forma explicita) um GUID a um tipo .Net. Geralmente o ProgId da classe também é gerado de forma automática com base no namespace e nome da classe. Contudo, se utilizarmos o atributo ProgIdAttribute conseguimos controlar este aspecto da exportação para COM. O código seguinte demonstra as alterações efectuadas no código C# e as respectivas consequências:
[
Guid("E344084F-BE24-4a74-B3DC-F74B70A8A47E"),
InterfaceType(ComInterfaceType.InterfaceIsIDual)
]
public interface ICarNet
{
int TopSpeed{get;set;}
string Brand{get;set;}
}
[
Guid("E344084F-BE24-4a74-B3DC-F74B70A8A44E"),
ProgId( "PontoNet.Car" ),
ClassInterface(ClassInterfaceType.None)
]
public class CarNet:ICarNet
{
O idl correspondente é o seguinte:
// Generated .IDL file (by the OLE/COM Object Viewer)
//
// typelib filename: carlib.tlb
[
uuid(2101F7FC-FD21-3DB1-AB1E-5167FE9931F0),
version(1.0),
custom(90883F05-3D28-11D2-8F17-00A0C9A6186D, CarLib, Version=1.0.1695.40547, Culture=neutral, PublicKeyToken=c8820e5c7f53a54e)
]
library CarLib
{
// TLib : // TLib : Common Language Runtime Library : {BED7F4EA-1A96-11D2-8F08-00A0C9A6186D}
importlib("mscorlib.tlb");
// TLib : OLE Automation : {00020430-0000-0000-C000-000000000046}
importlib("stdole2.tlb");
// Forward declare all types defined in this typelib
interface ICarNet;
[
odl,
uuid(E344084F-BE24-4A74-B3DC-F74B70A8A47E),
version(1.0),
dual,
oleautomation,
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, CarNetLib.ICarNet)
]
interface ICarNet : IDispatch {
[id(0x60020000), propget]
HRESULT TopSpeed([out, retval] long* pRetVal);
[id(0x60020000), propput]
HRESULT TopSpeed([in] long pRetVal);
[id(0x60020002), propget]
HRESULT Brand([out, retval] BSTR* pRetVal);
[id(0x60020002), propput]
HRESULT Brand([in] BSTR pRetVal);
};
[
uuid(E344084F-BE24-4A74-B3DC-F74B70A8A44E),
version(1.0),
custom(0F21F359-AB84-41E8-9A78-36D110E6D2F9, CarNetLib.CarNet)
]
coclass CarNet {
interface _Object;
[default] interface ICarNet;
};
};
Para além destes atributos, ainda existem outros importantes que serão apresentados no próximo item desta série. Bem, mas então como é que utilizamos estas classes a partir de unmanaged code?
Utilização das classes a partir de unmanaged code
Para demonstrarmos a utilização destas classes a partir de COM vamos utilizar uma página simples de HTML. O código utilizado é o seguinte (encontra-se no ficheiro teste.html que acompanha este artigo):
function ProcessClick()
{
var aux = new ActiveXObject( "PontoNet.Car" );
aux.TopSpeed = document.all.topSpeed.value;
aux.Brand = document.all.brand.value;
alert( "Top Speed: " + aux.TopSpeed + "\nBrand: " + aux.Brand );
}
Como é possível verificar, o objecto foi criado de acordo com o ProgId definido através do atributo ProgIdAttribute.
Conclusões finais
Como é possível verificar, a construção de objectos .Net para consumo COM não é muito complicada. O processo de exportação pode ser definido através de um conjunto de atributos, fazendo assim com que seja possível controlarmos os interfaces/classes apresentadas aos clientes COM.
Neste artigo começámos a analisar alguns dos principais aspectos relativos à construção deste tipo de objectos. No próximo item vamos continuar a nossa análise e vamos falar acerca da exportação de elementos (para além dos interfaces e das classes, existem também vários outros tipos que podem ser exportados), de eventos e de ActiveX Controls. 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. O código que acompanha este artigo está disponível na secção dos downloads do site.
Leiam o meu blog em: http://members.netmadeira.com/luisabreu