Article Details                  
 
Programação COM em .Net - parte I

Introdução à programação COM em .Net. Este artigo apresenta as principais diferenças entre estes tipos de tecnologias e apresenta conceitos relativos à importação de componentes COM para utilização em .Net.

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

short

int, long

hyper, int64, _int64

unsigner char, byte

wchar_t, unsigned short

unsigned int, unsigned long

unsigned hyper

float

double

VARIANT_BOOL

void*

HRESULT

SCODE

BSTR

LPSTR

LPWSTR

VARIANT

DECIMAL

DATE

GUID

CURRENCY

IUnknow*

IDispatch*

SAFEARRAY
System.Int32
System.SByte

System.Int16

System.Int32

System.Int64

System.Byte

System.UInt16

System.Int32

System.Int64

System.Single

System.Double

System.Boolean

System.IntPtr

System.IntPtr ou System.Int16

System.Int32

System.String

System.String

System.String

System.Object

System.Decimal

System.DateTime

System.Guid

System.Decimal

System.Object

System.Object

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




Written By: labreu
Date Posted: 4/14/2006
Number of Views: 492

Return