Você está na página 1de 33

Programação COM em .

Net - parte I
Nível: Iniciado/Médio
Conteúdo: Introdução à programação COM em .Net
Ferramentas: Visual Studio 2003
Linguagem: C#

A pergunta impõe-se: será que o COM (Component Object Model) está morto? E a resposta é... CLARO
QUE NÃO!!! Pois é...apesar de todas as novas funcionalidades fornecidas pela plataforma .Net, a
verdade é que ainda hoje são escritas milhares de linhas C++/VB6 e o alvo preferencial são objectos
COM. Bem, coloca-se então uma nova questão: então e o que fazemos nós (programadores da
plataforma .Net)? Largamos tudo e voltamos ao VB6 (por favor, tudo menos isto!!! ;) ) ou ao C++?
Resposta: CLARO QUE NÃO!!! Afinal de contas já não conseguimos viver (viver = programar) sem esta
plataforma. A solução para este enigma: utilização do chamado Interop.

O Interop permite-nos interagir com o código pré-.Net, ou seja, permite-nos utilizar (quase de forma
transparente) os milhares e milhares de linhas de código que foram construídas antes do aparecimento
do .Net. Podemos afirmar que existem dois tipos principais de Interop:
← Comunicação com funções exportadas de DLLs tradicionais, com recurso ao chamado PInvoke;
← Comunicações com objectos COM, com recurso aos RCW (Runtime Callable Wrapper); podemos
ainda construir os nossos próprios objectos .Net para serem consumidos por aplicações COM;
nesse caso temos de utilizar os CCW (COM Callable Wrappers).

Ao longo desta série de artigos iremos falar sobretudo sobre interacção entre .Net e o COM. Espero
abordar vários aspectos importantes relacionados com o consumo de objectos COM e com a construção
de objectos .Net que deverão ser consumidos por aplicações (unmanaged) COM. Esta é uma área ampla,
pelo que iremos dividir todo o conteúdo por vários artigos, de forma a melhor demonstrarmos todos os
aspectos importantes.

Hoje vamos começar por efectuar uma introdução ao COM. Iremos apontar as principais diferenças entre
as tencologias COM e .Net. Vamos também ver como é que podemos consumir um objecto COM a partir
de código .Net. Queria apenas referir que, ao contrário do que tem sido hábito, este artigo é bastante
teórico, sem nenhum exemplo prático da utilização de componentes COM em .Net. Isto deveu-se
essencialmente a dois factores: não me pareceu necessário, uma vez que o objectivo principal é
introduzir as principais diferenças entre ambas as tecnologias e explicar como é que os dados são
importados de um lado para o outro; por outro lado, nestes últimos dias tenho tido o mesmo problema
que afecta a grande parte das pessoas que trabalham nesta área: falta de tempo.

COM: Component Object Model

Bem, se tivesse que descrever pormenorizadamente o COM, penso que era capaz de escrever vários
livros (em vez de um artigo). Isto porque são tantos os pormenores que num instante conseguia
preencher páginas e mais páginas com informação. Bem, para além disso ainda havia o facto de
praticamente ninguém conseguir perceber o COM em toda a sua extensão. Provavelmente a única
excepção é o inevitável Don Box (pelo menos são os rumores que correm).

Ora bem, mas então como podemos definir o COM? Penso que posso afirmar que o COM foi um
protocolo importante para permitir a reutilização de código por parte de várias aplicações. Na teoria,
todos os objectos herdavam de um interface comum: IUknown. Este interface continha 3 métodos
importantes:
← QueryInterface: este era provavelmente o método mais importante deste interface. Era através
deste método que podíamos verificar se um objecto implementava outro interface. Portanto,
podemos afirmar que se quiséssemos fazer um cast em COM teríamos de recorrer a este
interface.
← AddRef: permitia incrementar a contagem de referência;
← Release: permitia efectuar o decremento da contagem de referência.

Os últimos dois métodos eram muito importantes para manterem um determinado objecto "vivo". Isto
porque no COM era utilizado um mecanismo de contagem, em que um objecto era eliminado sempre
que a sua contagem chegava a zero. Para garantir a correcção deste mecanismo, temos de actualizar a
contagem interna dos objectos através dos métodos AddRef e Release. O principio de funcionamento era
simples: sempre que passávamos um objecto, incrementávamos a sua contagem interna evocando o
método AddRef (isto podia acontecer, por exemplo, quando retornávamos um objecto como parâmetro
de output); quando já não precisávamos dele, então informávamos o objecto através do método
Release, decrementando assim o valor interno de referência do elemento. Quando esse valor chegava a
zero, o elemento era responsável por proceder à sua própria eliminação.

Apesar do principio ser fácil, eram poucos os programadores que conseguiam construir código correcto.
Bem, os programadores de VB tiveram a sua vida simplificada, porque conseguiam construir
componentes de forma praticamente transparente. Infelizmente (ou talvez não;) ), tal não aconteceu
com os programadores de C++, que tinham de escrever (por vezes) várias linhas de código para
efectuarem uma operação simples.

Mas voltando novamente ao COM, e uma vez que esta tecnologia deveria servir para reaproveitar o
código existentes entre as várias tecnologias da época, chegou-se à conclusão que deveria haver (pelo
menos) mais um conjunto de interfaces que permitissem executar determinadas operações comuns. A
seguir ao IUknonw (que, como pudemos, comprovar foi a fonte de todo o mal ;) ), o interface mais
famoso foi, sem sombras de dúvidas, o IDispatch. Este interface permitia aos chamados late-bound
cients criar instâncias de classes sem saberem antecipadamente os métodos/propriedades que esses
objectos possuíam. Este interface era extremamente importante para permitir a utilização de objectos
COM a partir de linguagens script (e também, porque não dizê-lo, de versões mais antigas da linguagem
VB).

À semelhança do que acontecia com o interface IUnknown, este interface também apresentava um
conjunto métodos cuja implementação era obrigatória. Os mais importantes eram:
← GetIDsOfNames: permitia converter um nome de um método ou propriedade no seu ID, de
forma a que seja possível invocar esse elemento;
← Invoke: utilizado para evocar um membro de uma classe, utilizando sempre o ID obtido através
do método descrito no item anterior.

Para além destes dois interfaces muito conhecidos, ainda existiam inúmeros outros (bem, são tantos que
eu não conheço metade deles!). E, para aumentar a confusão, deveríamos expor as funcionalidades
básicas do nosso objecto através de interfaces personalizados (ou seja, as funcionalidades do objecto
deveriam ser expostas por interfaces construídos pelo próprio programador). Estes interfaces derivavam
(quase sempre) de IUnknow ou e IDispatch, e continham os métodos/propriedades que queriamos
fornecer aos clientes.

Bem, como podem ver, a construção de um objecto COM não era muito fácil. Para além de termos de
construir as funcionalidades básicas que os objectos deviam fornecer, tínhamos ainda de implementar
toda a framework necessária ao correcto funcionamento dos nossos componentes! Portanto, COM era
sinónimo de muito trabalho! (e ainda nem falei nos vários aspectos relativos ao registo dos
componentes!) Com o passar do tempo, a Microsoft tentou facilitar a vida dos programadores, com a
construção de diversas frameworks (como por exemplo o ATL), mas a verdade é que a construção de
componentes COM era, como dizem os nossos amigos ingleses, um PITA (ou, por extenso, pain in the
ass).

Felizmente para nós, apareceu uma framework chamada .Net! Nesta framework, a construção de
componentes faz-se, como se costuma dizer, com "uma perna às costas". Podemos nos concentrar
apenas nas funcionalidades que pretendemos fazer e temos inúmeras vantagens, como por exemplo:
← Fácil integração do componente com outras linguagens de programação;
← Fácil utilização de várias versões de um componente (parece que acabou o DLL hell)
← Uma framework com um modelo lógico decente (o que, como é óbvio, não acontecia antes).

Estas são apenas algumas das vantagens decorrentes da utilização da .Net. Existem muitas mais, mas
penso que estas já servem para dar uma ideia dos ganhos provenientes da utilização desta plataforma.
Bem, mas se é assim, porque é que estou a escrever um artigo sobre programação COM através de
.Net? Bem, porque ao que parece ainda vamos continuar a utilizar ( e mesmo a construir) objectos COM
nos próximos anos. Sinceramente não estou a vê-los desaparecerem tão cedo. Como prova,
actualmente estou a trabalhar na conversão de um projecto COM VB6 (constituído por vários
componentes) para componentes .Net que irão ser consumidos através de COM (isto porque a aplicação
apenas consegue consumir add-ins construídos em COM, apesar de no futuro estar prevista a saída de
uma nova versão que permita o acesso directo a .Net).

Bem, prevendo todas estas condicionantes, a Microsoft dotou a framework de um conjunto de


mecanismos que permitem o reaproveitamento de objectos COM em aplicações .Net e vice-versa, ou
seja, se quisermos também podemos construir objectos .Net de forma a que estes sejam consumidos
por aplicações COM.

Localização de componentes

A localização de componentes é feita de forma muito diferente em ambos os casos. Os componentes


COM podem estar localizados em qualquer lado (mesmo numa máquinas diferente da que o componente
é utilizado). A única informação centralizada são os dados relativos ao componente, que ficam sempre
guardados em chaves situadas no Registry. Geralmente um objecto deste tipo guarda sempre
informações relativas ao seu GUID (Globally Unique Identifier) e ao seu ProgID (Programatic Id) em
chaves pré-definidas. A informação guardada no registry contém ainda informação relativa ao chamado
appartment em que o objecto deseja ser uitlizado. A quantidade de informação armazenada no registry
depende também do tipo de componente que estamos a construir (os controlos ActiveX, por exemplo,
contém quase sempre mais informação do que um componente simples).

Por outro lado, em .Net há apenas dois locais onde o componente se pode encontrar: GAC (Global
Assembly Cache) ou então na pasta onde a aplicação está instalada.

Identificação

Também a forma como os objectos são identificados difere entre objectos COM e objectos .Net. No COM,
toda a informação importante (se assim podemos dizer) está directamente relacionada com um GUID.
Um GUID não é mais do que uma estrutura de 128 bits que (supostamente) identifica unicamente um
objecto em todo o mundo. Este GUID está sempre associado a um progID (que podemos definir como
um nome amigável - geralmente da forma namespace.className), e, como referimos anteriormente,
toda esta informação está armazenada no registry.

