Cualquier cosa que valga la pena se hace en equipo

Cualquier cosa que valga la pena se hace en equipo

domingo, 23 de enero de 2011

Mas sobre reflection




Manual del

Lenguaje de programación

C#

Parte III


Reflection




OBJETIVO DEL MANUAL 3

OBJETIVO DEL MANUAL 3

Reflection 3

¿Como funciona esto y para que sirve realmente? 3

El enfoque en C# 7

Ejemplo I: 7

¿Como acceder a una clase del assembly? 7

¿Como acceder a un metodo de la clase? 8

Algo más al respecto 12

EJEMPLO II 13

El proyecto biblioteca1 14

El proyecto PruebaBiblioteca 16

¿ Como enlazar una biblioteca al proyecto ? 18

El proyecto UsoDeReflection 21

¿Como acceder a los modulos de un assembly? 22

Accediendo a los metodos 24



OBJETIVO DEL MANUAL

Reflection


Para la ejecucion de una aplicación .NET debemos tener presente el concepto de CONTENEDORES. Este es un concepto que se ha popularizado los ultimos años a traves de Java y difiere de los habituales run-times en que no es un agregado que lleva el ejecutable o un conjunto de bibliotecas que resuelven aspectos especificos (como graficos), SINO QUE ES UNA MAQUINA VIRTUAL dentro de la cual corre la aplicación.-

Es decir esta cascara es la que separa a la aplicación del Sistema operativo.-

Sus ventajas en el mundo Java son obvias en tanto estos contenedores aislan al programador de especificidades de tal o cual sistema operativo.-

No nos resultan tan obvias en el mundo Microsoft donde existe UN SOLO sistema operativo.-

Sin embargo hay otras decisiones que seguramente habrán pesado en la decisión de Microsoft de hacer un esquema de contenedores.-

El primero es que este esquema permite que exista un nivel de compilación que no es la
generación del ejecutable mismo, sino de un lenguaje intermedio (MSIL para Microsoft, Bytecodes para Java y… P-CODE para quienes tuvimos oportunidad de trabajar profesionalmente con el maravilloso Pascal ¡!).-

El segundo es que varios lenguajes (al menos Visual Basic, C# y C++ en modo manejado) pueden trabajar sobre esta maquina virtual y compartir aquellos servicios, bibliotecas y assemblies que la maquina virtual necesita. Esto es un enorme avance desde el punto de vista de cómo cada lenguaje expone funcionalidades comunes a los programadores.-

Tambien aca podemos decir que esto existía, para determinados servicios, en los sistemas operativos propietarios de Digital e IBM.-

Sobre todo en los del primero, su sistema operativo VMS (hoy propiedad de Hewlett Packard) presentaba para la familia de lenguajes (Fortran, C, Cobol y Basic) idénticos nombres de rutinas para todo el sistema de I/O y de archivos ISAM.-

Solo que en estos casos un runtime que se adjuntaba al programa compilado era el encargado de proveer esta funcionalidad comun. Esto por supuesto duplicaba el codigo en cierto punto, aunque los servicios que utilizaba el runtime estaban en el kernel del SO por lo que no era tan problemático.-

¿Como funciona esto y para que sirve realmente?

Vamos a hacer un par de aplicaciones que nos muestren como funciona esto.

Antes de avanzar nos gustaría que repasemos como, en un lenguaje como el C, resolvemos un problema común:

Queremos invocar un método, que esta en una biblioteca CUYO ENLACE no se produce cuando compilamos, sino en tiempo de ejecución.-

Esto da a nuestros programas otra versatilidad. Es decir nuestro programa es capaz de:

Cargar una biblioteca a traves de su nombre LITERAL. Es decir un string en tiempo de ejecucion (por ejemplo leido de un archivo de configuración) dira cual es la biblioteca a a cargar)

Colocar un apuntador a una funcion de dicha biblioteca (definible tambien como un string)

Hay alguna habilidad que debe tener el SO en el que trabajamos y es la de aceptar bibliotecas compartidas. En Windows estas tienen la extension .DLL y en UNIX la extension .SO

