Cualquier cosa que valga la pena se hace en equipo

Cualquier cosa que valga la pena se hace en equipo

viernes, 4 de junio de 2010

Programacion Reflexiva . PARTE III

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:

1. 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.-

2. 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 hara del modo habitual, es decir sin utilizacion de reflection.
EL codigo C# de este proyecto esta en un solo modulo al que llamaremos Program y es el que se ve a continuacion:

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");

}
}
}

Programacion Reflexiva . PARTE II



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 contendra las clases que queremos acceder a traves de Reflection.- Es por eso que utilizamos el metodo estatico: 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 esta dentro del codigo actualmente en ejecucion.- Obtenemos un objeto de la clase Assembly de este modo : Assembly assem = new Assembly()) Este el objeto creado nos permitira, 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 metodo 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 codigo siguiente muestra la utilizacion del metodo

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 traves de un parametro que es un texto ("Ejemplo2").-
Observe tambien que uno de los parametros de CreateInstance() es un array de Object al que se le envian los parametros 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 mas 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 funcion para hacer una llamda exitosa.-

¿Como acceder a un metodo de la clase?

Ahora bien, tenemos un objeto O que es una instancia de una clase del assembly. ¿Como hacemos ahora para acceder a un metodo de dicha clase?
El handler al assembly (assem) tiene un metodo llamado GetType que obtiene información sobre el tipo exacto que se produce en tiempo de ejecucion 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í no sinteresa, tiene un metodo llamado GetMethod() que, tomando como parametro el nombre del metodo, nos trae información del mismo. Asi:

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

Obtenida la información sobre el metodo, se lo puede invocar a traves del metodo Invoke de MethodInfo .
Igual que en el caso anterior, si bien es posible obtener acceso al metodo a traves de un literal (en este caso el nombre del metodo es : MetodoDeEjemplo ), el uso posterior del metodo requiere conocer cuales son los parametros adecuados.

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

En este caso el entero 42 se corresponde al tipo de datos que corresponde al Metodo, MetodoDeEjemplo EL retorno que produce Invoke es un Object.

EL uso posterior del mismo sera parte de la logica de negocios del programa.-

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
















Y pruebe este codigo:

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);
}
}

Ejecute el codigo y vera que nos muestra lo siguiente:

El nombre del assembly
El metodo que hemos ejecutado y el valor que retorna el mismo
El punto de entrada del assembly

La programacion Reflexiva-PARTE I

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 sabra cargar una biblioteca por nombre. En nuestro caso los nombres son lib1.so y lib2.so. Estos nombres son totalmente arbitrarios y podrian ser cualquier otro valido para el Sistema operativo.-
El programa cargara alguna de estas 2 bibliotecas y llamara la funcion saludo().
Esta funcion 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.h";
#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 ;

/* 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 ;

/* 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 patron comun: La resolucion en tiempo de ejecucion de una funcionalidad determinada.-
Los lenguajes modernos como C# y Java resuelven esto con el paradigma denominado programacion reflexiva.
Como en muchos otros conceptos de estos lenguajes modernos, las ideas se generaron en Smalltalk y C++.-