Por outro lado, os objectos .Net não recorrem aos GUIDs. Recorrem aos chamados fully qualified names.
Se quisermos identificar unicamente uma classe contida num assembly, podemos fornecer-lhe o
chamado strong name. Neste caso, conseguimos identificar unicamente um assembly e, por
conseguinte, uma classe contida nesse assembly, utilizando para isso, o seu nome, o namespace onde
reside e a informação relativa ao assembly (que inclui, nome, versão e cultura).

Scope dos objectos

Como referi acima, os objectos COM possuem um scope controlado internamente por um mecanismo de
contagem. Para tal utilizamos os métodos AddRef e Release do interface IUnknown. A construção destes
objectos parte do principio que o método Release é implementado de tal forma que quando a contagem
de elementos chega a zero, este é automaticamente eliminado.

Por outro lado, em .Net temos um funcionamento bastante diferente. Como todos sabemos, a gestão
dos objectos é efectuada pelo CLR. Neste caso, temos um Garbadge Collector que efectua a limpeza de
memória, utilizando para tal um algoritmo baseado em gerações. Na minha opinião, o grande problema
desta estratégia reside no facto da finalização não puder ser determinística (o que, como iremos ver,
poderá ter algumas implicações na programação .Net -COM).

Informação sobre os tipos

Ora cá está uma área em que a plataforma .Net bate o COM aos pontos! Os objectos COM fornecem
uma descrição do seu interface através das chamadas Type Libraries. Estes ficheiros permitem-nos obter
a informação relativa aos vários métodos/propriedades contidos num objecto. O problema é que a
descrição contida nas type libraries não consegue guardar toda a informação associada a um objecto.
Para os que nunca tiveram a sorte de construir uma type lib "à mão", convém ter a noção que as type
libs são construídas através de uma linguagem designada de IDL. Como referi, em run-time, sempre que
precisamos de verificar se um componente implementa um determinado interface, temos de recorrer ao
método QueryInterface e verificar o resultado contido no HRESULT retornado.

Em .Net a conversa é bem diferente. Para tal contribui o facto dos componentes .Net conterem SEMPRE
a chamada metadata, que nos permite obter todo o tipo de informação relativo ao componente através
de técnicas designadas de Reflection. Portanto, os componentes .Net são totalmente encapsulados e
contém toda a informação necessária à auto-descrição dos tipos contidos num assembly.

Tratamento de erros

Mais uma área em que as diferenças são abismais. No COM, recorremos ao valores retornados do
método para sabermos se o método foi executado com êxito. Isto obriga a que todos os resultados seja
encapsulados num chamado HRESULT, que não é mais do que um inteiro de 32 bits. O COM apresenta
vários HRESULTS pré-definidos, como por exemplo, o S_OK. Podemos também construir os nossos
próprios resultados, desde que respeitemos determinadas regras.

Em .Net, podemos recorrer às excepções para sinalizar o facto de um método não ser executado
correctamente. No COM não podemos utilizar este mecanismo pois ele não é comum a todas as
linguagens onde o componente pode ser utilizado, ao contrário do que acontece em .Net.

Interacção entre componentes COM e componentes .Net

Como era de esperar, a Microsoft facilitou-nos a integração entre estes dois tipos de componentes, com
a excepção da utilização de User Controls como ActiveX Controls. Neste caso, apenas é garantido o
funcionamento de componentes .Net em hosts MFC 7.X ou no Internet Explorer. Contudo, e após seguir
as algumas dicas do "grande" Chris Sells, já consegui efectuar o hosting de um User Control numa
aplicação MFC de versão anterior (mais informações num artigo futuro).

Quando construímos um componente .Net para ser utilizado em COM, este assume o modelo Both por
defeito. Isto obriga-nos a escrever código que pode ser acedido simultaneamente por mais do que uma
thread. Por outro lado, quando queremos utilizar componentes COM no nosso código estes são colocados
num apartment MTA por defeito (ou seja, por defeito os componentes COM devem estar preparados
para ser acedidos simultaneamente a partir de várias threads).

Infelizmente, a maior parte dos componentes não está preparado para este tipo de acesso (basta
recordar que muitos foram construídos em VB6). Por outro lado, o tipo de apartment em que o objecto é
utilizado (em .Net) pode ser configurado através dos atributos STAThreadAttribute (que o Visual Studio
coloca automaticamente no método Main das aplicações Windows Form - obrigado Visual Studio ;) ) e
MTAThreadAttribute ou através da propriedade ApartmentState da classe Thread.

Marshaling

A comunicação entre componentes destes mundos tão diferentes é feita, como referi anteriormente, de
forma praticamente transparente. Contudo, convém ter a noção que quando estabelecemos a
comunicação entre componentes de mundos diferentes é possível (ou melhor, é necessário) enviar
valores de um lado para outro. Para tal basta pensarmos que muitos dos métodos recebem parâmetros.
Neste caso (em que transmitimos informação de um "mundo" para outro), estamos a utilizar o chamado
Interop Marshaling. A conversão automática dos tipos depende da representação dos tipos em memória.
Muitos dos tipos têm uma representação comum em ambos os tipos de componentes, pelo que a
conversão entre ambos os mundos consiste em fazer o mapeamento directo entre um tipo e outro tipo
(aqui deve-se interpretar mapeamento directo como sendo a cópia directa dos bits de um espaço de
memória para outro).

Por exemplo, os tipos numéricos inteiros têm conversão directa entre .Net e COM. Infelizmente isso não
acontece com outros tipos, como por exemplo as strings ou os arrays. Estes elementos são ambíguos,
pois existe mais do que um tipo possível de representação. Neste caso, podemos deixar o chamado
Marshaller escolher uma representação de forma automática ou podemos indicá-la explicitamente
através de atributos. Se necessitarmos de interagir de uma forma mais avançada com o COM, podemos
também recorrer à classe Marshal, que contém muitos métodos úteis.

Eventos

Mais uma vez estamos perante estratégias completamente diferentes. O COM recorre aos chamados
Connection Points, que permitem a um componente externo indicar que está interessado em receber
eventuais eventos. Neste caso, é costume o objecto interessado em receber os eventos implementar um
interface (definido pelo objecto COM) e depois registar-se junto ao componente que despoleta o evento
de forma a conseguir ser informado desse evento.

Por outro lado, o .Net recorre a uma estratégia mais amigável baseada em eventse, por conseguinte, em
delegates). Pergunta: qual a melhor? Prefiro responder à: qual a minha preferida? A de .Net ;).

Utilização de componentes COM em .Net

Como foi referido anteriormente, existem várias diferenças entre os componentes COM e os
componentes .Net. Então como é que estabelecemos a comunicação entre ambos estes tipos de
componentes? A resposta é simples: através dos chamados Interop Assemblies. Estes assemblies são
assemblies especiais que permitem estabelecer a comunicação entre componentes .Net e componentes
COM.

Portanto, o primeiro passo para reutilizar componentes COM em .Net consiste em gerar os Interop
Assemblies. Para tal, temos várias hipóteses:
← Recorrer à importação do componente através do Visual Studio;
← Recorrer directamente às ferramentas .Net (tlbImp.exe e aximp.exe);
← Utilizar a classe TypeLibConverter e efectuar a conversão através de código.

Como é possível aferir, qualquer pessoa pode construir um assembly de interop baseado num
determinado componente COM. Isto pode gerar alguns problemas. Por exemplo, se vários vendedores
diferentes gerarem Interop assemblies dum mesmo componente, então, se necessitarmos de utilizar os
vários componentes desses vendedores, teríamos de ter cópias dos vários interop assemblies referentes
ao mesmo objecto COM. Estes assemblies são considerados diferentes pois contém (poucos) pormenores
diferentes (como por exemplo, informação sobre quem os gerou).

Para resolver este tipo de situação é possível construir um tipo especial de Interop Assembly: estou a
falar dos Primary Interop Assemblies. Este tipo de assembly deve ser construído pelas empresas que
construíram o objecto COM inicial. Por exemplo, a Microsoft já disponibilizou as PIA para as suas versões
do Office.

Geração de Interop Assemblies através do Visual Studio

A utilização do Visual Studio simplifica bastante a geração deste tipo de assemblies. Se quisermos
utilizar um componente COM, basta clicarmos com o botão direito sobre o projecto e escolher a opção
Add Reference. Em seguida, escolhemos o tab COM e indicamos o objecto que pretendemos utilizar.

Por outro lado, se pretendermos utilizar um ActiveX (portanto, um componente COM com interface
gráfico), temos de em primeiro lugar adicioná-lo à toolbox do VS, através da opção Add/Remove
Components. Após adicionarmos o componente à toolbox, basta arrastá-lo para o form e podemos
utilizá-lo normalmente (como se este tivesse sido desenvolvido em .Net).

Em ambos os casos, o Visual Studio encarrega-se de gerar as dlls que contém o código necessário ao
Interop (para comprovar, basta verificar o conteúdo da pasta bin).

Geração de Interop Assemblies através do tlbimp.exe e aximp.exe

Bem, para os que não têm a felicidade de terem o Visual Studio, existem as ferramentas tlbimpl.exe e
aximp.exe que podem ser utilizadas para gerarem os assemblies de Interop. A ferramenta tlbImpl.exe é
utilizada para gerar um assembly de Interop "normal", enquanto que o aplicativo aximp.exe pode ser
utilizado para gerar wrappers para componentes ActiveX.

Começando pelo tmbImp.exe, é possível verificar que existem várias opções que podem ser utilizadas.
Por exemplo:

tlbimp myProject.tlb /out:project.dll /namespace:test /sysarray

Gera um assembly com o nome definido pelo switch out, que se encontra contido no namespace
indicado (neste caso, Test) e procede à conversão de todos os elementos do tipo SAFEARRAY em
Arrays .Net. Para além destas opções, existem ainda outras, como por exemplo o reference, que permite
indicar explicitamente qual o ficheiro que contém Interop Assemblies referenciados pela tlb que está a
ser convertida (isto pode ser necessário porque as type libraries podem importar outras type libs).

