Código inseguro (Unsafe Code) e interoperabilidad.

Anuncio
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Código inseguro (Unsafe Code) e interoperabilidad.
Código inseguro
Referencias y punteros.
En C#, para acceder a memoria se utilizan referencias. Las referencias son similares a
los punteros, con la salvedad de que no permiten manipular el valor de la dirección a la
que apuntan (valor de la dirección, no contenido).
Es decir, si una referencia apunta a una variable que está en la dirección 0x004FFFA8, a
través de la referencia es posible utilizar la variable pero no se puede obtener el valor
0x004FFFA8 (que es la dirección de la variable) y tampoco se le pueden sumar valores a
la dirección para apuntar a otra dirección (por ejemplo: sumarle 4 a 0x004FFFA8 para
acceder a la variable contenida a partir de la posición 0x004FFFAB).
La gran ventaja que ofrecen las referencias es que evitan muchos errores derivados del
mal uso de los punteros, pero también tiene desventajas:
-
Al no permitir acceder directamente a la memoria, limitan el rendimiento que
podría dar a una aplicación hacerlo.
Ciertas aplicaciones como, por ejemplo, un depurador o debugger, necesitan
manejar direcciones de memoria.
Las dll antiguas (el API de Windows, por ejemplo) tienen funciones que utilizan
punteros como parámetros, de modo que no podrían ser invocadas si no se
permite la utilización de punteros.
De lo comentado se deduce que, en ocasiones, puede ser interesante utilizar punteros.
Un primer acercamiento a los punteros son los delegates, que pertenecen al llamado
“safe code” o “código seguro” pero únicamente se comportan como punteros a
funciones.
En C# se permite utilizar punteros y al código en el que se utilizan punteros se le
denomina “código inseguro” o “unsafe code”. El código inseguro debe ser marcado
utilizando la palabra reservada “unsafe”.
Es posible marcar como “unsafe”:
-
Una clase
Un miembro de una clase
Un bloque de código
En C# no se permite declarar punteros a tipos referencia. Tampoco es posible marcar
como unsafe una variable local (ha de marcarse el método o bloque de código en el que
está).
Además de lo comentado, al compilar código inseguro es necesario indicárselo al
compilador utilizando el flag unsafe (independientemente de si se compila desde el
Visual Studio .NET o desde la línea de comandos).
1/38
Marco Besteiro y Miguel Rodríguez
•
Código Inseguro
Desde la línea de comandos es tan sencillo como indicar /unsafe al compilar:
csc /unsafe ...
•
Desde Visual Studio .NET se ha de mostrar la ventana de propiedades:
Figura 13.1
2/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.2
En caso de que no esté seleccionado, se ha de seleccionar el proyecto en desarrollo
desde la explorador de soluciones:
3/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.3
Al hacerlo, en la ventana propiedades se mostrará la información sobre el proyecto y
se activarán los botones e iconos del cuadro de la ventana. Pulse sobre el cuarto icono
de la ventana de propiedades y aparecerá el cuadro de diálogo con las página de
propiedades del proyecto.
En propiedades de configuración se ha de elegir Generar y Permitir bloques de
código no seguros a True.
4/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.4
De este modo será posible compilar código inseguro. En caso contrario el compilador
no lo permitirá (genera error en tiempo de compilación).
Por lo demás, el uso y declaración de punteros en C# es igual que en C y C++, con una
excepción: en C y C++, cuando se declaran varias variables de tipo puntero en una
misma línea se separan por comas y van precedidas todas ellas por *. Por ejemplo:
int *p1, *p2;
En C# sólo la primera variable de tipo puntero ha de ir precedida por *. Por ejemplo:
int *p1, p2;
Ejemplo:
int num = 5;
int num2 = 10;
int *pNum;
//la variable num ocupa 4 bytes de memoria
//(stack o pila), por ejemplo, a partir
//de la posición 0x004FFFA8 hasta la
//0x004FFFAB. El contenido de esas posiciones
//es el valor 5.
//la variable num2 ocupa 4 bytes
//de memoria (stack o pila), por
//ejemplo, a partir de la posición 0x004FFFA4 hasta
//la 0x004FFFA7. El contenido de esas
//posiciones es el valor 10.
// pNum es una variable de tipo puntero a entero y
//ocupa 4 bytes de memoria (stack o pila), por
//ejemplo, a partir de la 0x004FFFA0 hasta la
//0x004FFFA3. El contenido de esas posiciones no está
//definido (basura).
5/38
Marco Besteiro y Miguel Rodríguez
*pNum = &num2;
Código Inseguro
//al asignar la dirección de la variable num2 al
//contenido de la variable pNum, las posiciones
//correspondientes a la variable de tipo puntero pNum
//(0x004FFFA0 a 0x004FFFA3) pasan a
//contener la dirección de la variable num2, es
//decir, 0x004FFFA4.
Aritmética de punteros.
Es posible añadir o restar enteros a un puntero utilizando los operadores +, -, ++, --, +=
y -=. Por ejemplo, supóngase que se añade la instrucción pNum++ al ejemplo anterior:
int num = 5;
int num2 = 10;
int *pNum;
*pNum = &num2;
pNum++;
Al incrementar en una unidad pNum lo que sucede es que su contenido pasa a ser el
actual aumentado en cuatro unidades, ya que el tamaño de una variable de tipo entero es
4 bytes y el compilador entiende que se quiere que pNum apunte a la siguiente variable
de tipo entero y no al siguiente byte. De este modo, el contenido de pNum será
0x004FFFA4 + 4, es decir 0x004FFFA8, que es la dirección de la variable num.
Es importante notar que si el código hubiese sido:
int num = 5;
int num2 = 10;
int *pNum;
*pNum = #
pNum++;
Al incrementar pNum pasaría a valer 0x004FFFA8 + 4, que es 0x004FFFAC. Aquí se
presenta el problema inherente al manejo directo de punteros. ¿Qué hay en
0x004FFFAC? Lo más posible es que no haya un entero sino algo desconocido, lo cual
implicará un malfuncionamiento del programa cuyos efectos no son fácilmente
predecibles.
El siguiente ejemplo muestra cómo desarrollar código inseguro y utilizar punteros y
variables, indicando información sobre éstos.
using System;
class CodigoInseguroPunteros
{
static unsafe void Main (string[] args)
{
int
int_num = 15;
int
*p_int_num = &int_num;
Console.WriteLine("El valor de int_num es:" + *p_int_num);
Console.WriteLine("La dirección de int_num es: " +
(uint) p_int_num);
Console.WriteLine("El tamaño de int_num es:"+sizeof (int));
Console.WriteLine("El valor de p_int_num es: " +
6/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
(uint) p_int_num);
Console.WriteLine("La dirección de p_int_num es:
" +
(uint) &p_int_num);
Console.WriteLine("El tamaño de p_int_num es: " +
sizeof (int*));
}
}
El resultado es:
Figura 13.5
Tanto int_num como p_int_num son almacenados en la pila o stack ya que son datos de
tipo primitivo (valor).
Un int (entero) tiene un tamaño de 4 bytes. Un puntero tiene un tamaño de 4 bytes
(independientemente del tipo de datos al que apunte). El contenido de p_int_num es la
dirección de int_num.
Se puede observar que la dirección de p_int_num es 1243464, es decir, anterior en 4
bytes (que es lo que ocupa el puntero) a int_num. Puesto que la pila crece hacia abajo y
las variables se le añaden en el orden en que han sido declaradas, lo lógico es que
p_int_num ocupe las posiciones más bajas.
7/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.6
El siguiente ejemplo es similar al anterior pero abarca más tipos de datos de tipo valor
(todos primitivos).
using System;
class ManejoPunteros
{
static unsafe void Main (string[] args)
{
bool bool_val = true;
char char_val = 'a';
sbyte sbyte_num = 5;
short short_num = 10;
int
int_num = 15;
long long_num = 20;
double double_num = 25;
float float_num = 30;
bool *p_bool_val = &bool_val;
char *p_char_val = &char_val;
sbyte *p_sbyte_num = &sbyte_num;
short *p_short_num = &short_num;
int
*p_int_num = &int_num;
long *p_long_num = &long_num;
double *p_double_num = &double_num;
float *p_float_num = &float_num;
Console.WriteLine("El valor de bool_val es:
" +
*p_bool_val); //podría ponerse bool_val
Console.WriteLine("La dirección de bool_val es: " +
(uint) p_bool_val); //podría ponerse &bool_val
Console.WriteLine("El tamaño de bool_val es:
" +
sizeof (bool));
Console.WriteLine("El valor de char_val es:
" +
*p_char_val);
8/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Console.WriteLine("La dirección de char_val es: " +
(uint) p_char_val);
Console.WriteLine("El tamaño de char_val es:
" +
sizeof (char));
Console.WriteLine("El valor de sbyte_num es:
" +
*p_sbyte_num);
Console.WriteLine("La dirección de sbyte_num es:
(uint) p_sbyte_num);
Console.WriteLine("El tamaño de sbyte_num es: " +
sizeof (sbyte));
Console.WriteLine("El valor de short_num es:
" +
*p_short_num);
Console.WriteLine("La dirección de short_num es:
(uint) p_short_num);
Console.WriteLine("El tamaño de short_num es: " +
sizeof (short));
Console.WriteLine("El valor de int_num es:
*p_int_num);
Console.WriteLine("La dirección de int_num es: " +
(uint) p_int_num);
Console.WriteLine("El tamaño de int_num es:
" +
sizeof (int));
Console.WriteLine("El valor de long_num es:
" +
*p_long_num);
Console.WriteLine("La dirección de long_num es: " +
(uint) p_long_num);
Console.WriteLine("El tamaño de long_num es:
" +
sizeof (long));
Console.WriteLine("El valor de double_num es: "
*p_double_num);
Console.WriteLine("La dirección de double_num es:
(uint) p_double_num);
Console.WriteLine("El tamaño de double_num es: " +
sizeof (double));
Console.WriteLine("El valor de float_num es:
" +
*p_float_num);
Console.WriteLine("La dirección de float_num es:
(uint) p_float_num);
Console.WriteLine("El tamaño de float_num es: " +
sizeof (float));
}
}
El resultado será:
9/38
" +
" +
" +
+
" +
" +
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.7
Aquí puede observarse también la colocación de las variables en la pila (en este caso no
se muestra la información sobre los punteros).
Casting de punteros.
Es posible convertir punteros a un tipo de datos a punteros a otro tipo diferente de datos.
Por ejemplo:
int num = 5;
int *pNum;
byte *pOcteto;
pOcteto = #
pNum = #
System.Console.WriteLine(“Contenido apuntado por pNum {0}”, *pNum);
System.Console.WriteLine(“Contenido apuntado por pOcteto {0}”,
*pOcteto);
Tanto pOcteto como pNum apuntan a num, que es una variable que ocupa 4 bytes en
memoria (stack), ya que es una variable de tipo int. Cuando se acceda a num mediante
pNum, se permitirá manejar los 4 bytes de num como lo que es (un entero), en cambio,
cuando se acceda a num a través de pOcteto, sólo podrá manipularse el primer byte u
octeto de los 4 que ocupa num, porque a través de un puntero a byte sólo puede
manejarse un byte, independientemente del tipo de datos real al que se refiera la
dirección contenida en el puntero a byte. Es importante deducir de esto que
dependiendo del casting que se haga es posible que no tenga utilidad alguna.
Un casting lógico puede ser convertir cualquier tipo de puntero en un puntero a sbyte,
para poder recorrer byte a byte el contenido de una variable.
También es posible convertir un puntero a otro tipo de datos no puntero, como puede
ser int, uint, long... De todos estos casting los más lógicos son a int o uint, ya que
un dato de tipo int o uint ocupa 4 bytes y uno de tipo long 8, etc...
10/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Además, si se desea mostrar por consola el valor de un puntero, por ejemplo pNum, no
puede hacerse:
//error en compilación
System.Console.WriteLine(“Contenido de pNum {0}”, pNum);
Ha de hacerse lo siguiente:
System.Console.WriteLine(“Contenido de pNum {0}”, (uint) pNum);
Nota: en C#, al igual que en C y C++ puede utilizarse el operador sizeof para obtener
el tamaño de un tipo de datos.
Punteros void.
No es una buena idea utilizar punteros a void en C# pero en ocasiones puede ser
necesario. El caso más común es cuando se desea invocar una función del API que
necesita recibir parámetros de tipo void*.
Se puede declarar punteros a void y hacer casting entre punteros de otros tipos y void:
void * pVoid;
int * pInt;
int num = 5;
*pInt = &num
pVoid = (void*) pInt;
pero no es posible desreferenciar punteros a void utilizando el operador *, es decir
*pVoid causará un error de compilación en cualquier expresión en que aparezca (en las
declaraciones no, por supuesto).
Punteros a estructuras y a miembros de estructuras.
C# permite declarar punteros a estructuras con la condición de que la estructura para la
que se declare el puntero no posea miembros de tipo referencia. Si se salta esta norma,
el compilador da un error.
Salvada esta restricción, los punteros a estructuras funcionan de modo similar a los
punteros a los tipos valor predefinidos (byte, int, uint, long…).
Por ejemplo:
struct Punto
{
public int X;
public int Y;
}
Punto * pPunto;
Punto p1 = new Punto();
pPunto = &p1;
(*pPunto).X = 5; //también es válido pPunto->X = 5;
(*pPunto).Y = 10; //también es válido pPunto->Y = 10;
System.Console.WriteLine (“X: “ + (*pPunto).X );
11/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
System.Console.WriteLine (“Y: “ + (*pPunto).Y );
Por supuesto, también se pueden crear punteros a los miembros de una estructura. Por
ejemplo:
Punto * pPunto;
int * pX, pY;
Punto p1 = new Punto();
pX = &(p1.X); //también
pY = &(p1.Y); //también
*pX = 5;
*pY = 10;
System.Console.WriteLine
System.Console.WriteLine
es válido pX = &(p1->X);
es válido pY = &(p1->Y);
(“X: “ + pX );
(“Y: “ + pY );
Punteros a clases y a miembros de clases.
No es posible crear punteros a clases (una clase es un tipo referencia) ya que se
almacenan, como toda variable de tipo referencia, en el heap y el garbage collector
elimina las variables del heap cuando dejan de estar referenciadas por referencias,
aunque queden punteros apuntándolas.
En cambio, es posible que un puntero apunte a los miembros de tipo valor de una clase
(sólo a estos). No obstante, se plantea un problema: los miembros de una variable de
una clase serán eliminados cuando el garbage collector elimine la variable del heap (ya
que el garbage collector no tiene en cuenta los punteros). La solución es utilizar la
palabra reservada fixed cuando se declara un puntero a un miembro de una clase.
fixed le indica al compilador que el código que genere ha de asegurar que el garbage
collector no eliminará el objeto al que pertenece el miembro fijado mientras se esté
utilizando.
Por ejemplo:
class
{
Punto
public int X;
public int Y;
}
int * pX, pY;
Punto p1 = new Punto();
fixed (pX = &(p1.X))
{
//manejar pX
*pX = 5;
System.Console.WriteLine (“X: “ + pX );
}
...
fixed (pY = &(p1.Y))
{
//manejar pY
*pY = 10;
System.Console.WriteLine (“Y: “ + pY );
}
12/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Lo anterior podría haberse programado de modos alternativos.
•
Modo 1 (en grupo):
...
...
int *
Punto
fixed
fixed
{
pX, pY;
p1 = new Punto();
(pX = &(p1.X))
(pY = &(p1.Y))
//manejar pX
*pX = 5;
System.Console.WriteLine (“X: “ + pX );
//manejar pY
*pY = 10;
System.Console.WriteLine (“Y: “ + pY );
}
...
...
•
Modo 2 (anidado):
...
...
int * pX, pY;
Punto p1 = new Punto();
fixed (pX = &(p1.X))
{
//manejar pX
*pX = 5;
System.Console.WriteLine (“X: “ + pX );
fixed (pY = &(p1.Y))
{
//manejar pY
*pY = 10;
System.Console.WriteLine (“Y: “ + pY );
}
}
...
...
13/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Interoperabilidad.
Se denomina “managed code” al código que se puede ejecutar bajo el control del CLR y
“unmanaged code” a aquél cuya ejecución no puede ser controlada por el CLR y por
tanto no pueden aplicársele las reglas de control de seguridad del CLR ni la recolección
de basura (garbage collector).
El “managed code” necesita en muchas ocasiones operar con el unmanaged code
(código nativo) existente. Incluso habrá casos en los que sea necesario desarrollar una
parte de un proyecto en código nativo e invocar tal código nativo desde el código IL que
es ejecutado, de modo controlado (managed code), por el CLR.
Para permitir esto, el CLR soporta dos formas de operación con el código nativo:
-
Platform Invocation Services (P/Invoke).
COM Interoperability.
Es importante no confundir unmanaged code con unsafe code. Este último es managed
code precedido por la palabra clave unsafe al que se le permite utilizar características no
seguras (unsafe) del lenguaje C++, como son los punteros.
Platform Invocation Services.
Invoke o Platform Invocation Services es el nombre que se da a la tecnología que
permite invocar “unmanaged code” desde el código .NET o “managed code”.
Utilización de dll nativas desde .NET.
Para poder invocar una función de una dll desde código .NET basta con utilizar el
atributo DllImport o SysImport.
El atributo DllImport se comentó en el tema referente a los atributos y el atributo
SysImport es similar, sólo que se utiliza para invocar funciones de las dll del sistema
(user32.dll, gdi32.dll y kernel32.dll).
Al explicar el atributo DllImport se utilizó un ejemplo en el que se invocaba a la
función MessageBox de la librería user32.dll:
using System;
using System.Reflection;
using Clases_NameSpace;
//Para utilizar DllImport es necesario el namespace
//System.Runtime.InteropServices
using System.Runtime.InteropServices;
class PruebaAtributosReservados
{
[DllImport("User32.dll")]
public static extern int MessageBox(int hParent, string Message,
string Caption, int Type);
public static void Main()
{
14/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Clase2.FunClase2 ();
Clase3.FunClase3 ();
System.Console.WriteLine(" ");
System.Console.WriteLine("Para llamar a MessageBox pulse
Enter: ");
System.Console.ReadLine();
MessageBox(0, "Hola desde el API de Windows",
"Mensaje invocado desde .NET", 0);
}
}
El mismo ejemplo, utilizando SysImport es:
using System;
using System.Reflection;
using Clases_NameSpace;
//Para utilizar DllImport es necesario el namespace
//System.Runtime.InteropServices
using System.Runtime.InteropServices;
class PruebaAtributosReservados
{
[SysImport(dll="User32.dll")]
public static extern int MessageBox(int hParent, string Message,
string Caption, int Type);
public static void Main()
{
...
...
El resultado de ejecutar este ejemplo es:
Figura 13.8
Tras pulsar Enter se mostrará el cuadro de mensaje:
15/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.9
COM interoperability.
El CLR ofrece soporte tanto para utilizar componentes COM desde C# como para
utilizar objetos C# desde componentes COM y desde código nativo en general como si
los objetos C# fuesen componentes COM.
La interoperabilidad entre componentes COM y C# puede realizarse mediante enlace
temprano (en compilación) o tardío (en ejecución).
Utilización de componentes COM desde .NET.
El uso de componentes COM desde C# es diferente dependiendo de si se utiliza enlace
temprano o tardío.
Utilización de componentes COM desde .NET mediante enlace temprano.
Para poder utilizar un componente COM desde .NET de modo que queden todas las
referencias resueltas en tiempo de compilación (enlace temprano o early binding) es
necesario que exista un objeto que siga la tecnología .NET (para poder incluirlo en el
código .NET) y que exponga las interfaces del componente COM.
A este objeto se le llama Runtime Callable Wrapper (RCW) y actúa como proxy que
utiliza .NET para acceder al componente COM.
El RCW se encarga de traducir las llamadas que desde .NET se hagan a sus métodos en
las llamadas correctas al componente COM. También se encarga de traducir la respuesta
del componente COM de modo que pueda ser utilizado desde .NET y de controlar el
ciclo de vida del componente COM (Addref, Release...).
Para crear el RCW correspondiente a un componente COM se ha de utilizar
TlbImp.exe.
Por ejemplo: supónga que se dispone de un componente COM contenido en la dll
quartz.dll (C:\Winnt\Sustem32\quartz.dll) y que se desea utilizar desde .NET, en
concreto desde una aplicación C#. Los pasos a seguir son:
1) Crear el RCW correspondiente al componente contenido en quartz.dll y
copiarlo en el directorio de la aplicación .NET que lo va a utilizar (no tiene
porque ser así):
tlbimp c:\winnt\system32\quartz.dll /out:QuartzTypeLib.dll
16/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
QuartzTypeLib.dll es el RCW, que realmente es una clase .NET, la cual podrá
utilizarse en la aplicación C# que llamará al componente contenido en
quartz.dll.
2) Crear la aplicación que utiliza el componente contenido en quartz.dll
//Fichero InteropCOM_Enlace_Temprano_Consola.cs
using System;
using QuartzTypeLib;
namespace COMInterop_Enlace_Temprano_Consola
{
/// <summary>
/// Summary description for Aplicacion_COMInterop_Consola.
/// </summary>
public class Aplicacion_COMInterop_Consola
{
public Aplicacion_COMInterop_Consola()
{
//
// TODO: Add constructor logic here
//
}
public static void Main()
{
QuartzTypeLib.FilgraphManager fgm = new FilgraphManager();
// La siguiente instrucción equivale a una llamada
// a QueryInterface pidiendo el interface IMediaControl
QuartzTypeLib.IMediaControl mc =
(QuartzTypeLib.IMediaControl)fgm;
// El método RenderFile recibe como parámetro el fichero a
//reproducir
mc.RenderFile(@"C:\Archivos
de
programa\Microsoft.NET\FrameworkSDK\Samples\
technologies\remoting\advanced\
remotingcom\mediaplayer\client\clock.avi");
//Run reproduce el fichero.
mc.Run();
Console.WriteLine("Pulse Enter para continuar.");
Console.ReadLine();
}
}
}
3) Compilar la aplicación incluyendo la dll del RCW como referencia:
csc /r:QuartzTypeLib.dll InteropCOM_Enlace_Temprano_Consola.cs
4) Invocar al ejecutable de la aplicación:
InteropCOM_Enlace_Temprano_Consola
17/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
El resultado es:
Figura 13.10
Este mismo ejemplo, hecho desde el Visual Studio .NET es más sencillo gracias al
cuadro de diálogo del menú principal Proyecto/Agregar Referencia:
18/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.11
Tras aceptar la librería seleccionada quartz.dll Visual Studio muestra un mensaje
indicando que no existe el RCW para el componente contenido en quartz.dll y
preguntando si se desea que lo cree.
Figura 13.12
Suponiendo
que
se
acepta,
Visual
Studio
.NET
crea
una
dll
llamada
QuartzTypeLib.dll (RCW) y la almacena en el directorio de la aplicación. A partir de
este momento, la aplicación puede utilizar la clase o, mejor dicho, las clases del paquete
QuartzTypeLib (puede haber más de una clase o componente en una dll). En el
explorador de soluciones se ve que aparece una referencia a QuartzTypeLib.
19/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.13
Una de las ventajas que da Visual Studio .NET es que muestra ayuda contextual al
escribir el código que utiliza la referencia añadida (en este caso QuartzTypeLib).
Figura 13.14
Nota: En caso de que el componente esté en una dll no incluida por defecto en la
pestaña “COM” del cuadro de diálogo Agregar referencia puede utilizarse el botón
Examinar para buscar e incluir la dll que se desee.
20/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Utilización de componentes COM desde .NET mediante enlace tardío.
Cuando se utiliza enlace tardío para invocar los métodos de un componente COM desde
una aplicación, ésta ha de obtener la dirección de tales métodos durante la ejecución. Es
decir, al compilar la aplicación nada se sabe sobre la dirección de los componentes que
va a utilizar la aplicación.
En enlace tardío no es necesario crear el RCW para el componente COM. Se ha de
utilizar el método GetTypefromProgID de la clase Type.
Los pasos a seguir para utilizar un componente COM mediante enlace tardío son:
1) Incluir el namespace System.Interop.InteropSevices.
2) Crear un objeto Type para el componente COM utilizando
Type.GetTypeFromProgID() o Type.GetTypeFromCLSID().
3) Crear un componente COM utilizando Activator.CreateInstance().
4) Llamar a los métodos del componente COM utilizando el método
InvokeMember del objeto Type.
El enlace tardío ya es conocido de un tema anterior y se puede observar que se sigue
una idea similar al llamar a componentes COM mediante enlace tardío. Es importante
saber que cuando se invoca a los métodos de un componente COM mediante enlace
tardío, la invocación se hace utilizando la interface IDispatch.
En el siguiente ejemplo va a intentarse invocar utilizando enlace tardío a los métodos
RenderFile y Run del componente FilgraphManager, contenido en Quartz.dll. Por
supuesto, no hay que añadir una referencia a Quartz.dll en el proyecto.
using System;
using System.Reflection;
using System.Runtime.InteropServices;
namespace COMInterop_Enlace_Tardio_Consola
{
/// <summary>
/// Summary description for Aplicacion_COMInterop_Consola.
/// </summary>
public class Aplicacion_COMInterop_Consola_Enlace_Tardio
{
public Aplicacion_COMInterop_Consola_Enlace_Tardio()
{
//
// TODO: Add constructor logic here
//
}
public static void Main()
{
Type objTypeQuartz;
object objQuartz;
object[] arObjs = {@"C:\Archivos de programa\
Microsoft.NET\FrameworkSDK\Samples\
technologies\remoting\advanced\
remotingcom\mediaplayer\client\clock.avi"};
21/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
objTypeQuartz =
Type.GetTypeFromProgID("Quartz.FilgraphManager");
objQuartz = Activator.CreateInstance(objTypeQuartz);
objTypeQuartz.InvokeMember("RenderFile",
BindingFlags.InvokeMethod, null, objQuartz, arObjs);
objTypeQuartz.InvokeMember("Run",
BindingFlags.InvokeMethod, null, objQuartz, null);
Console.WriteLine("Pulse Enter para continuar.");
Console.ReadLine();
}
}
}
Si se compila esta aplicación, al ejecutarse lanza una excepción.
Figura 13.15
Si se pulsa No:
22/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.16
La excepción se lanza al intentar crear un objeto del tipo FilgraphManager, es decir, al
ejecutar la línea:
objQuartz = Activator.CreateInstance(objTypeQuartz);
ya que objTypeQuartz es null y no una referencia a un objeto. El problema real esta
en la línea anterior:
objTypeQuartz= Type.GetTypeFromProgID("Quartz.FilgraphManager");
El problema es que no está registrado el ProgID de FilgraphManager. La verdad es
que no está registrado ni siquiera FilgraphManager. Para comprobarlo no hay más que
mostrar el registro del sistema y buscarlo.
Figura 13.17
23/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.18
El resultado será que no se encuentra:
24/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.19
Si se busca FilgraphManager, tampoco se encontrará. Esto puede hacer sospechar que
no está registrado, pero la verdad es que sí lo está (puede ejecutarse regsvr32
C:\Winnt\System32\Quartz.dll para asegurarse). Si se busca Quartz:
Figura 13.20
Se encontraran varias coincidencias. La que interesa es:
25/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.21
Es decir, Quartz.dll sí está registrada y se conoce su CLSID. Podría pensarse que tal
vez valga este CLSID, aunque el que se quiere es el de FilgraphManager, que no se
encuentra y por tanto cambiar el código de modo que se utilice el CLSID, en lugar del
ProgID:
using System;
using System.Reflection;
using System.Runtime.InteropServices;
namespace COMInterop_Enlace_Tardio_Consola
{
/// <summary>
/// Summary description for Aplicacion_COMInterop_Consola.
/// </summary>
public class Aplicacion_COMInterop_Consola_Enlace_Tardio
{
public Aplicacion_COMInterop_Consola_Enlace_Tardio()
{
//
// TODO: Add constructor logic here
//
}
public static void Main()
{
Type objTypeQuartz;
object objQuartz;
26/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
object[]
arObjs
=
{@"C:\Archivos
de
programa\Microsoft.NET\FrameworkSDK\Samples\technolog
ies\remoting\advanced\remotingcom\mediaplayer\client\
clock.avi"};
System.Guid clasid = new System.Guid("70E102B0-555611CE-97C0-00AA0055595A");
objTypeQuartz = Type.GetTypeFromCLSID(clasid);
objQuartz = Activator.CreateInstance(objTypeQuartz);
objTypeQuartz.InvokeMember("RenderFile",
BindingFlags.InvokeMethod, null, objQuartz, arObjs);
objTypeQuartz.InvokeMember("Run",
BindingFlags.InvokeMethod, null, objQuartz, null);
…
…
Pero en tiempo de ejecución sigue lanzando una excepción (en modo no debug):
Figura 13.22
Podría utilizarse el CLSID pero:
-
Tendría que ser el CLSID del componente deseado.
El componente deseado tendría que soportar la interface IDispatch.
Llegar aquí y no realizar un ejemplo que funcione deja la duda en el aire, de modo que
habrá que realizar un ejemplo válido.Hay que buscar un componente registrado que
tenga un progID (o un versionIndependentProgID) o un CLSID y que soporte la
interface COM IDispatch. Powerpoint.Application puede ser el componente que
cumpla tal ejemplo.
27/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.23
El código de este ejemplo será:
using System;
using System.Reflection;
using System.Runtime.InteropServices;
namespace COMInterop_Enlace_Tardio_Consola_Funciona
{
/// <summary>
/// Summary description for Aplicacion_COMInterop_Consola.
/// </summary>
public
class
Aplicacion_COMInterop_Consola_Enlace_Tardio_Funciona
{
public Aplicacion_COMInterop_Consola_Enlace_Tardio_Funciona()
{
//
// TODO: Add constructor logic here
//
}
public static void Main()
{
Type objTypeApplicationPowerPoint;
object objApplicationPowerPoint;
Console.WriteLine("Pulse
PowerPoint.");
Console.ReadLine();
28/38
Enter
para
lanzar
Marco Besteiro y Miguel Rodríguez
Código Inseguro
objTypeApplicationPowerPoint
=
Type.GetTypeFromProgID("PowerPoint.Application");
objApplicationPowerPoint
=
Activator.CreateInstance(objTypeApplicationPowerPoint
);
objTypeApplicationPowerPoint.InvokeMember("Activate",
BindingFlags.InvokeMethod,
null,
objApplicationPowerPoint, null);
Console.WriteLine("Pulse Enter para continuar.");
Console.ReadLine();
}
}
}
Y el resultado de ejecutarla:
Figura 13.24
Al pulsar Enter aparecerá Powerpoint.
29/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.25
Utilización de componentes .NET desde componentes COM y dll nativas.
Lo más común es que al desarrollar código .NET se necesite, en ocasiones, utilizar
componentes COM existentes o nuevos. No obstante, también puede darse el caso
contrario, es decir, la necesidad de invocar componentes .NET desde código nativo
Windows (componentes COM o funciones de dll).
Para poder utilizar componentes .NET como si fuesen componentes COM es necesario
que los componentes .NET estén registrados (en el registro del sistema o registry) y
exista un proxy al que se invoque a través de los datos registrados, siendo el proxy el
que realmente invocará los métodos del objeto o componente .NET.
La aplicación RegAsm (Register Assembly) registra un assembly y todos sus clases
(componentes), creando además el proxy. Se puede decir que es la inversa de TlbImp.
Una vez registrado un assembly hay que realizar varios pasos para que sea realmente
accesible como un componente COM:
1. Crear un nombre fuerte (strong name) para asignarlo al assembly. El nombre
fuerte identificará al assembly (consta de un nombre de tipo texto, un número de
versión, información sobre la cultura si se ha indicado, una clave pública y una
firma digital).
sn –k assemblyx.snk
30/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
2. Crear un fichero AssemblyInfo.cs que contenga una referencia al fichero con
el nombre fuerte:
using System.Reflection;
[assembly: AssemblyKeyFile(“assemblyx.snk”)]
donde assemblyx.snk es el nombre fuerte del assembly.
3. Compilarlo, generando un módulo:
csc /t:module /out:AssemblyInfo.dll AssemblyInfo.cs
4. Compilar assemblyx.cs utilizando el módulo assemblyinfo.dll
generando una librería:
y
csc /t:library /addmodule:assemblyinfo.dll assemblyx.cs
5. Copiar la librería assemblyx.dll a la caché global (si no se hace, no será
accesible desde el código nativo (COM y funciones de dll no COM):
gacutil /i assemblyx.dll
En el siguiente ejemplo se crea una clase muy sencilla llamada Doble (en el fichero
N_Doble.cs), que contiene un método Doblar que multiplica por dos el valor que
recibe como parámetro.
namespace N_Doble
{
using system;
public class Doble
{
public int Doblar(int num)
{
return (num * 2);
}
}
}
Lo primero que se ha de hacer es compilar N_Doble.cs y generar N_Doble.dll.
31/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.26
Una vez generada la librería, se utiliza regasm para registrarla en el registro del sistema.
Figura 13.27
En el registro del sistema ha de encontrarse una tentrada para el componente Doble.
32/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.28
Ahora es necesario copiar N_Doble.dll a la caché global para que pueda ser accedida
desde el código Windows nativo. El primer paso es generar un “strong name” o nombre
fuerte (es una buena idea generarlo y depositarlo en el archivo N_Doble.snk).
Figura 13.29
33/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Una vez se tiene el fichero con el nombre fuerte, ha de ser referenciado desde un fichero
que luego se añadirá al assembly N_Doble.dll. En este caso se llama
AssemblyInfo.cs y contiene:
using System.Reflection;
[assembly: AssemblyKeyFile("N_Doble.snk")]
Antes de añadir esta información a N_Doble.dll habrá que generar un módulo a partir
de AssemblyInfo.cs.
Figura 13.30
Para añadir la información de tal módulo a N_Doble.dll lo que realmente se hará es
regenerar N_Doble.dll a partir de N_Doble.cs indicando que se añada el módulo
AssemblyInfo.dll.
Figura 13.31
34/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Una vez se ha obtenido N_Doble.dll con la información de AssemblyInfo.dll sólo
resta copiarlo a la caché global.
Figura 13.32
A partir de este momento es posible utilizar desde código Windows nativo el
componente Doble. Un posible ejemplo puede ser un script (VBScript) contenido, por
ejemplo, en una página Web:
//Fichero PrueballamarNETdesdeCOM.html
<html>
<head>
<title> Prueba de llamada a .NET desde Windows nativo </title>
</head>
<body>
<script language=VBScript>
Option Explicit
Dim objeto_N_Doble
Dim Numero
Dim Resultado
Set objeto_N_Doble = CreateObject("N_Doble.Doble")
Numero = InputBox("Número a doblar")
Resultado = objeto_N_Doble.Doblar(CLng(Numero))
Call MsgBox(Resultado)
</script>
</body>
</html>
Al cargar la página:
35/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura 13.33
36/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
Figura
13.34
37/38
Marco Besteiro y Miguel Rodríguez
Código Inseguro
38/38
Descargar