Trabajando con componentes COM desde .NET

Fecha: 13/Jul/2005 (12/07/2005)
Autor: Gerardo Contijoch - mazarin9@yahoo.com.ar

 


Me decidí a escribir este artículo por dos motivos. El primero es que necesitaba encontrar información sobre el tema y encontré bastante, pero en inglés. El segundo es que ninguno de los lugares donde encontré información resumía todo lo que necesitaba en un mismo lugar. Es por ello que creo que este artículo es una buena introducción a un tema que puede ser bastante complicado para un programador inexperto (o por lo menos inexperto en .NET). El mismo está pensado a modo de tutorial y no guía de referencia por lo que para sacarle el jugo sugiero que lo lean de principio a fin.
Una aclaración antes de empezar. A lo largo del artículo voy a usar los términos "aplicación" y "componente" indistintamente, es decir, cuando lean "aplicación", no necesariamente tiene que ser una aplicación ejecutable, puede ser una librería que es consumida por una aplicación, y cuando lean "componente" no significa que lo mismo no se aplique a un archivo ejecutable.

A lo largo del artículo voy a hacer referencia a herramientas que vienen con Visual Studio .NET o con el .NET Framework. Sugiero que antes de empezar a leer el artículo repasen la sección "Otras herramientas útiles" para asegurarse de conocer estas herramientas y como utilizarlas.

Nociones básicas

Existen dos formas de interacción entre un componente COM y otro .NET. La primera se da cuando desde un ensamblado .NET se hace referencia a un componente COM, la otra, mucho menos común, se da cuando necesitamos trabajar con un componente .NET desde una aplicación COM. Para ambos casos el Framework .NET nos provee de herramientas para crear wrappers (envoltorios) alrededor de los componentes, de manera que los ensamblados creados con .NET sean visibles para las aplicaciones COM y estas, sean tratadas como ensamblados .NET.

Todas las dlls que trabajan con el CLR (dlls con código administrado), son auto descriptivas, es decir, contienen su definición dentro de si mismas. Esta definición se encuentra en los metadatos. Antes de que llegara .NET, esta definición de la que hablamos se podía encontrar en archivos .tlb (Type Library) o embebidos en .dlls, .exes u .ocxs. Es decir, una librería COM puede tener esta definición en si misma o no (incluso esta definición no necesariamente tiene que ser correcta o estar completa) y por lo tanto no se puede decir que son auto descriptivas. Esto, sumando a otros factores, las hace incompatibles con el CLR. Es por ello que es necesario algún método para que el CLR pueda leer la información que necesita sin tener que depender de la información que se encuentra en la dll COM.

Trabajar con componentes COM desde .NET

Para resolver este problema es necesario generar un Runtime Callable Wrapper (RCW). Como su nombre lo indica, no es más que un wrapper o Proxy, el cual va a ser llamado (y generado) en tiempo de ejecución y que mediará entre el CLR y una librería COM, haciendo que esta última se vea como un ensamblado .NET. Para crearlos disponemos de 3 métodos. El primero de ellos y el más sencillo es hacerlo desde Visual Studio .NET. Solo es necesario agregar una referencia a un componente COM desde la ventana de diálogo "Agregar Referencia" (ver imagen 1).

Imagen 1: Cuadro de diálogo "Agregar Referencia" de VS.NET


Una vez hecho esto, se habrá generado una Alternative Interop Assembly (AIA) en el directorio bin\[Debug/Release] de nuestro proyecto. Interop Assembly (IA) es el nombre que se le da a los ensamblados que contienen lo necesario para generar wrappers de componentes COM. Los RCW son creados en tiempo de ejecución cuando se los necesita y las IAs contienen el "plano" para generarlos, es decir, las IAs no contienen código, solo las instrucciones de como generarlo. El hecho de que el Interop Assembly generado sea "Alternative" se explicará luego, cuando se vean los tipos de IAs.

El segundo método de crear un RCW es mediante la herramienta Type Library Importer (tblimp.exe), que viene con el .NET Framework SDK y con Visual Studio .NET. Esta herramienta lee la información de una librería y con ella genera un RCW con metadatos .NET, a través del cual el CLR va a interactuar con la librería COM. Su uso es muy sencillo:

tlbimp ArchivoEntrada [opciones]


donde ArchivoEntrada es el nombre del archivo .dll, .tlb, etc. sobre el cual deseamos crear el wrapper.
 