Como sempre, a documentação que acompanha a framework contém uma descrição de todos os
parâmetros que podem ser utilizados com este comando.

Se necessitarmos de utilizar um ActiveX Control, então temos de recorrer a outra ferramenta:


aximp.exe. Este aplicativo consegue construir uma dll de Interop que contém toda a metadata
necessária para que os ActiveX sejam utilizados numa aplicação que contenha Forms. Não esquecer que
para utilizarmos Um ActiveX control num formulário é necessário gerar um controlo (classe) que deriva
de AxHost que contém toda a informação necessária ao hosting do ActiveX que queremos utilizar (esta
classe é gerada pela ferramenta aximp.exe).

Utilização da classe TypeLibConverter

Finalmente, podemos recorrer à classe TypeLibConverter para efectuarmos conversões entre


componentes COM e .Net (e vice-versa, se for necessário). Esta classe apresenta três métodos
importantes:
← ConvertAssemblyToTypeLib: como o próprio nome indica, permite construir uma type library a
partir de um assembly .Net;
← ConvertTypeLibToAssembly: bem, pelo nome percebe-se que efectua a operação inversa do
método anterior;
← GetPrimaryInteropAssembly: gera um Primary Interop Assembly.

O sample que acompanha este código contém um projecto (designado de COMToNetConverter) que
demonstra a utilização desta classe para construirmos um wrapper em torno de um objecto COM. O
código é bastante simples e não efectua qualquer tipo de handling de eventuais erros que possam surgir
(portanto, atenção quando realizarem os testes ;) ).

O único aspecto mais complexo (se é que assim podemos dizer) reside na definição do local e gravação
do assembly criado dinamicamente. Para conseguirmos definir a pasta onde deve ser efectuada a
gravação, temos de recorrer ao método DefineDynamicAssembly, como é mostrado no seguinte excerto:

AssemblyBuilder outputFile = converter.ConvertTypeLibToAssembly( typeLib, _outputPath.Text,


GetTypeLibFlags(),
handler,
GetPublicKey(), GetStrongNameFromFile( _strongNamePath.Text ),
GetNamespace(),
GetVersion() );
//break output file name
int pos = _outputPath.Text.LastIndexOf( "\\" );
string dir = _outputPath.Text.Substring( 0, pos );
string file = _outputPath.Text.Substring( pos + 1 );

outputFile.DefineDynamicModule( dir );

Como é possível verificar, a conversão de um componente COM para .Net não poderia ser mais simples.
A passagem da aplicação para multithreading é deixada como exercício para o leitor ;) .

Como é que efectuada a conversão dos tipos

Como é sabido, as type libraries são utilizadas para descrever os tipos contidos num objecto COM.
Apesar de não ser possível descrever os tipos COM de forma tão pormenorizada como os componentes
construídos em .Net, as type libraries contém muita informação que terá de ser obrigatoriamente
convertida em metadata por forma a que esses componentes sejam consumidos por aplicações .Net.

Começando pelos tipos, a tabela seguinte mostra as conversões efectuadas entre um tipo IDL e o
respectivo tipo .Net:
IDL . Net
bool char, small System.Int32 System.SByte
short System.Int16
int, long System.Int32
hyper, int64, _int64 System.Int64
unsigner char, byte System.Byte
wchar_t, unsigned short System.UInt16
unsigned int, unsigned long System.Int32
unsigned hyper System.Int64
float System.Single
double System.Double
VARIANT_BOOL System.Boolean
void* System.IntPtr
HRESULT System.IntPtr ou System.Int16
SCODE System.Int32
BSTR System.String
LPSTR System.String
LPWSTR System.String
VARIANT System.Object
DECIMAL System.Decimal
DATE System.DateTime
GUID System.Guid
CURRENCY System.Decimal
IUnknow* System.Object
IDispatch* System.Object
SAFEARRAY Tipo de elemento[]
Alguns destes tipos merecem algum cuidado, como por exemplo, void*. Este tipo era utilizado para
representar um apontador para qualquer elemento e é (geralmente) representado em .Net pelo tipos
System.IntPtr. O System.IntPtr é definido na MSDN como sendo um tipo inteiro suficientemente grande
para armazenar um apontador (ou seja, o tamanho reservado pelo elemento depende da plataforma na
qual ele é utilizado).

Para além do void*, o Marshaller recorre a este tipo sempre que não consegue efectuar uma conversão
de um tipo IDL para um tipo .Net. A utilização deste tipo permite trabalharmos directamente com a
memória (ou seja, podemos construir blocos unsafe com este tipo de elemento). É ainda utilizado para
permitir o armazenamento das chamadas Handles.

Se necessitarmos de ler/escrever/reservar memória para este elemento podemos recorrer a vários


métodos da class Marshal, como por exemplo, o ReadByte e o WriteByte. Se por acaso o IntPtr referir
uma string, então os métodos PtrToStringAnsi e al permitem a leitura do valor armazenado na string.

Importação de Arrays

Na minha opinião, os arrays são um dos tipos mais problemáticos a nível de desenvolvimento COM.
Como é sabido, existem dois tipos diferentes de arrays em COM: os arrays típicos (semelhantes aos que
são definidos em C) e os SAFEARRAYS.

Os SAFEARRAYs têm (provavelmente) uma importância superior aos arrays "tipo C", pelo que hoje irei
falar apenas acerca deste tipo de elementos. Bem, os SAFEARRAYS apareceram para permitir a
comunicação entre o VB e COM. Ao contrário do que acontece com os arrays tradicionais, nos
SAFEARRAYs podemos ter várias dimensões. Cada dimensão contém, como é óbvio, vários elementos.
Cada dimensão de um SAFEARRAY, ao contrário dos outros arrays, não tinha que começar
obrigatoriamente na posição zero.

Ao serem importados para .Net são transformados em arrays de acordo com o tipo de elemento
armazenado pelo SAFEARRAY. Contudo, se o SAFEARRAY não começar na posição 0, então o elemento
será importado como System.Array, em vez de como array do tipo contido no SAFEARRAY. Se
necessitarmos, podemos recorrer à opção /sysarray (do tlbimp.exe) para controlarmos a importação de
forma explicita.

Importação de coclasses

O conceito de coclass pode ser uma novidade para alguns programadores, apesar de serem utilizados
por todos os programadores quando costróiem um componente COM. Ora bem, como foi possível aferir,
o COM é uma tecnologia baseada em interfaces (ou melhor, é uma tecnologia em que os interfaces
desempenham um papel muito importante). Sempre que era necessário construir um componente, era
necessário definir os métodos propriedades que o nosso objecto ia fornecer. Essa informação era
definida através de um interface (que, portanto, servia de contracto). Após definir o interface, era
necessário implementar esse interface: essa era a função das chamadas coclasses! Estes conceitos
podem parecer um pouco estranhos, especialmente para os programdores provenientes do VB6. Isto
porque, se quiséssemos, poderíamos apenas trabalhar com as coclasses em VB6, esquecendo toda a
lógica inerente à definição dos interfaces. A informação relativa a cada coclass também era definida na
type library.

A conversão das chamadas coclasses também é feita de forma (mais ou menos) simples. Como referi, as
coclasses são responsáveis pela criação de objectos COM e geralmente implementam um ou mais
interfaces definidos nas livrarias IDL. O processo de importação de uma coclass resulta na geração de
uma classe e de um interface. A classe gerada tem o nome coclassClass, ou seja, se a coclass tiver o
nome Test, então a classe gerada designar-se-á de TestClass. Esta é a classe fundamental na
comunicação entre o .Net e o COM (ou seja, é esta classe que contém todo o código necessário à gestão
dos componentes COM em .Net).

Para além da classe, também é gerado um interface (como referi anteriormente). Este interface possui o
mesmo nome da coclass e herda do interface por defeito da coclass (o interface por defeito encontra-se
marcado com o atributo default no IDL associado à type library). Este interface simplifica a vida aos
programadores oriundos do VB6, uma vez que estes estão habituados a criar os objectos directamente e
a evocarem os métodos de um interface directamente sem passarem pelo processo de aquisição de
interfaces.

Uma vez que uma coclass pode implementar vários interfaces, pode ocorrer uma colisão entre os vários
métodos definidos em interfaces diferentes. Esta colisão só é problemática se ambos os métodos tiverem
os mesmos parâmetros. Se isto acontecer, não poderão ser importados através de overload (como
acontece nas restantes situações), pelo que será necessário efectuar a distinção entre ambos os
métodos. Neste caso, os métodos que não estão associados ao default interface serão precedidos do
nome do interface (ex.: supor dois métodos, designados de Testar; se o interface não marcado com o
atributo IDL default for designado de interface 2, então o método será importado com o nome
interface2_Testar).

Importação de interfaces COM

A importação de interfaces é feita de forma directa, com o pequeno detalhe de, em .Net, não ser feita
qualquer referência aos interfaces IUnknow e IDispatch. Estes interfaces .Net são anotados com os
atributos Guid e ComInterface com os mesmos valores especificados pelos interfaces originais COM. O
atributo ComInterface indica se o interface é Dual, IUnknown ou IDispatch. É este atributo que permite
ao .Net saber como é que é feita a evocação dos métodos contidos nos interfaces.

Importação de métodos

A única referência digna de destaque reside na forma como são importados os parâmetros
"apontadores". Ao serem passados para .Net, são passados por referência, ou seja, o parâmetro é
precedido pelo termo ref (ByRef em Vb.Net). Se estivermos a falar de apontadores com mais do que
nível d(por ex.: int** p), então estes tipos serão obrigatoriamente importados para .Net como IntPtr.

Por outro lado, os parâmetros dos métodos anotados com o atributo IDL [out,retval] poderão ser
transformados no tipo de retorno do método aquando da importação, caso tal seja pretendido.

Importação de propriedades

A conversão para .Net transforma uma propriedade IDL numa propriedade .Net, que pode ser de leitura,
de escrita ou de leitura/escrita (ou seja, pode ter apenas a secção do get, a secção do set ou ambas as
secções).

Importação de outros tipos de elementos