Es decir la biblioteca que invocaremos desde nuestra aplicación sera del tipo compartida (Shared) y no del tipo estatica (STATIC).-

El ejemplo lo haremos para UNIX solo para ver alguna pequeña diferencia de cómo lo hariamos en Windows.-

El programa que haremos sabrá cargar una biblioteca por nombre. En nuestro caso los nombres son lib1.so y lib2.so. Estos nombres son totalmente arbitrarios y podrían ser cualquier otro valido para el Sistema operativo.-

El programa cargara alguna de estas 2 bibliotecas y llamara la función saludo().

Esta función existe en ambas bibliotecas y el programa es capaz de trabajar con esta redefinicion (override) ya que dependera de la biblioteca que cargue lo que determinara cual es la tarea que termine realizando.-

Este es el codigo de una biblioteca escrito en ANSI C:


#include <stdio>;

#include <dlfcn.h>; /* bibliotecas de manejo dinamico de funciones:

dlopen(), dlclose(), dlsym() ... */


/* Explicamos como usar el programa. */

void usage(int argc, char* argv[])

{

fprintf(stderr, "Usar asi: %s [12] a los efectos de llamar a la lib1 o a la lib2 \n", argv[0]);

exit(1);

}


int main(int argc, char* argv[])

{

int lib_num; /* Que biblioteca usar ? */

char biblioteca[100]; /* nombre del archivo de la biblioteca */

void* lib_handle; /* manejador para la biblioteca compartida cargada*/

void (*fn_de_la_biblioteca)(); /* puntero a UNA FUNCION de la biblioteca*/

const char* error; /* por si hay errores... */


/* necesito un argumento */

if (argc != 2)

usage(argc, argv); /* Chau!! */


/* Esta es la biblioteca a cargar. */

lib_num = atoi(argv[1]); /* */


if (lib_num < 1 lib_num > 2)

usage(argc, argv); /* Chau !! */



/* ahora armo el nombre de la biblioteca concatenando la palabra "lib" con el numero 1 o 2

para obtener lib1.so o lib2.so */

sprintf(biblioteca, "lib%d.so", lib_num);



/* carga la biblioteca que desea (lib1.so o lib2.so) */

lib_handle = dlopen(biblioteca, RTLD_LAZY);

if (!lib_handle) {

fprintf(stderr, "Error: %s\n", dlerror());

exit(1);

}


/* cargar la funcion saludo QUE ESTA EN CUALQUIERA DE LAS DOS BIBLIOTECAS !*/

fn_de_la_biblioteca = dlsym(lib_handle, "saludo");


error = dlerror();

if (error) {

fprintf(stderr, "Error: %s\n", error);

exit(1);

}


/* Llamar a la funcion. */

(*fn_de_la_biblioteca)();


/* liberar el recurso */

dlclose(lib_handle);


return 0;

}


Este modulo es LIB1.SO:


#include <stdio>;


/* Esta es la funcion de la biblioteca*/

void saludo()

{

printf("Que puedo decir que no sea HOLA MUNDO?\n");

}


/* cleanup de la biblioteca */

void _fini()

{

printf("Limpiando la biblioteca 'lib2.so'\n");

}


Este modulo es LIB2.SO:

#include <stdio.h>;


/* funcion de inicializacion: OBLIGATORIO llamarla '_init'. */

void _init()

{

printf("Inicializando biblioteca 'lib1.so'\n");

}


/* Esta es lo funcion que nos importa */

void saludo()

{

printf("HALOAA !!\n");

}


Como vemos este problema responde a un patrón común: La resolución en tiempo de ejecución de una funcionalidad determinada.-

Los lenguajes modernos como C# y Java resuelven esto con el paradigma denominado programación reflexiva.

Como en muchos otros conceptos de estos lenguajes modernos, las ideas se generaron en Smalltalk y C++.-


El enfoque en C#

El cargador del lenguaje común administra lo que se denominan DOMINIO de APLICACIONES.

El lenguaje común es el CONTENEDOR que aísla al programa del SO. Cualquier aplicación desarrollada en .NET no se comunicara directamente con APIs de WIN32, tal como lo hacíamos anteriormente sino a través de funciones, servicios, etc. del contenedor.-