Entre las opciones disponibles se destacan:

Opción Significado
/out:ArchivoSalida Determina el nombre de la dll que se generará al ejecutar el comando. Este archivo es el llamado Interop Assembly.
/primary Especifica que la dll resultante es un Primary Interop Assembly (PIA). Para esto la dll tiene que ser firmada por medio de la opción /keyfile.
/keyfile:NombreKeyFile Firma el ensamblado resultante con el archivo especificado en NombreKeyFile. Este archivo tiene que ser creado con la herramienta sn.exe.
/namespace:namespace Determina el namespace en el cual se encontrarán las clases que se encuentran en la dll. Comúnmente se suele usar el namespace Interop.NombreDLL. Si no se especifica se utiliza el nombre del archivo de salida.

Como se ve, esta herramienta es muy poderosa a la hora de crear IAs y su uso ofrece beneficios muy superiores al primer método. De hecho, al crear una IA desde VS.NET (primer método) se esta utilizando esta herramienta, pero no se están aprovechando sus capacidades. Aquí hay mas información sobre esta herramienta, junto con una lista completa de opciones.


Existe un último método para crear IAs y es usando la clase System.Runtime.InteropServices.TypeLibConverter. Este artículo no tratará sobre esta clase debido que su uso es exclusivo para personas con un conocimiento avanzado sobre el tema, y excedería el contenido de esta introducción. El interesado puede ir a aquí para mas información.

Un sencillo ejemplo

Pongamos en práctica lo que acabamos de ver. En este ejemplo vamos a crear una dll desde Visual Basic 6.0 y vamos a consumirla desde una aplicación hecha en C#.

Creación del componente COM

Primero creamos un proyecto ActiveX dll en Visual Studio 6.0 y le cambiamos el nombre a la clase que es creada por defecto a algo mas intuitivo. Lo mismo hacemos con el proyecto. En mi caso preferí llamar al proyecto "COMTest" y a la clase "COMClass". Luego agregamos un método llamado FormatearNumero el cual debe aceptar un número (en forma de string) y lo devuelve formateado. Debería quedar algo como como la imagen 2.

A continuación abrimos una ventana de comandos y nos posicionamos en el directorio donde generamos la dll. Tipeamos lo siguiente:

tlbimp.exe COMTest.dll /out:COMTestNET.dll /namespace:COMTest.Interop

El resultado se puede ver en la imagen 3.
 

Imagen 3: Resultado de ejecutar la herramienta Type Library Importer.



Acabamos de crear un IA para la dll COMTest.dll llamado COMTestNET.dll y las clases que esta contenga se encontrarán en el namespace COMTest.Interop. Veamos que podemos hacer con esta dll.
 

Creación del componente .NET

Ahora creamos un nuevo proyecto con Visual Studio. NET. Yo elegí un proyecto de consola con C#, pero el resultado es el mismo con cualquier otro tipo de proyecto u otro lenguaje. Ahora hacemos click con el botón derecho de mouse en Referencias (del proyecto) y seleccionamos "Agregar Referencia...". Hacemos click en "Examinar...". Buscamos el archivo COMTestNET.dll y cerramos todas las ventanas haciendo click en Aceptar. Ya tenemos una referencia hecha a nuestro IA (ver Imagen 4).

Imagen 4: Referencia a un componente hecho en VB6.


A continuación, en el método Main de la clase que se crea por defecto escribimos el siguiente código:

Console.Write("Ingrese un numero: ");
string tmp = Console.ReadLine();

COMTest.Interop.COMClassClass claseCOM;
claseCOM = new COMTest.Interop.COMClassClass();

Console.WriteLine("El número formateado es: {0}.", claseCOM.FormatearNumero(tmp));
Console.ReadLine();

y presionamos F5 para compilar y ejecutar el código.

Ingresamos un número cuando sea solicitado y presionamos ENTER. El resultado se puede ver en la imagen 5.
 

Imagen 5: Resultado de la ejecución de nuestra aplicación.

 

Interop Assemblies en el GAC

¿Que sucede si quiero consumir los servicios que brinda esta dll en otras aplicaciones .NET? Si se presenta este caso, lo ideal es colocar la IA en el Global Assembly Cache (GAC), pero para ello tiene que estar firmada y eso es lo que vamos a hacer ahora. Podemos usar tlbimp para firmar el ensamblado a la vez que lo generamos, esto se logra con la opción /keyfile:keyfile. Suponiendo que ya tenemos archivo .snk creado (usando otra herramienta llamada sn.exe, ver "Otras herramientas útiles" más abajo), deberíamos usar tlbimp de este modo:

tlbimp.exe COMTest.dll /out:COMTestNET.dll /namespace:COMTest.Interop /keyfile:COMkey.snk

Ahora nuestra dll esta lista para registrarse en el GAC mediante gacutil.exe (ver "Otras herramientas útiles").

AIAs y PIAs

Existen dos tipos de IAs: Alternative Interop Assemblies y Primary Interop Assemblies. Ambos son iguales salvo por un detalle: los PIAs son como IAs "oficiales", mientras que los AIAs no. Con "oficiales" estoy queriendo decir que los PIAs están firmados por los creadores de los componentes (COM) originales, asegurando así que la definición de los tipos de estos es correcta.

Es sencillo saber si estamos haciendo referencia a un PIA o a un AIA. Desde VS.NET, seleccionamos la referencia a la dll en el Explorador de Soluciones y presionamos F4 para ver sus propiedades (si es que no las estamos viendo ya). Si es un PIA, entonces la propiedad "Nombre Seguro" debe valer "True", caso contrario, "False".
Si no usamos VS.NET, podemos determinar si estamos frente a un PIA usando la utilidad MSIL Disassembler (ildasm.exe, ver "Otras herramientas útiles"). Al ejecutarla se nos abrirá el desensamblador .NET.

Vamos al menú Archivo/Abrir y buscamos la dll en cuestión. Una vez abierta veremos algo parecido a la imagen 6.

Imagen 6: Nuestro código desensamblado.


Hacemos doble click en MANIFEST para abrir el manifiesto del ensamblado y se nos abrirá una especie de Block de notas con el manifiesto de la dll que abrimos. Si estamos frente a un PIA tendremos que encontrar una referencia al atributo PrimaryInteropAssemblyAttribute. Por ejemplo:

.custom instance void [mscorlib]System.Runtime.InteropServices.PrimaryInteropAssemblyAttributeint32)
                                                               = ( 01 00 01 00 00 00 00 00 00 00 00 00 ) 

Si esta referencia no esta entonces no estamos frente a un PIA.

Trabajando con PIAs y AIAs

Hay que tener en cuenta un detalle importante: Si dos aplicaciones .NET comparten un mismo objeto COM y una de las aplicaciones hace referencia a un PIA y la otra a un AIA (donde ambos contienen la definición del objeto, claro está), el CLR generará una excepción, es por ello que siempre hay que usar PIAs cuando sea posible. Afortunadamente, si estamos trabajando con VS.NET, no vamos a tener que preocuparnos mucho por esto ya que si hacemos referencia a un Alternative Interop Assembly (no PIA) y VS.NET encuentra un PIA correspondiente a la dll original, entonces nuestra referencia será ignorada y se referenciará a el PIA.

Otro detalle importante a tener en cuenta antes de generar un PIA es que si este tiene referencias (opción /reference de tlbimp) a otras dlls, estas deben ser PIAs también. Caso contrario, la generación fallará.

Otras herramientas útiles.

A lo largo del artículo he mencionado algunas herramientas que nos son indispensables para trabajar con ensamblados. Estas herramientas, al igual que tlbimp, vienen con el SDK del Framework .NET y con VS.NET.

Strong Name Tool (sn.exe)

Descripción:

Esta herramienta es la utilizada para firmar assemblies con "nombres seguros". Un nombre seguro es necesario para que un ensamblado pueda ser distinguido del resto y es obligatorio si este tiene que encontrarse en el GAC. Este consiste en:

  • El nombre del ensamblado
  • La versión del ensamblado
  • Información cultural del ensamblado
  • Una clave pública
  • Una firma digital
  • Uso:

    sn -k NombreArchivoSNK [opciones]


    donde NombreArchivoSNK es el nombre del archivo que contendrá la clave generada. Comunmente se utiliza el nombre del archivo que se desea firmar, por ejemplo:

    sn -k archivo.dll

    generaría el archivo archivo.dll.snk. Mas informacion sobre la herramienta
    aquí.

    Una vez que tengamos el archivo .snk generado, podremos firmar un assembly con este de dos formas. Desde VS.NET, abrimos el archivo AssemblyInfo del proyecto al cual queremos firmar. Alli agregamos el atributo AssemblyKeyFileAttribute del siguiente modo:

    [assembly: AssemblyKeyFile("NombreArchivoSNK.snk")] (C#) o
    <Assembly: AssemblyKeyFile("NombreArchivoSNK.snk")> (VB.NET)

    Ahora cada vez que compilemos el proyecto se generará un assembly firmado.
    Es muy recomendable setear la versión del ensamblado a mano modificando el atributo AssemblyVersionAttribute y no dejar que este atributo se autogénere automaticamente:

    [assembly: AssemblyVersion("1.1.3.0")] (C#) o
    <Assembly: AssemblyVersion("1.1.3.0")> (VB.NET)

    El otro modo de firmar un ensamblado es mediante de la utilidad Assembly Linker (al.exe). Solo me limitaré a mostrar un ejemplo del uso:

    al nombremodulo.netmodule /out:archivo.dll /keyfile:archivo.dll.snk

    Gacutil (gacutil.exe)

    Descripción:

    Esta herramienta es utilizada para ver y manipular los ensamblados que se encuentran en el Global Assembly Cache (GAC).
    Si deseamos que un ensamblado esté disponible para multiples aplicaciones entonces este deberia estar en el GAC. Si no estuviera ahi, entonces cada aplicación tendría que tener su propia copia local del ensamblado.

    Uso:

    gacutil [opciones] [parametros]


    donde parametros depende de la opcion especificada. Entre las opciones diponibles podemos encontrar las siguientes:

    OpciónSignificado
    /i NombreEnsambladoInstala el ensamblado NombreEnsamblado en el GAC. Si ya existe un ensamblado con el mismo nombre, entonces el comando fallará.
    /if NombreEnsambladoInstala el ensamblado NombreEnsamblado en el GAC. Si ya existe un ensamblado con el mismo nombre, entonces este será sobreescrito.
    /u NombreCompletoEnsambladoDesinstala el ensamblado NombreCompletoEnsamblado del GAC. Debido a que en el GAC pueden convivir distintas versiones del mismo ensamblado el nombre debera incluir la versión del mismo, la información cultural y el PublicTokenKey del mismo, separados por comas sin espacios.
    /l [NombreEnsamblado]Lista los assemblies que se encuentran en el GAC y cuyo nombre es NombreEnsamblado. Si no se especifica NombreEnsamblado, entonces se lista todo el contenido del GAC.

    Mas información sobre la herramienta aquí.

    Algunos ejemplos:

    gacutil /if ComponenteNet.dll
    Instala el assembly ComponenteNet.dll en el GAC sin importar si ya fue registrado antes o no.

    gacutil /l ComponenteNet
    Lista todas las versiones del assembly ComponenteNet.dll que se encuentran en el GAC.

    gacutil /u ComponenteNet.dll,Version=1.1.0.3,Culture=es,PublicKeyToken=8s4e29ab8f4e255b
    Desinstala el ensamblado ComponenteNet.dll cuya version es 1.1.0.3, su cultura es "es" (español) y su PublicKeyToken es "8s4e29ab8f4e255b".

    MSIL Disassembler (ildasm.exe)

    Descripción:

    Esta aplicación se encarga de "desensamblar" un ensamblado con código administrado (es decir, MSIL, Microsoft Intermediate Language) y genera a partir de este un archivo de texto. Esto nos permite modificar los assemblies a mano y luego reensamblarlos con el ensamblador .NET (MSIL Assembler, ilasm.exe).

    Uso:

    ildasm [NombreEnsamblado] [opciones]


    donde NombreEnsamblado es el nombre del ensamblado que deseamos desensamblar. Entre las opciones diponibles podemos encontrar las siguientes:

    OpciónSignificado
    /output:NombreArchivoEjecuta el desensamblador y guarda la salida en NombreArchivo.
    /textEjecuta el desensamblador y envia la salida a la consola.
    nadaEjecuta el desensamblador en modo interfaz gráfica.

    Mas informacion sobre la herramienta aquí.

    Algunos ejemplos:

    ildasm Componente.dll
    Desensambla el assembly Componente.dll y muestra su contenido mediante la interfaz gráfica.

    ildasm /output:desensamblado.txt
    Desensambla el assembly y guarda el contenido de este en el archivo desensamblado.txt.


    ir al índice