Para além dos tipos referidos anteriormente, as type libraries podem ainda definir outros tipos. A
importação destes tipos é simples e irá ser brevemente abordada nesta secção.

O IDL também permite a criação de estruturas. Como é fácil de prever, estas são importadas
directamente para .Net através de structs.

Por outro lado, já não é possível importar uma união directamente para .Net. Isto porque o .Net são
suporta este conceito. Então para que tipo é que são importadas as livrarias? Surpresa: para uma struct.
Contudo, ao contrário do que acontece com a importação de uma estrutura, neste caso estamos a falar
de uma estrutura especial anotada com os atributos StructLayoutAttribute e FieldOffsetAttribute. É a
utilização destes atributos permite simular uma union. Por exemplo, se possuirmos a seguinte union
numa type lib:

[swicth_type(short)]union Testing
{
[case(1)]short a;
[case(1)]long a;
}

Seria convertida em .Net para:

[StructLayout(LayoutKind.Explicit)]
Public sealed struct Testing
{
[FieldOffset(0)]public Int32 l;
[FieldOffset(0)]public Int32 a;
}

Portanto, como estamos a ver, o FieldOffset permite definir o posicionamento do campo a partir do inicio
de espaço utilizado pela estrutura. Como ambos são posicionados no início, então acabamos por
conseguir simular uma union com este tipo de código.

As enumerações também não apresentam problemas na sua importação pois são convertidas numa
enumeration .Net. O mesmo já não podemos dizer em relação às constantes pois estas são
simplesmente ignoradas, não sendo por isso importadas para .Net.

Se por acaso a type library contiver tipos definidos através de typefefs, então estes serão substituídos
pelos tipos indicados, ou seja, se possuirmos o seguinte código IDL:

[public]typedef int idade;

Todas as ocorrências do tipo idade serão substituídas pelo tipo int aquando da importação para .Net

Eventos

O COM implementava os eventos através dos chamados connection points. Um cliente interessado em
ser notificado pelo objecto implementa um interface especial (designado de sink interface) e sinaliza
esse facto ao objecto COM. Esse interface é definido pelo objecto COM e é anotado na type library com o
atributo source.

Na maior parte dos casos, estes interfaces derivam directamente de interface standard IDispatch
(portanto, são designados na gíria por dispinterfaces). A descoberta destes interfaces de outgoing
requer a implementação de dois outros interfaces standard: IConnectionPointContainer e
IConnectionPoint.

Ao importarmos um interface COM anotado com o atributo IDL source para .Net, são automaticamente
gerados todos os delegates necessários ao processamento dos eventos a partir de .Net. De notar que a
construção destes delegates pode implicar a construção de outras classes necessárias. Portando,
podemos afirmar que obtemos sempre:
← Interface .Net equivalente ao interface source COM. Este interface apresenta sempre o mesmo
nome (do interface COM) com o sufixo _Event.
← Delegate para cada membro do interface importado;
← Classe com o nome interfaceImportado_SinkHelper, responsável pela implementação de todos
os passos necessários à correcta implementação do interface importado (utilizado mecanismo
de connection points do COM);
← Uma outra classe com o sufixo _EventProvider, que é responsável por efectuar a comunicação
com o interface IConnectionPointContainer do objecto COM.

Estas classes (que são geradas automaticamente) permitem-nos processar os eventos COM da mesma
forma que processamos os eventos .Net.

Conclusões finais

Este artigo serviu principalmente para introduzir os principais conceitos relativos à reutilização de
componentes COM a partir de .Net. O código que acompanha este artigo contém apenas um projecto
C#, designado de ComToNetConverter, que demonstra como podemos criar um assembly .Net a partir
da classe TypeLibConverter. Esta classe permite-nos a criação de assemblies em run-time, abrindo as
portas a várias hipóteses muito interessantes, como por exemplo, a criação de determinados elementos
baseados em tipos COM sem que essa informação tenha sido disponibilizada aquando da geração da
nossa aplicação.

Onde estamos?

Bem, por hoje é tudo. Como é possível verificar, a utilização de componentes COM a partir de aplicações
.Net não oferece muitas dificuldades (isto apesar e haver alguns aspectos que, por falta de tempo, não
referi). No próximo artigo, vamos falar acerca da construção de aplicações .Net para serem consumidas
através de COM.

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

Programação COM em .Net - parte II


Nível: Iniciado/Médio
Conteúdo: Introdução à programação COM em .Net
Ferramentas: Visual Studio 2003
Linguagem: C#

No último artigo efectuámos uma introdução à programação COM. Como foi possível averiguar, a
utilização de objectos COM a partir de .Net não é muito complicada. Claro que, se estivermos a falar de
projectos complexos, a utilização correcta desses objectos não é tão simples como deveria ser...

Hoje vamos terminar a abordagem iniciada no primeiro artigo desta série e vamos falar acerca dos
seguintes tópicos:
← Utilização de objectos COM;
← Libertação de recursos: problemas oriundos da utilização do Garbadge Collector em .Net;
← Apartments;

Utilização de objectos COM

Os leitores do primeiro artigo desta série deverão ter ficado com a ideia de que a construção de objectos
COM é baseada em interfaces. Por outras palavras, quando queremos construir um objecto COM,
devemos expor as funcionalidades desse objecto através de interfaces. Esses interfaces definem um
contracto entre o objecto COM e eventuais clientes que venham a utilizar esses componentes.

Na prática podemos definir dois tipos principais de interfaces: os interfaces VTBL (V-Table) e os
interfaces IDispatch. Existe ainda um outro tipo (misto) de interface que costuma ser utilizado e que é
designado de dual. Mas então qual será a diferença entre estes tipos de interfaces? Bem, para
percebermos o papel de cada um destes interfaces, nada melhor do que uma análise às principais
características de cada um deles. Comecemos então pelos interfaces VTBL.

Interfaces VTBL

Os componentes que suportam este tipo de interfaces permitem o chamado early binding. Os objectos
que possuem este tipo de interfaces não podem ser utilizados por clientes script, devido ao facto de o
código script ser interpretado e não compilado. O binding pode ser visto como sendo o processo de
relacionar a função que está a ser evocada com o código que implementa essa função. Quando estamos
a falar de early binding, estamos a falar de um processo que ocorre aquando da compilação. Nessa
altura, todas as referências aos nomes das funções são substituídas pelos respectivos endereços onde se
encontram as instruções dessa função.

Os componentes COM organizam os endereços das funções de acordo com uma tabela de apontadores,
designada de V-Table. Uma V-Table não é mais do que um array de endereços de memória em que cada
entrada representa um método que indica o endereço onde se encontra o código relativo a esse método.
Quando obtemos uma referência a um objecto COM, (podemos) também obter um apontador que nos
permite aceder à V-Table desse objecto e assim podemos evocar os métodos pretendidos (claro que na
prática deixamos esse tipo de coisas para o compilador ;) ). Uma vez que todos os componentes COM
têm de herdar obrigatoriamente do interface IUnknonw, então podemos afirmar que as três primeiras
entradas numa V-Table serão sempre referentes aos três métodos definidos por este interface. Por
exemplo, quando precisamos de evocar o método AddRef, o cliente já sabe a posição desse método na
V-Table do componente (devido ao facto de todos os componentes terem o IUnknown como interface de
topo) e por isso pode evocar rapidamente esse método.

Como é possível aferir a partir da discussão anterior, se necessitarmos de evocar uma determinada
propriedade ou método personalizado (isto é, definido pelo programador do componente através de um
interface personalizado), só o poderemos fazer se tivermos conhecimento do V-Table desse componente.
Portanto, necessitamos deste tipo de informação quando estamos a compilar a nossa aplicação. Ora
daqui decorre aquele que é o único problema da utilização deste tipo de interfaces: necessidade de
compilação (que, como é sabido, não está ao alcance de todos os clientes).

Interfaces IDispatch

Para além do IUnknow, o COM também define o interface IDispatch. Os componentes que implementam
este interface são conhecidos como dispinterfaces. Como referimos no artigo anterior, este interface
apresenta dois métodos muito importantes:
← GetIDsOfNames: permite converter um nome de um método ou propriedade no seu ID, de
forma a que seja possível invocar esse elemento;
← Invoke: utilizado para evocar um membro de uma classe, utilizando sempre o ID obtido através
do método descrito no item anterior.

A utilização deste interface como forma de expor um conjunto de métodos/propriedade oferece algumas
vantagens. Por exemplo, não é necessário conhecer antecipadamente a V-Table do objecto para utilizar
o componente. Este é o esquema utilizado pelos clientes script.

Quando definimos um interface do tipo IDispatch, temos de atribuir um Id a cada membro por forma a
identificar cada elemento implementado pelo componente. Estes ids eram conhecidos por dispids, e são
fundamentais para identificarem cada membro do componente. Do ponto de vista do cliente, sempre
que é necessário evocar um membro do componente é necessário:
1. Obter o dispid do membro, utilizando para tal o método GetIDsofNames;
2. Evocar o método/propriedade utilizando a função Invoke do interface IDispatch.

Nota: como é óbvio, os clientes script continuam a utilizar o V-Table do componente; contudo, neste
caso, apenas podem aceder à V-Table relativa ao interface IDispatch, sendo o acesso a todos os outros
métodos feitos através da evocação do par GetIDsOfNames/Invoke.

Interfaces Dual

Esta é uma aproximação que, supostamente, agrada a "gregos e a troianos". Com este tipo de interface
possibilitamos a utilização do componente através de early binding, caso o cliente suporte esta opção,
ou então através de late binding, nos restantes casos. Neste caso, o nosso interface deriva de IDispatch
temos de anotar o IDL com o atributo dual e anotar os métodos do componente com o atributo dispid ( o
sample que acompanha este artigo utiliza esta estratégia).

Interfaces VTBL vs IDispatch puros

Ambos os casos apresentam vantagens e desvantagens. Os interfaces VTBL apresentam um grande