EL DOMINIO de la APLICACIÓN es el contorno o los limites en los que trabajan aquellos objetos que pertenecen al mismo alcance de aplicación.-

La administración de este cargador incluye la carga de cada assembly en el dominio de aplicación adecuado y controla la disposición y arreglo en memoria del tipo de jerarquía en cada assembly.-

Veamos unos ejemplos.-

Ejemplo I:

En este caso haremos un proyecto que contendrá las clases que queremos acceder a través de Reflection.-

Es por eso que utilizamos el método estático:

Assembly.GetExecutingAssembly();

para poder tener un objeto de la clase Assembly.

Este es el modo de hacerlo cuando lo que necesitamos es un objeto de una clase que está dentro del código actualmente en ejecución.-

Obtenemos un objeto de la clase Assembly de este modo: Assembly assem = new Assembly())

Este el objeto creado nos permitirá, por ejemplo, instanciar objetos de las clases contenidas en el assembly.-


Para eso el primer paso es:

¿Como acceder a una clase del assembly?


El objeto de la clase Assembly tiene un método llamado CreateInstance(). Este método admite como parámetro el nombre de una clase así como los parámetros que necesitase su constructor.


Este método es uno de los métodos capitales de la programación reflexiva ya que nos permite a través de un texto que representa el nombre de la clase, obtener un objeto que es una instancia de la misma.-


El código siguiente muestra la utilización del método CreateInstance():


// Crear un objeto de la clase Ejemplo2, desde el assembly que la contiene

// El constructor de Ejemplo2 necesita un parametro que es un entero

//


Object o = assem.CreateInstance("Ejemplo2", false,

BindingFlags.ExactBinding,

null, new Object[] { 2 }, null, null);


Observe que se accede a una clase y se instancia un objeto de la misma a través de un parámetro que es un texto ("Ejemplo2").-

Observe también que uno de los parámetros de CreateInstance() es un array de Object al que se le envían los parámetros QUE EL CONSTRUCTOR de la clase necesita.

Esto es: No es suficiente con conocer el nombre de clase en el assembly sino que es preciso conocer algo más de ella.

Esto es similar a lo que nos sucede en el programa C que vimos al inicio en el que debemos conocer algo del PROTOTIPO de la función para hacer una llamada exitosa.-

¿Cómo acceder a un método de la clase?

Ahora bien, tenemos un objeto O que es una instancia de una clase del assembly.

¿Cómo hacemos ahora para acceder a un método de dicha clase?

El handler al assembly (assem) tiene un método llamado GetType que obtiene información sobre el tipo exacto que se produce en tiempo de ejecución para el objeto corriente.-

Es decir GetType retorna un tipo (Type). La clase Type es la base de reflection para acceder a la metada del objeto.

Type es una interfaz que tiene distintas implementaciones. La que aquí nos interesa, tiene un método llamado GetMethod() que, tomando como parámetro el nombre del método, nos trae información del mismo. Asi:



MethodInfo m = assem.GetType("Ejemplo2").GetMethod("MetodoDeEjemplo");



Obtenida la información sobre el método, se lo puede invocar a través del método Invoke de MethodInfo .

Igual que en el caso anterior, si bien es posible obtener acceso al método a través de un literal (en este caso el nombre del método es : MetodoDeEjemplo ), el uso posterior del método requiere conocer cuáles son los parámetros adecuados.


Object ret = m.Invoke(o, new Object[] { 42 });


En este caso el entero 42 se corresponde al tipo de datos que corresponde al Método, MetodoDeEjemplo

EL retorno que produce Invoke es un Object. EL uso posterior del mismo será parte de la lógica de negocios del programa.-