handicap nos dias que correm: não podem ser executados por clientes script. Aliás, esta é grande
motivação para construir interfaces IDispatch. Por outro lado, apenas os interfaces VBTL conseguem
utilizar todos os tipos existentes a nível do IDL, uma vez que os interfaces utilizados a nível de clientes
de script (ou seja, os dispinterfaces) estão limitados a tipos que possam ser representados através de
um VARIANT. Para além disto, os objectos que contém interfaces VTBL podem conter vários interfaces,
ao contrário do que sucede com os interfaces IDispatch (tecnicamente é possível conter vários interfaces
IDispatch; contudo, os clientes script apenas poderão aceder a um desses interfaces, não conseguindo
"navegar" para outro interface como acontece no caso de clientes que suportam o early bindging).

O sample que acompanha este artigo contém um objecto que implementa um interface dual. Este
interface é muito simples e foi construído como dual para demonstrar a diferença a nível de evocação
dos métodos.

Utilização de objectos COM

Como descrevi no último artigo, antes de utilizarmos um objecto COM temos de proceder à criação dos
RCW (Runtime Callable Wrappers). Isto pode ser feito através do Visual Studio ou, alternativamente,
através do tlbimp. No caso de estarmos a trabalhar com interfaces do tipo dual ou VTBL, a utilização é
bastante simples; contudo, no caso dos IDispatch puros torna-se necessário recorrer a métodos da
classe Type.

O código que acompanha este artigo contém duas samples: VTBLBind e DispatchBind. Em cada uma
delas é possível observar os passos necessários à criação dos tipos utilizando ambos os tipos de binding.
Antes de explicarmos os pormenores de cada um dos projectos, vamos definir o nosso componente,
utilizando para tal o IDL:

// TestProject.idl : IDL source for TestProject


//

// This file will be processed by the MIDL tool to


// produce the type library (TestProject.tlb) and marshalling code.

import "oaidl.idl";
import "ocidl.idl";

[
object,
uuid(BF9137C8-604E-46BD-BF65-B9CC370AA1A8),
dual,
nonextensible,
helpstring("ICarDual Interface"),
pointer_default(unique)
]
interface ICarDual : IDispatch{
[propget, id(1), helpstring("Gets or sets the top speed")] HRESULT TopSpeed([out, retval] LONG*
pVal);
[propput, id(1), helpstring("Gets or sets the top speed")] HRESULT TopSpeed([in] LONG newVal);
[propget, id(2), helpstring("Gets or sets the brand of the car")] HRESULT Brand([out, retval] BSTR*
pVal);
[propput, id(2), helpstring("Gets or sets the brand of the car")] HRESULT Brand([in] BSTR newVal);
[propget, id(3), helpstring("Gets the current speed of the car")] HRESULT CurrentSpeed([out, retval]
LONG* pVal);
[id(4), helpstring("Reduce the speed to the indicated value")] HRESULT Break([in] LONG speed);
[id(5), helpstring("Speeds up the car to the correct speed")] HRESULT SpeedUp([in] LONG newSpeed);
};

[
uuid(C58F3269-B512-4345-80C3-F064C4390CBE),
version(1.0),
helpstring("TestProject 1.0 Type Library")
]
library TestProjectLib
{
importlib("stdole2.tlb");
[
uuid(564733F8-D181-4E35-A9DF-FA4A6DD00EF2),
helpstring("CarDual Class")
]
coclass CarDual
{
[default] interface ICarDual;
};

};

Como é possível verificar, o componente implementa apenas um interface, do tipo dual. Este interface
apenas contém duas propriedades de leitura e escrita (TopSpeed e Brand), uma propriedade de leitura
(CurrentSpeed) e dois métodos (Break e SpeedUp).

Como é possível verificar através do código da sample VTBLBind, o acesso a este objecto através de
early binding é feito de forma praticamente transparente. Todas as particularidades do COM estão
escondidas pelo RCW e até parece que estamos a trabalhar em VB6 ou em C++ com (smart pointers).
Contudo, a situação muda radicalmente quando utilizamos o componente através do interface IDispatch.
Ao contrário do que acontece com o VB6, em que criamos o objecto com a função CreateObject e, em
seguida, trabalhamos normalmente com o objecto (ou seja, acedemos às suas propriedades como se
tivéssemos utilizado early binding), em .Net temos de recorrer aos métodos contidos nas classes
Type/Activator e que nos permitem evocar os vários métodos/propriedades desse tipo de elementos,

Apenas um aparte para indicar que, apesar de tudo, esta situação é bem mais agradável do que quando
temos de aceder a IDispatch puros a partir de C++.

Utilização do interface IDispatch "puro"


Bem, a primeira pergunta que se coloca é: como criamos um objecto que expõe um interface IDispatch?
Antigamente teríamos de utilizar o CoCreateInstance (ou uma variante deste método) se estivéssemos a
utilizar o C++ ou então o CreateObject, se o projecto fosse construído em VB. Em .Net, a forma de
proceder à criação destes componentes consiste em utilizar alguns métodos da classe Type e Activator.
Uma vez que existem várias operações que são repetidas quando trabalhamos com este tipo de
interfaces, optei por construir um wrapper em torno dessas mesmas operações. É essa a principal
função da classe LAInteropUtils. Esta classe contém métodos que permitem obter/modificar valores de
propriedades, evocar métodos, criar instâncias, etc.

Comecemos então pela criação. O método LAInteropUtils.CreateObject contém o seguinte código:

public static object CreateObject( string progId )


{
Type type = Type.GetTypeFromProgID( progId, true );
return Activator.CreateInstance( type );
}

Este método começa por tentar obter o tipo representado pelo ProgId e em seguida tenta criar uma
instância desse tipo. Até aqui nada de novo. As novidades surgem apenas na evocação das propriedades
e métodos (especialmente para os programadores oriundos do VB). Existem vários métodos utilizados
para encapsular este tipo de acesso. Assim, quando necessitamos de obter o valor de uma propriedade,
devemos recorrer ao método GetPropertyValue. Para obtermos a velocidade actual podemos utilizar o
seguinte código:

int aux = Convert.ToInt32( LAUtils.LAInteropUtils.GetPropertyValue( _car, "CurrentSpeed" ) );

Por sua vez, o método GetPropertyValue (ou melhor, um dos seus overloads) contém as seguintes
instruções:

public static object GetPropertyValue( object caller, string propertyName, object [] vals )
{
return AuxiliaryInvokeMethod( caller, propertyName, BindingFlags.GetProperty, vals );
}
private static object AuxiliaryInvokeMethod( object caller, string memberInfo, BindingFlags flags, object
[] parameters )
{
Type aux = caller.GetType();
return aux.InvokeMember( memberInfo, flags, null, caller, parameters);
}

O método GetPropertyValue recorre ao método AuxiliaryInvokeMethod para efectuar os cálculos


necessários à obtenção do valor da propriedade. O grande responsável pelo trabalho de obtenção do
valor da propriedade é o método InvokeMember da classe Type. Este método (que também pode ser
utilizado sobre tipos .Net) permite definir o nome e o tipo de membro que está a ser evocado (primeiro e
segundo parâmetros).

Apesar de tudo isto parecer muito estranho para obter apenas o valor de uma propriedade
(especialmente para todos aqueles que sempre utilizaram VB), não é mais do que o procedimento
normal para acesso a propriedades definidas através de um dispinterface. Aliás, aqueles que
costumavam utilizar o C++ para aceder a este tipo de interfaces devem estar bastante contentes com as
facilidades fornecida pelos membros das classes Type/Activator (bem, pelo menos eu estou muito
contente com estes métodos! :) ). Os métodos contidos na classe LAInteropUtils tentam encapsular
todos os pormenores relativos à utilização deste tipo de interfaces a partir de .Net, pelo que se
redirecciona o leitor para uma consulta desta classe de forma a obter mais pormenores.

Libertação de recursos

Apesar de todas as facilidades fornecidas pela framework para integração de objectos COM, existe um
pequeno grande problema na utilização deste tipo de objectos. Estou a referir-me ao facto de
estarmos a integrar duas tecnologias com diferentes tipos de finalização. Como é do conhecimento geral,
os objectos .Net não são determinísticos. Logo não podemos prever a altura em que é feita a destruição
de instâncias dessas classes. Já os componentes COM necessitam de uma gestão determinística, em que
o tempo de vida do elemento é determinado por um contador interno que é incrementado sempre que o
utilizador evoca o mótodo AddRef e decrementado sempre que o utilizado chama a função Release. É da
responsabilidade do programador utilizar correctamente estas funções.

É nesta diferença que residem os principais problemas de Interop entre .Net e COM. Para percebermos
melhor a razão de tais problemas, temos de, em primeiro lugar, perceber o funcionamento interno dos
RCW e compará-los com o funcionamento dos objectos COM. Começando pelos objectos COM, podemos
afirmar que sempre que passamos uma referência (por exemplo, através do operador =) devemos
sempre incrementar o contador interno do objecto. Obviamente devemos decrementar essa referência
sempre que já não necessitamos desse componente. Este tipo de operações é feito de forma automática
se estivermos a utilizar o VB ou os smart pointers (em C++).

Por outro lado, os RCW efectuam a gestão (interna) de um elemento COM de forma diferente. Ao
contrário do que acontece normalmente, os RCW mantém apenas uma única referência ao objecto COM
(ou seja, o contador interno deste objecto está sempre igual a um) independentemente do número de
referências efectuadas ao objecto RCW. Por outras palavras, código deste tipo:

RCWRef myVar = obter_referencia_para_Wrapper;


RCWRef myVar2 = myVar;

Resulta sempre numa contagem do objecto COM contido no wrapper igual a um. Então como é que o GC
sabe que pode destruir o wrapper e, por sua vez, o objecto COM que está no seu interior? Simples: o GC
efectua a sua gestão normalmente de acordo com as suas regras relativas à limpeza de memória. Por
outras palavras, o wrapper está sujeito às mesmas regras de uma instância de uma classe definida
em .Net. Se necessitarmos de libertar um objecto COM de forma explicita, podemos recorrer ao método
ReleaseComObject da classe Marshal . A evocação deste método irá libertar todas as referências
relativas a este objecto COM. Ao executarmos este método, obtemos um número inteiro que nos
permite averiguar acerca da existência de uma outra contagem interna. O objecto COM apenas será
eliminado quando este método (ReleaseComObject) retornar zero. Mas então que valor é este retornado
pelo objecto método ReleaseComObject? Ao que consta, está relacionado com uma contagem interna
efectuada pelo wrapper que indica o número de vezes que foi feito o marshalling da referência COM
contida no wrapper. Portanto pode ser necessário evocá-lo várias vezes até que o objecto seja
eliminado. Se quisermos, podemos construir uma rotina genérica que liberta qualquer objecto COM:
basta evocar sucessivamente o método ReleaseComObject até que o valor retornado seja zero - este
tipo de rotina está encapsulado no método ReleaseComObjecto da classe LAInteropUtils. Até hoje
continuo sem perceber porque é que a Microsoft não nos forneceu uma rotina deste género...

Só uma nota final: após utilizarmos a rotina LAInteropUtils.ReleaseComObject, libertamos os recursos


COM, fazendo com que eventuais acessos aos wrappers gerem excepções. Por exemplo:

RCWRef myVar = obter_referencia_para_Wrapper;


RCWRef myVar2 = myVar;
... //codio aqui
LAInteropUtils.ReleaseComObject( myVar );
myVar2.Test = "UUU"; //excepção Gerada aqui!!!!

No excerto anterior geramos uma excepção se tentarmos aceder à variável myVar2 após evocarmos o
método LAInteropUtils.ReleaseComObject. Portanto, quando desenvolvemos uma aplicação complexa há
que ter muito muito cuidado com a libertação dos recursos COM. Este tipo de código é, na minha
opinião, um regresso ao passado! Há quem pense que podemos tornar este código mais object oriented
de acordo com as regras da framework. Para tal basta construir um wrapper (em torno do RCW) que iria
implementar o interface IDisposable. Bem, actualmente todas as minhas soluções relacionadas com COM
utilizam um conjunto de métodos muito semelhantes aos que apresento no sample e, até hoje, tudo tem
funcionado bem. A constante utilização do método LAInteropUtils.ReleaseComObject espalhado ao longo
do código é um mal com o qual tenho convivido nestes últimos tempos...enfim, nada a fazer se quiser
continuar a utilizar o C# (o que não será por muito tempo, pois o pouco que vi de managed C++ já me
convenceu a passar a utilizar essa linguagem).

É devido a estes factores que ambas as samples que utilizam o objecto COM contém chamadas ao
método LAInteropUtils.ReleaseComObject por forma a proceder às necessárias operações de limpeza. Só
para terminar esta secção, gostaria de redireccionar o leitor para uma discussão muito interessante sobre
o porquê da não implementação de wrappers que implementassem o padrão IDisposable que se
encontra no blog do Sam Gentile.
Utilização de Apartments

A utilização de componentes COM em ambientes multithreading foi, desde que me consigo lembrar,
motivo de grande preocupação entre os programadores. A escrita destas linhas já me trouxe
recordações (algo desagradáveis) do único componente COM multithreaded que fiz até hoje. Se bem me
recordo, as minhas palavras finais após a conclusão do dito cujo foram: ".*$*!$**...nunca mais".
Presumo que isto resume, de uma forma mais ou menos sintetizada, o gozo que obtive na construção
desse componente ;).

Contudo, a utilização de componentes COM neste tipo de ambientes (multithreaded) segue um conjunto
de regras bem definidas. A colocação de um componente num determinado apartment (se não sabem o
que é um apartment, não se preocupem que já vou tentar explicar este conceito) depende dum conjunto
de aspectos definidos pelo próprio objecto COM e pelo cliente que acede a esse objecto.

Para os que já não estão recordados, quando utilizamos um componente COM temos sempre de
inicializar a thread onde o componente irá ser utilizado. Para tal tínhamos de recorrer ao método
CoInitialize(Ex), algo que, para variar era feito automaticamente em VB6. Por outro lado, os
componentes COM também definiam o tipo de acesso a que pretendiam estar sujeitos, utilizando para
isso um conjunto de entradas no registry (ou, no caso de estamos a falar de componentes EXE,
utilizando também o método CoInitialize(Ex); contudo, para simplificar, vamos falar apenas de
servidores dll até ao final do artigo).

Quando um objecto COM é inicializado na aplicação cliente, ambas as regras definidas pelo cliente
(através da inicialização da thread feita com o método CoInitialize(Ex)) e pelo próprio componente são
comparadas; se estas forem diferentes, então o cliente e servidor COM ficarão em apartments
diferentes, sendo a comunicação entre eles efectuada através de um proxy (o que introduz a
necessidade de efectuar o chamado marshaling). Caso contrário (quando estão no mesmo apartment), a
comunicação entre o cliente e o servidor é feita de forma directa, sem necessidade de utilizar
intermediários. A beleza desta solução reside no facto de não ser necessário ao cliente ter conhecimento
prévio sobre as características do componente COM (e vice-versa).

Mas então o que é essa coisa chamada de apartment? Provavelmente a forma mais fácil de perceber o
conceito consiste em considerar um apartment como sendo um contentor lógico situado no interior de
um processo utilizado por componentes COM que têm determinados requisitos quanto ao nível de acesso
por parte das várias threads (que podem existir dentro de um processo). É importante não esquecer que
um objecto é colocado num apartment durante a sua criação e aí irá residir até que seja destruído.

O COM permite a colocação de um objecto num de três apartments:


← STA (Single threaded apartment): neste caso, a framework COM garante que o acesso aos
componentes colocados neste tipo de apartment nunca será efectuado por mais do que uma
thread (ou seja, nunca iremos ter várias threads a acederem simultaneamente a um método de
um componente); o acesso a este tipo de componentes é sempre feito por intermédio da thread
que inicializou o apartment. Devido a isto é normal afirmar-se que os componentes STA têm
thread-affinity. Um processo pode ter vários apartments deste tipo (para isso basta criar várias
threads e inicializá-las de acordo com este tipo de apartment -mais detalhes à frente). Antes de
passarmos às MTAs, falta referir que um apartment STA pode conter vários componentes COM
deste tipo (para tal, basta criar vários objectos COM numa thread que tenha sido inicializada
para este tipo de apartment);
← MTA( Multi-threaded apartment): como o próprio nome indica, os componentes colocados neste
tipo de apartment podem ser acedidos por várias threads ao mesmo tempo. Devido a isto, o
programador do componente tem de efectuar o sincronismo no acesso aos dados internos do
componente (utilizando para tal as várias primitivas de sincronização existentes). Um processo
tem, no máximo, um apartment deste tipo onde residem todos os objectos que sejam
concebidos para este tipo de apartment.
← TNA(Thread Neutral apartment): este é o último tipo de apartment. Ao contrário do que
acontece com os dois tipos anteriores, este apartment não "possui" nenhuma thread. O acesso
a este tipo de objectos é sempre feito através de um proxy especial. A vantagem deste
apartment reside no facto de as chamadas a métodos deste componente não requererem o
chamado thread switch, sendo por isso muito mais rápidas do que quando temos de efectuar a
comunicação com um componente que está num apartment diferente da thread "cliente". Por
outras palavras, este tipo de componentes será sempre evocado na thread "cliente" (se
estivermos a chamar um método a partir de uma thread STA, então essa evocação será
efectuada nessa thread STA; se estivermos a evocar o método a partir de uma thread MTA,
então essa evocação será efectuada nessa mesma thread MTA).

Agora só falta mesmo explicitarmos como é que o cliente e o servidor COM definem o apartment que
querem utilizar! Vamos então começar pelo cliente...
Definição de uma apartment do cliente

Como referimos acima, um cliente define o tipo de apartment que deseja criar através do método
CoInitialize(Ex). O método CoInitializeEx aceita um parâmetro que define o tipo de apartment:
← COINIT_APARTMENTTHREADED: neste caso, a thread irá ser associada a um apartment do tipo
STA.
← COINIT_MULTITHREADED: neste caso, a thread está a inicializar (ou a juntar-se) a um
apartment MTA.

Portanto, um cliente antes de aceder a um objecto COM tem de evocar o método CoInitialize(Ex)
indicando qual o tipo de apartment ao qual deseja associar a thread. É importante referir que o método
CoInitialize tem o mesmo comportamento do método CoInitializeEx( NULL,
COINIT_APARTMENTTHREADED).

Definição do apartment feita pelo componente COM

A escolha do apartment é feita através de uma chave do registry situada em CLSID\InprocServer32


designada de ThreadingModel. Ao contrário do que o leitor já deve estar a suspeitar, podemos atribuir
cinco (e não três!) valores a esta entrada:
← Single: neste caso o componente deve ser colocado na primeira STA criada pelo processo; este
valor era utilizado pelos primeiros componentes desenvolvidos em VB.
← Apartment: componentes devem ser colocados obrigatoriamente numa STA.
← Free: componentes devem residir na MTA do processo;
← Both: ao contrário do que o nome indica, o componente deve residir no apartment associado à
thread onde ele foi criado; provavelmente a Microsoft deveria ter utilizado o nome Either para
este tipo de componente.
← Neutral: componente deve estar associado ao apartment NTA.

Apartments em .Net

Na prática as nossas aplicações .Net não são mais do que clientes dos componentes COM. Devido a isso
regem-se pelas mesmas regras definidas na secção sobre apartments no cliente. Aliás, os mais atentos
devem ter reparados que as aplicações Windows Forms geralmente contém código semelhante ao
seguinte:

[STAThread]
static void Main()
{
Application.Run(new Form1());
}

Neste caso, a thread está a ser associada a um apartment do tipo STA. Bem, de certo que não será
nenhuma surpresa se eu afirmar que também existe um atributo MTAThreadAttribute, que associa uma
thread ao apartment MTA do processo. De referir que, em aplicações .Net podemos aceder a uma
threading pool (por exemplo, quando utilizamos um delegate) ou então, podemos criar uma thread
explicitamente através da classe Thread. Sem me querer alongar sobre este assunto, quero apenas
referir que apenas quando criamos uma thread de forma explicita podemos controlar o tipo de
apartment que será associado à thread. Se utilizarmos uma thread proveniente da thread pool, então
não teremos hipótese de configurar o apartment. Essa thread estará, por defeito, sempre associada à
MTA do processo.

Como é óbvio, a definição do apartment é apenas utilizada quando utilizamos componentes COM nessa
thread.

Conclusões finais

Com este artigo aprofundámos os nossos conhecimentos sobre o Interop entre .Net e COM. Falámos
sobre três tópicos importantes:
← tipos de interfaces e respectiva evocação de métodos a partir de .Net: como foi possível
observar, a evocação de métodos contidos em interfaces VTBL não oferece nenhum tipo de
dificuldade; o mesmo não se pode dizer em relação aos métodos oriundos de um dispinterface
puro.
← Limitações decorrentes da forma como funciona o garbadge collection em .Net: .Net e COM tem
filosofias muito diferentes no que diz respeito à finalização; apesar de não ser tão fácil como
devia (isto é só a minha opinião!), a framework .Net permite-nos terminar explicitamente a
utilização de um objecto COM, libertando assim os recursos utilizados por esse objecto.
← Apartments: esta foi sempre uma das áreas mais complicadas para os programadores COM. Foi
sempre uma área rodeada de imensos rumores, muitos dos quais sem fundamento!

Antes de concluir, só (mais) uma observação: o leitor pode ter ficado com a ideia de que a construção
de componentes MTA resolve todos os problemas existentes a nível de performance. Tal não é verdade!
É preciso não esquecer que se o cliente definir a sua thread como STA e o nosso componente for
configurado como MTA introduzimos sempre o chamado thread switch quando o cliente evocar um
método no componente. Provavelmente neste caso obteríamos melhores resultados se o componente
fosse configurado para ser utilizado numa STA (porque neste caso iria ficar no mesmo apartment do que
o cliente, eliminando-se assim o thread switch e a utilização de proxy!)

Onde estamos?

Bem, por hoje é tudo. Apesar de no artigo anterior ter prometido que neste iríamos abordar aspectos
relacionados com a construção de componentes .Net para serem utilizados em aplicação COM, achei por
bem aprofundar um pouco mais os aspectos relacionados com a utilização de componentes COM
em .Net, ficando assim a construção de componentes .Net para utilização em aplicações COM para um
próximo artigo.

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

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

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:

1. Construir o componente .Net;


2. Criar uma type library (opcional);
3. Atribuir um strong name ao assembly;
4. 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

Programação COM em .Net - parte V

Autor: Luís Abreu


Conteúdo: Continuação do último item da série e apresentação de várias ideias relativas ao desenvolvimento de controlos ActiveX
Ferramentas: Visual Studio 2003

Na sequência desta saga (que têm sido estes artigos sobre COM), cá estou eu para uno novo episódio. Hoje o "prato do dia" consiste em
aprofundarmos alguns conceitos fundamentais ao desenvolvimento de componentes .Net para serem consumidos em COM . Este artigo irá
apresentar alguns aspectos importantes que nos permitirão construir um ActiveX utilizando C# (algo que será feito no próximo artigo da
série). Bom, o caminho é longo, pelo que é melhor avançarmos...

Exportação de tipos

Como vimos no último artigo, a utilização de um componente .Net numa aplicação COM é feita através de um wrapper. Esse wrapper serve
de ponte entre a aplicação COM e o componente .Net. Como seria de esperar, o interface do wrapper depende do interface definifo pelo
componente .Net. Portanto, para construirmos componentes COm através de .Net é necessário termos pelo menos noções sobre as
transformações que são efectuadas e encapsuladas no wrapper.

Em COM os tipos estão descritos numa type library. As type libs não são mais do que ficheiros que contémuma descrição (mais ou menos)
completa dos vários interfaces/objectos contidos numa dll com. Por outro lado, em .Net a definição dos tipos está contida no próprio
assembly. Por isso não deve ser supresa para ninguém, se eu disser que um assembly tem de ser convertidonuma type lib para ser
consumido por um objecto COM (como vimos no último artigo, este tipo de transformação pode ser feito através da ferramenta regasm).
Uma type library é identificada através de três itens: GUID também conhecido por Library Id ou LIBID), um LCID (Locale Identifier - este
elemento é opcional) e versão. Estes valores são obtidos das definições existentes no assembly. Assim, o LIBID é obtido através da
combinação do nome do assembly e da chave pública deste. Podemos personalizar este valor através aplicando oatributo GuidAttribute ao
assembly.

Como é óbvio, a versão da type lib é obtida a partir da versão definida no assembly ( a única observação relevante aqui prende-se com o
facto de apenas serem aproveitas os valores referentes às partes major e minor da versão - ex.: se o nosso assembly possuir a versão
1.1.2.3, então a type lib será anotada com a versão 1.1). Finalmente, o LCID, que é opcional, também é obtido a partir do assembly, caso
este contenha essa informação. Se tal não acontecer, então será utilizado o LCID 0.

A exportação de namespaces poderá trazer alguma surpresa devido ao facto deste tipo de exportação...simplesmente não existir! Por
outras palavras, se um tipo estiver contido no interior de um namespace, apenas o nome do tipo será importado para a type lib (ex.: o tipo
namespace.class será apenas exportado como class para a type lib).

Por defeito, todas as classes públicas são exportadas para a type lib sob a forma de coclasses. Estas serão (como sempre acontece no
COM) anotadas com um determinado GUID. Todas as classes abstractas e/ou sem construtor por defeito público serão também anotadas
com o atributo noncreatable (de forma a que se saiba que estas classes não devem ser criadas). Eventuais heranças existentes não serão
propagas para a type lib pois não são suportadas a nível das coclasses. Assim, as coclasses irão expor todos os interfaces por si
implementados. Para além disso, no caso das heranças de classes, uma classe irá também expor eventuais interfaces implementadas pela
classe base (se não houver interfaces explicitos, então será gerado um que irá conter todos os métodos/propriedades públicas - mais
informações sobre esta geração automática podem ser encontradas no artigo anterior).

A exportação de interfaces é feita de forma (mais ou menos) transparente. Com isto quero dizer que os interfaces são transformados em
interfaces COM na type lib. É possível controlar o tipo de interface através do atributo ComInterfaceType (que foi apresentado no último
artigo). A herança de interfaces é exportada de forma ligeiramente diferente. Por exemplo, suponhamos o seguinte caso:

namespace CarNetLib
{

interface A
{

}
inteface B: A
{
}
[ClassInterface(ClassInterfaceType.None)]
public class Final: B
{
}
}

Ao contrário do que se possa pensar, este tipo de hierarqui irá ser traduzido no seguinte IDL:

[
....//atributos idl removidos
]
interface A:IDispatch
{
}

[
....//atributos idl removidos
]
interface B:IDispatch
{
}

coclass Final
{
interface _Object;
[default] interface IDerived;
interface IBase;
}

A explicação para este tipo de comportamento prende-se com o facto do COM não suportar o conceito de hierarquia em runtime (ainda
estão lembrados o QueryInterface???). Mesmo quando temos uma herança a nível do IDL, essa herança não tem o mesmo significado que
geralmente lhe é associado nas linguagens orientadas a objectos.

A exportação de métodos segue um principio de funcionamento mais lógico. Assim, todos os parâmetros passados por valor são exportados
como parâmetros de input (anotados na type lib com o valor in). Por outro lado, se os parâmetros forem passados como referência, então
serão transformados em parâmetros in, out. O mesmo acontece com os apontadores (que continuam a ser utilizados em unsafe C# ou em
Managed C++ - ou, se preferirem, C++/CLI na nova versão da framework).

Convém recordar que o tipo de retorno de um método (em COM) é sempre dado através do tipo HRESULT. Assim sendo, será necessário
efectuar uma conversão de forma a que um método .Net que retorna um determinado valor possa ser exportado para uma type lib. A
solução para este problema é simples (e não é novidade para os programadores de C++). O valor de retorno passa a ser definido como
parâmetro do método em causa e é anotado com o valor out, retval de forma a indicar que esse valor contém o o resultado do
processamento do método. Por exemplo:

//NET
long Process();

//COM
HRESULT Process( [out, retval]int64* aux );
É devido a esta anotação que é possível trabalhar em VB com métodos pertencentes a um objecto COM utilizando-os como se estes
devolvessem um valor dum tipo diferente de HRESULT. Claro que o framework irá ter de modificar o código do wrapper. Assim, se tudo
correr bem, o wrapper automaticamente retorna S_OK (um dos muitos HRESULT pré-definidos que é utilizado para indicar sucesso). Se
houver uma excepção, então esta será mapeada num HRESULT que será utilizado como tipo de retorno.

Para os interessados, existe aqui uma tabela com o mapeamento entre as excepções e os respectivos HRESULTs. Se for necessário,
podemos criar os nossos próprios HRESULT e associá-los a uma eventual excepção. Para tal, basta associarmos o valor por pretendido à
propriedade protegida (protected) HResult definida na classe Exception (que, como é sabido, serve de base a todas as excepções). A
criação de um valor adequado para ser utilizado como HRESULT requer alguns cuidados. Todos os HRESULTS não originários da Microsoft
não podem começar com o valor 0x8004 e devem possuir um valor superior a 0x200 (num próximo artigo iremos abordar melhor este
tema).

Nota: os HRESULTs personalizados podem ser reutilizados em interfaces diferentes.


Nesse caso, o significado de cada HRESULT depende do interface em que foi obtido o
erro.

As propriedades públicas também são exportadas. Nestes casos, temos:

← acessor get de uma propriedade é transformado num método anotado com o atributo idl propget;

← acessor set de uma propriedade é convertido num método anotado com o atributo idl propput;

← se o tipo de propriedade for um interface ou uma classe, então o método associado ao set será anotado com o idl proputref.

Nota: a utilização do atributo proputref permite ao editor do VB tratar a


propriedade como objecto (ou seja, temos de utilizar a
propriedade em conjunto com o famoso Set).

Os tipos structs são exportados para o seu análogo em idl. Por outras palavras, uma estrutura será sempre transformado numa struct idl.
Convém chamar a atenção para o facto de poder ser necessário fixar a definição da estrutura através do atributo LayouKindAttribute.
Finalmente, falta referir ainda que as enumerações .Net são transformadas em...(já sei, advinharam!!!) enum na type lib correspondente.
Após esta descrição pormenorizada, nada melhor do que passarmos a um tópico muito interessante: definição e processamento de eventos.

Definição e processamento de eventos

Como é do conhecimento geral, em .Net os eventos são baseados na definição de delegates. Contudo, e como seria de esperar, o COM
recorre a uma aproximação diferente. Em COM os eventos são definidos através dos chamados connection points. Como não poderia deixar
de ser, este mecanismo assenta na definição de interfaces.

A ideia é simples: definimos um interface com um determinado número de métodos. Este interface, ao contrário do que acontece com os
interfaces normais (que são implementados pelo objecto servidor) será implementado pelo cliente. Claro que o cliente terá de sinalizar essa
implementação ao servidor. Se tal não acontecer, o servidor não sabe que esse cliente está interessado em ser notificado dos
acontecimentos descritos por esse interface. Na prática os eventos correspondem à evocação dos métodos definidos no interface. Esta
evocação é sempre iniciada pelo servidor e processada pelos cliente (ou seja, é como se ambos trocassem os papeis que estão a
desempenhar).

Este tipo de ligações é sempre definida através de duas partes: uma fonte, que efectua a evocação dos métodos definidos no interface (é
sempre o servidor); e um cliente, que implementa os métodos definidos nesse interface. Tecnicamente, é normal encontrarmos o termo
source ou connection point associado à fonte e o termo sink associado ao objecto que implementa o interface.

O cliente (sink) tem de indicar ao servidor (source) que está interessado em ser notificado desses eventos. Para tal, o cliente tem de
estabelecer uma ligação ao servidor utilizando para tal o interface IConnectionPoint (que, como é óbvio, tem de ser implementado pelo
servidor). Este interface contém a seguinte definição:

interface IConnectionPoint: IUnknown


{
HRESULT GetConnectionInterface( [out] IID* pIID );
HRESULT GetConnectionPointContainer( [out] IConnectionPointContainer** pp );
HRESULT Advise( [in] IUnknown* unkSink, [out]DWORD* cookie );
HRESULT Unadvise( [in]DWORD cookie);
HRESULT EnumConnections( [out]IEnumConnections* ppEnum );
}

Vamos lá então descrever estes métodos. O método GetConnectionInterface é utilizado por parte do cliente de forma a obter o IID do
interface associado ao connection point (este valor não é mais do que um GUID que é associado ao interface que contém os métodos
utilizados para notificar o cliente - por outras palavras, este método retorna o IID do interface que define os eventos). Como referi, o
cliente deve possuir uma implementação do interface identificado por este IID. Se o cliente desejar ser notificado de eventuais eventos,
então deverá indicar essa intenção ao objecto servidor, utilizando para tal o método Advise.

Ao evocar esse método, o cliente tem de enviar um apontador (IUnknow*) relativo ao objecto que implementa o sink interface e deve
guardar o valor proveniente do parâmetro cookie (que é um parâmetro de output). Este parâmetro identifica a ligação estabelecida entre o
cliente e o servidor e irá ser utilizado mais tarde, de forma a que o cliente seja capaz de indicar ao servidor que não está interessado em
receber mais notificações relativas a esse evento (este mecanismo de cookies é necessário pois um servidor pode notificar vários clientes).
Quando isso acontecer, então deverá evocar o método Unadvise e passar o cookie obtido aquando da evocação do método Advise.

O método EnumConnections permite obter uma enumeração de todas as ligações actuais mantidas por um connection point. Se o objecto
source necessitar de evocar um método do interface, tem de percorrer esta lista e evocar sucessivamente todos os métodos dos objectos
contidos nesta lista interna (portanto, através deste método obtemos uma lista de todos os clientes uqe desejam ser ntoficados acerca de
um determinado evento).

Apesar da elegância desta solução, ainda temos um problema para resolver: e se o objecto necessitar de possuir vários connections points?
A resposta a este problema vem sobre a forma de um novo interface: IConnectionPointContainer. Este interface apresenta os seguintes
métodos:

interface IConnectionPointContainer:IUnknow
{
HRESULT EnumConnectionPoints( [out]IEnumConnectionPoints** ppEnum);
HRESULT FindConnectionPoint( [in]REFIID riid, [out] IConnectionPoint** ppCP );
}

Com este tipo de solução, um cliente deve sempre começar por obter este interface. Só em seguida deverá tentar obter o connection point
desejado. O exemplo seguinte (em C++) apresenta um exemplo tipico da utilização destes interfaces (a partir do cliente ou sink):

CComPtr<IUnknown> src = //referencia ao IUnknown do objecto source


CComPtr<_IMyEvents> sink = //objecto que ira receber os eventos; neste caso
implementa o interface
// _IMyEvents (este interface foi definido algures e encapsula os metodos
// relativos aos eventos COM)

DWORD cookie; //utilizado para desligar a ligacao ao connection


CComPtr<IConnectionPointContainer> pointContainer;
HRESULT hr = src.QueryInterface( &pointContainer ); //obter o
IConnectionPointContainer

//metodo de verificacao de erro removido para simplificar o codigo


//agora vamos obter o connection point para o interface pretendido
CComPtr<IConnectionPoint> cnnPt;
pointContainer->FindConnectionPoint( __uuidof(_IMyEvents), &cnnPt );
//agora vamos estabelecer a ligacao
cnnPt->Advise( sink, &cookie );

//quando ja na quisermos ser notificados...


cnnPt->Unadvise( cookiew );

Vamos lá analisar o código. A classe CComPtr é um template que facilita a utilização de objectos COM. A vantagem da utilização desta
classe reside na automatização da contagem relativa à referência dos objectos COM (ainda estão lembrados do AddRef e do Release,
pertencentes ao interface IUnknown?) . O primeiro passo consiste em obtermos um apontador para o interface utilizado nas notificações
(no exemplo esse interface tem o nome de _IMyEvents). Este interface pode ser implementado pela coclasse cliente ou então pode ser
implementado por um objecto interno. Em seguida, começamos por pedir ao objecto servidor o interface IConnectionPointContainer. Como
vimos, se o servidor suportar eventos, então terá de implementar este interface (convém relembrar que em COM temos sempre de utilizar
o método QueryInterface para interrogar o objecto em relação a um interface - podem considerar isto como sendo um cast à COM).

O passo seguinte consiste em verificar se o objecto contém um connection point relativo ao interface em causa. Para tal, temos de passar o
ID do interface (ou melhor, o IID, que é obtido no exemplo à custa do __uuidof - isto é mais um dos artíficios do C++ :) ). Se tal for
verdade, então indicamos ao objecto servidor que estamos interessados em ser notificados, utilizando para tal o método Advise. Quando já
não estivermos interessados nas notificações, podemos recorrer ao método Unadvise quebrar a ligação. Este tipo de mecanismo é muito
utilizado pelos ActiveX (que será o alvo do próximo item da série) de forma a notificar eventuais clientes de determinadas situações.

Acreditem ou não, este é o mecanismo utilizado pelo COM. Sempre que colocam um ActiveX num formulário de VB (ou mesmo VB.Net e
C#) são efectuadas estas negociações. Ah, grande VB! Protegeu-nos durante anos e anos deste tipo de código :)

Felizmente para nós, a framework .Net implementa estes interfaces através dos wrappers gerados.

Implicações a nível de objectos .Net

Agora que já temos uma noção básica sobre o funcionamento de eventos em COM, convém referir quais as implicações deste mecanismo
na construção de componentes .Net.

Tipicamente um evento é definido da seguinte forma em .Net:

public event ChangedHandler OnChange;


Infelizmente estes eventos não podem ser facilmente consumidos por um objecto COM. O problema reside na forma como o evento irá ser
exportado. Durante a exportação para a type lib, o evento irá ser transformado em dois métodos (add_OnChange e remove_OnChange).
Ambos estes métodos recebem um parâmetro do tipo ChangedHandler. O problema aqui reside no facto deste ser o mecanismo utilizado
por objectos .Net e não por objectos COM! Para além disso, o parâmetro necessário tem ser um tipo managed! Devido a isso, não é
possível ao objecto COM criar este tipo e passá-lo aos métodos.

A solução para este problema é simples: consiste em criar um interface (que irá funcionar de forma semelhante aos interfaces dos eventos
em COM) e associar este interface ao evento utilizando para tal o atributo ComSourceInterfaces. O exemplo seguinte ilustra este principio:

//criar interface dos eventos


[
Guid( ... ),
InterfaceType(ComInterfaceType.IsDispatch)
]
public interface IMyEvents
{
void SomethingChanged(); //nome do metodo tem de ser igual ao nome do evento!
}

//definir o delegate
//com a mesma assinatura do metodo
delegate void ChangedDelegate();

//exportar a classe e expor o interface anterior como fonte de eventos


[
ComSourceInterfaces(typeof(IMyevents))]
]
class Test
{
public event ChangeDelegate SomethingChanged;

//definir um metodo geral que apenas dispara o evento


public void FireEvent()
{
OnChange();
}
}

Portanto, passos muito simples:

← definir um interface com o método que deve ser evocado (este método tem a mesma assinatura do delegate e deve ter o
mesmo nome do evento);
← utilizar o atributo ComSourceInterfaces para indicar que o evento deve ser exposto a objectos COM através do interface
indicado .

Se tivéssemos mais eventos, então deveríamos definir métodos adicionais (que estivessem de acordo com os vários delegates) de forma a
que os clientes COM consigam responder aos vários eventos. Alternativamente, poderíamos definir esses eventos em interfaces diferentes.
Actualmente podemos definir até quatro interfaces diferentes através do construtor da classe. Se desejarmos indicar mais interfaces,
teremos de recorrer ao construtor que recebe uma string (neste caso, separamos os nomes dos tipos utilizando o caracter vírgula). Muito
importante é dar o mesmo nome aos eventos e aos métodos associados aos interfaces que irão ser associados aos eventos COM.

Conclusões finais

Por hoje é tudo. Com o material discutido ao longo deste artigo já temos as bases necessárias à implementação de um componente COM
com interface: estou a dalar dos ActiveXs. Isso será algo que iremos fazer no próximo artigo da série. Por favor enviem-me as vossas
opiniões/sugestões/críticas/correcções para progC@netmadeira.com.Fiquem bem e boa programação!

Você também pode gostar