Arme un proyecto de consola simple y coloque el código indicado en un modulo cs (C#)


Y pruebe este código:


using System;

using System.Collections.Generic;

using System.Text;

using System.Reflection;

using System.Security.Permissions;

using System.Configuration;




class Ejemplo2

{

private int factor;

public Ejemplo2(int f)

{

factor = f;

}


public int MetodoDeEjemplo(int x)

{

Console.WriteLine("\nEjemplo2.MetodoDeEjemplo({0}) ejecutado.", x);

return x * factor;

}


public string OtroMetodo(string a)

{


StringBuilder st = new StringBuilder("A este dato le concateno: ");

Console.WriteLine("\nEjemplo2.OtroMetodo({0}) ejecutado.", a);

st.Append(a);

return st.ToString();

}



}


public class ejecuta

{


public static void Main()

{


Assembly assem = Assembly.GetExecutingAssembly();


Console.WriteLine("Nombre completo del Assembly:");

Console.WriteLine(assem.FullName);


//

AssemblyName assemName = assem.GetName();

Console.WriteLine("\nName: {0}", assemName.Name);

Console.WriteLine("Version: {0}.{1}",

assemName.Version.Major, assemName.Version.Minor);


Console.WriteLine("\nAssembly CodeBase:");

Console.WriteLine(assem.CodeBase);


// Crear un objeto de la clase Ejemplo2, desde el assembly que la contiene

//El constructor de Ejemplo2 necesita un parametro que es un entero

//

Object o = assem.CreateInstance("Ejemplo2", false,

BindingFlags.ExactBinding,

null, new Object[] { 2 }, null, null);


// Ahora apuntamos a un metodo del objeto:


MethodInfo m = assem.GetType("Ejemplo2").GetMethod("MetodoDeEjemplo");

Object ret = m.Invoke(o, new Object[] { 42 });

Console.WriteLine("MetodoDeEjemplo retorno: {0}.", ret);


MethodInfo m2 = assem.GetType("Ejemplo2").GetMethod("OtroMetodo");

ret = m2.Invoke(o, new Object[] { "Este Texto" });

Console.WriteLine("OtroMetodo retorno: {0}.", ret);


Console.WriteLine("\nPunto de entrada del Assembly:");

Console.WriteLine(assem.EntryPoint);

}

}


La ejecución del código nos muestra lo siguiente:



Algo más al respecto


Los assemblies contienen módulos, los módulos contienen tipos y los tipos contienen funciones miembro o métodos.

-

Lo que Reflection provee son objetos que encapsulan a estas unidades (assemblies, módulos y tipos) de modo de crear dinámicamente una instancia de un tipo, ligar ese tipo a un objeto existente u obtener el tipo de un objeto existente.-

De este modo podemos invocar los métodos del tipo y/o acceder a sus campos y propiedades. El uso típico de reflection debería seguir estas reglas:

  • Usar la clase Assembly para definir y cargar assemblies, cargar los módulos que el assembly contiene y que se encuentran en el Manifiesto, ubicar el tipo del assembly en si y crear una instancia del mismo.-

   Esto lo podemos hacer con:


 Assembly a = Assembly.LoadFrom(args[0]);

O


Assembly a = Assembly.GetExecutingAssembly();


EL primer modo es genérico y solo requiere que se indique el assembly. (El paso completo del mismo será obligatorio si no se encuentra en la GAC). El segundo método está restringido a el trabajo con clases que se encuentran en el assembly actual.-

  • Usar el método CreateInstance() para instanciar un objeto de un tipo dado:

Object o = assem.CreateInstance("Ejemplo2", false,

BindingFlags.ExactBinding, null, new Object[] { 2 }, null, null);


  • Alternativamente observemos que esto puede hacerse así:

MethodInfo m = assem.GetType("Ejemplo2").GetMethod("MetodoDeEjemplo");

Object ret = m.Invoke(o, new Object[] { 42 });


El método GetType retorna un objeto de la clase MethodInfo.

Observemos que igual que en el caso anterior, el nombre de la clase y el de su metodo, SON LITERALES lo que indica que pdrian ser cargados en tiempo de ejecucion en variables string y la llamada a Assembly.GetType o a Assembly.CreateInstance se podria hacer con datos dinamicos.

Desde ya que esta es la funcion ultima de Reflection.-

Observe de todos modos que debe conocer "algo" del tipo o del metodo de modo de invocar correctamente a los constructores o pasar parametros adecuados a los metodos.-


EJEMPLO II


En este ejemplo haremos una variante que nos llevara a un caso más parecido al que planteamos al principio de esta y que mostramos con un ejemplo en lenguaje C.-

Construiremos 3 proyectos, ambos en la misma solucion solo a los efectos de la simplicidad.

Uno, al que llamaremos biblioteca1, correspondera al armado de una biblioteca donde estan las clases de las que crearemos objetos a los que a accederemos.-

El segundo, al que llamaremos pruebabiblioteca, sera una aplicacin intermedia de ayuda. Esto no es necesario para el proyecto final que es una aplicación que usando reflection hara uso de los metodos de la biblioteca. Pero es una biena idea probar la biblioteca SEPARADAMENTE antes de darle un uso generico. Es por eso que colocamos este proyecto dentro del ejemplo.-

El tercero, que es el que nos importa, sera la aplicación que a traves de Reflection accede a estos objetos y sus metodos en modo dinamico.-

El proyecto biblioteca1

El primer proyecto sera del tipo Class Library y en el colocaremos una clase con dos metodos.-

Luego esta sera nuestra biblioteca armada como Assembly.-

Vamos con el primer proyecto que consiste en armar la biblioteca de la clase que expondra funcionalidad a ser utilizada por el segundo proyecto:


Este es un proyecto Class Library de modo de armar una biblioteca con una clase que exporte sus metodos.-

El codigo que escribiremos para crear tal clase es el que se muestra a continuacion:


using System;

using System.Collections.Generic;

using System.Text;


namespace biblioteca1

{

public class UnaClase

{

public static Boolean SinNumeros(string dato)

{

char []numeros = new char[]{ '0', '1', '2', '3', '4', '5', '6', '7', '8', '9' };

if (dato.LastIndexOfAny(numeros) == -1)

return (true);

else

return (false);

}


public static Boolean SinÑuflos(string dato)

{

char []ñuflos = new char[] { '@', '^', '`', '¨', '~' };

if (dato.LastIndexOfAny(ñuflos) == -1)

return (true);

else

return (false);

}


}

}




Ahora vamos a armar la biblioteca (biblioteca1) como una dll que expondrá la clase UnaClase y sus métodos públicos.-


Esto generara el archivo biblioteca1.dll .-

Revise en la pantalla de salida donde queda generada:




El proyecto PruebaBiblioteca

Ahora vamos a construir el segundo proyecto:



Vamos a hacer una aplicación de consola que utilice la biblioteca creada en el proyecto anterior.

Lo hará del modo habitual, es decir sin utilización de reflection.

EL código C# de este proyecto esta en un solo modulo al que llamaremos Program y es el que se ve a continuación:

using System;

using System.Collections.Generic;

using System.Text;

using biblioteca1;


namespace PruebaBiblioteca

{

class Program

{

static void Main(string[] args)

{

string dato = "ABCDE";

if (biblioteca1.UnaClase.SinNumeros(dato) == true)

Console.WriteLine("{0}: No tiene numeros");

else

Console.WriteLine("{0}: Tiene numeros");


dato = "AB1C2DE";

if (biblioteca1.UnaClase.SinNumeros(dato) == true)

Console.WriteLine("{0}: No tiene numeros");

else

Console.WriteLine("{0}: Tiene numeros");


dato = "A~CDE";


if (biblioteca1.UnaClase.SinÑuflos(dato) == true)

Console.WriteLine("{0}: No tiene Ñuflos");

else

Console.WriteLine("{0}: Tiene Ñuflos");


}

}

}


¿ Como enlazar una biblioteca al proyecto ?

Para enlazar la biblioteca con el proyecto PruebaBiblioteca1 colóquese en la ventana "Solution Explorer" , coloque el mouse sobre el proyecto y pulsando el botón derecho elija Add Reference



Si la bibloteca esta bien construida la salida de la ejecucion de este programa sera algo asi:


El proyecto UsoDeReflection

Armemos ahora el tercer proyecto.-

También será un proyecto de consola.-

Siendo que en el código no haremos una mención explicita a los métodos de la biblioteca, sino que haremos un binding tardío, cuando el programa se ejecute NO NECESITAMOS referenciar la biblioteca1.

Si necesitaremos desde ya el paso completo de donde se encuentra.-

En mi caso la colocare en el disco C: dentro del directorio M3.-



Es por eso que en el codigo hemos colocado:


static void Main(string[] args)

{

Assembly a = Assembly.LoadFrom(@"c:\m3\biblioteca1.dll");

…..

}//-

Desde ya que esta mencion "hard-code" puede ser reemplazada por la lectura de un parametro en un archivo de propiedades o en un archivo de configuracion.-

¿Como acceder a los modulos de un assembly?

El primer metodo que invocamos (llamado VerModulosYTipos) hace una recorrida sobre los modulos del assembly.-

En este caso solo encuentra un solo modulo ya que la biblioteca1 fue armada con un unico archivo cs dentro de un UNICO namespace:



Sobre esto aclaremos que lo que define al modulo no es la cantidad de archivo C Sharp (archivos .cs) ya que en todo caso el archivo físico es una unidad de agrupamiento de código compilable. Pero en el momento de enlazar estos archivos compilados, existen unidades superiores al archivo físico, como:

  • La clase en la que participa ese archivo (recordar que varios archivos se pueden utilizar para componer una sola clase)


  • El namespace al que pertenece el código que está en el archivo.


Con esto presente podemos entender que:


  1. La proyecto con el que armamos la DLL es el que le da el nombre a la DLL.-
  2. Podemos también armar una DLL dentro de un proyecto que se encuentra a su vez dentro de una solución.-
  3. Este proyecto tendrá, en el momento del armado, un NAMESPACE por defecto. Esto significa que dentro del proyecto podemos tener varios NAMESPACES. Esto NAMESPACES permitirán agrupar clases dentro de sí. Esa es la función básica del NAMESPACE: Un agrupador de clases que a criterio del diseñador del sistema deben estar bajo un espacio común.-
  4. El NAMESPACE mas el nombre de la CLASE es lo que denominados nombre calificado completo de la clase.-
  5. El NAMESPACE mas el nombre de la CLASE mas el nombre del método y sus parámetros identifican de modo UNIVOCO dentro de una solución la llamada a un método de una clase.-
  6. Un proceso ejecutándose es un espacio en el que existen recursos de procesador (tiempo y memoria) y en el que todas las partes participantes (métodos de clases y atributos en última instancia) deben estar unívocamente identificados.-


Dentro de una DLL cada MODULO es cada uno de los PROYECTOS que figura dentro de la SOLUCION. (Vea el Ejemplo III para mas detalles sobre estos puntos) .-


El trabajo de VerModulosYTipos es tomar los modulos del assembly en un array de Module[]


Module[] m = a.GetModules();


y de cada modulo (en este caso uno solo) revisar los tipos que los componen.-





for (int j = 0; j < m.Length; j++)

{

……


Type[] tipos = m[j].GetTypes();


for (int i = 0; i < tipos.Length; i++)

{

Type esteTipo = tipos[i];

……..

}

}


Nuestra intencion es utilizar aquellos tipos que nos permitan instanciar directamente. Es decir clases publicas.

Por eso es importante saber mirar esas propiedades o atributos. Se llaman IsClass e IsPublic y lo usamos en:


Console.Write ("Nombre del tipo {0}. Es una clase ? {1}",nombre,esteTipo.IsClass);


y


Console.WriteLine("Es publico ? {0}. ", esteTipo.IsPublic);


Esta es la salida por pantalla de la parte correspondiente a la información sobre los tipos que existen en el modulo





Accediendo a los metodos

El metodo VerLosMetodosDeLosTipos nos permite ver una tecnica para acceder a los metodos de una clase pública.-

Primero analizamos los atributos IsClass e IsPublic ya mencionados antes de llamar al metodo GetMethods de Type.-


if( esteTipo.IsClass && esteTipo.IsPublic)


Podemos tambien averiguar si el metodo es público o es un detalle de implementacion que no debe exponerse al mundo:


mi[k].IsPublic




Esta es la salida por pantalla de la parte correspondiente a la información sobre los metodos que tiene cada clase publica dentro del modulo



Pero para la invocación de un método precisamos saber algo más del mismo.-

Por ejemplo cuales son sus parámetros y quizás cuales son los tipos de dichos parámetros.-

No hay comentarios: