Introducción a .NET

Prólogo

Este artículo es tan solo una somera introducción a la tecnología .NET dando un repaso a sus principales características y explicando de forma breve el funcionamiento del CLR y de los assemblies de .NET de forma que, cuando trabajemos con referencias a ensamblados o bien usemos el System.Reflection tengamos una idea de que estamos haciendo. Buena parte de lo que yo se de este tema viene de dos libros que recomiendo a todo aquel que quiera tener una visión más detallada de .NET (ambos en inglés aunque no se si existen también en español)

  • Professional .NET Framework. Joe Duffy. Yo recomendaría este libro para empezar a conocer los entresijos de .NET, es decir, como funciona el CLR, que es el CIL, las librerías del framework... (aunque es muy recomendable por lo menos conocer c#, que es el lenguaje en el que están la mayoría de los ejemplos, y haber hecho algún programilla aunque sea pequeño)
  • Essential .NET, Volume 1. Don Box. Es un libro bastante exhaustivo que describe los fundamentos de .NET y del CLR con mucha eficacia aunque, como el mismo Don Box advierte en el prefacio, de forma bastante ardua, "poco para principiantes".

Ensablados o Assemblies

Durante este artículo (y de hecho probablemente en cualquier artículo de esta web) utilizo el término ensamblado o assembly para referirme a los Assembly de .NET, Assemly es el nombre ingles y ensamblado la traducción más adecuada española. En general puesto que el término español es básicamente correcto lo utilizaría y dejaría de lado el anglicismo pero, en programación en general y en .NET en particular, es común encontrar referencias a los términos ingleses en prácticamente cualquier lugar de forma que es facil mantener una conversación en la que se hable de GC, GAC y assemblies en vez de RB (por recolector de basura) y CEG (por cache de ensamblados global).

¿Qué es .NET?

Net Layers.preview.JPG

.NET es el nombre que engloba una tecnología creada por Microsoft como un intento (o un paso adelante) de mejorar la tecnología COM.

La técnología COM (Common Object Model), para quién no haya oido hablar de ella, es una tecnología que pretende ser (y en gran parte es) un protocolo de comunicación de aplicaciones (locales o remotas) independiente del lenguaje en que estén definidas dichas aplicaciones. COM introdujo conceptos tales como la separación bien definida de interfaces de implementación así como la definición de acuerdos entre las aplicaciones implicadas en cuanto a la forma de comunicarse. Sin embargo COM presentaba una serie de problemas tanto en cuanto a su definición como a su uso.

Para solucionar los problemas emergentes de .NET (ver el libro de Don Box citado en el prologo) Microsoft comenzó a trabajar en una extensión de COM que evitara dichos problemas y que terminó convirtiendose en el CLR (common language runtime) que constituye la base fundamental de .NET

.NET, CLR, CLI, CTS y CIL

Hay unos cuantos acrónimos que se repiten bastantes veces cuando lees documentación sobre .NET, CLR (common language runtime), CLI (common language infrastructure), CTS (common type system), CIL (common intermediate language).

  • El '''CLI''' constituye la especificación básica del .NET, es decir, el funcionamiento y arquitectura del sistema. Durante el desarrollo del CLR Microsoft ha mandado la especificación de dicho CLR a varias organizaciones de estandares, entre ellas especialmente la ECMA. De esta forma el CLI constituye una especificación para un sistema que proporciona todas la características que .NET ofrece (seguridad de tipos, administración de código, lenguaje intermedio...)
  • El CLI define la especificación del '''CTS''' o sistema de tipos común que define un sistema de tipos y herencias que dan lugar (en la implementación de .NET) a la jerarquía de objetos.
  • El CLI define también la especificación del '''CIL''' que constituye el lenguaje intermedio, es decir, indica que toda implementación del CLI debe generar assemblies (ya veremos esto más adelante) utilizando un lenguaje intermedio que más adelante será compilado en función de la máquina en la que se esté ejecutando el código.
  • El '''CLR''' (que paradójicamente hay que explicarlo al final) constituye una implementación (de hecho la única implementación actualmente) del CLI.
  • Finalmente .NET engloba tanto el CLR como el .NET Framework, una serie de librerías adicionales (un framework de trabajo puesto que uno de los objetivos de .NET era competir con el J2EE) que proporcionan una enorme funcionalidad adicional

Código administrado

En .NET se dice que el código está administrado. Puesto que lo que realmente se ejecuta es código intermedio, dicha ejecución esta vigilada de forma que no pueda realizar acciones inapropiadas.

Recolector de basura

.NET incorpora un recolector de basura. Esto significa que no deberemos preocuparnos de liberar manualmente la memoria que vayamos dejando de utilizar ya que el recolector de basura (Garbage Collector o GC en inglés) se encarga de monitorizar toda la memoria utilizada, decidir si dicha memoria es accesible de alguna forma y, en caso de no serlo, disponer de ella de la forma más adecuada.

Seguridad de acceso a código (CAS)

.NET incorpora un sistema llamado CAS (Code Access Security o Seguridad de Acceso a Código) que permite restringir los permisos de ejecución de un determinado código de una forma pormenorizada. Esto significa que podemos definir que un determinado segmento de código (asociado a un determinado rol) tansolo tenga acceso de lectura a tres archivos completos.

Asi mismo podremos también configurar que nuestra librería, que proporciona una serie de servicios proporcione distinta accesibilidad dependiendo del nivel de acceso del programa llamante (por ejemplo podriamos evitar que una aplicación llamante cree nuevos documentos en función de su nivel de acceso, o solo pueda ver determinados documentos).

Pero yo soy anti M$

Si tienes algún princpio esencial en contra de Microsoft eso no debe constituir una barrera. Existen varios desarrollos libres que implementan (o están en proceso de implementar) la especificacion del CIL asi como otras partes de la especificación ECMA lo que permite que ejecuten código intermedio y por tanto assemblies (y ejecutables) generados en dicho lenguaje intermedio. Hasta donde yo conozco hay dos implementaciones principalmente aunque ninguna de las dos está completa (especialmente la nueva funcionalidad del frameworw NET 2.0).

  • [http://www.mono-project.com/Main_Page El proyecto Mono.] El proyecto mono es el nombre de un proyecto iniciado por [http://es.wikipedia.org/wiki/Miguel_de_Icaza Miguel de Icaza] y adquirido más tarde por Novell cuya finalidad era crear una serie de herramientas compatibles con .NET y que siguieran la especificación ECMA.
  • [http://www.gnu.org/projects/dotgnu/pnet.html Portable .NET.] El proyecto Portable .NET es el equivalente al proyecto Mono pero desarrollado por la FSF (Free Software Foundation).

Estos dos proyectos permiten ejecutar proyectos .NET tanto en windows como en Linux (y creo que también en otros sistemas operativos como FreeBSD, MacOSX, etc)

CIL, Assemblies y JIT (Just In Time) Compilation

Código intermedio, la cabecera PE y Mono

Cuando generamos un ejecutable o una dll .NET en realidad lo que se genera no es código ensamblador (código objeto si hilamos fino) como ocurría si compilabas con Visual Studio 6 o si compilas con Delphi, sino que se genera una estructura especial llamada assembly (ensamblado) que contiene la "traducción" de nuestro código fuente a código intermedio que es la base de .NET.

La cabecera PE (portable executable) es la estructura de los ejecutables (tanto .exe como .dll) de Windows. .NET podriamos decir que extiende o modifica esa cabecera de forma que los ejecutables de .NET puedan ejecutarse de forma totalmente transparente.

En un ejecutable hay diversos secciones (dividas en segmentos), código, de datos y pila. Un ejecutable tradicional contiene la cabecera PE seguida de la sección de código tras las cuales están el resto de secciones en caso de que sean necesarias. En un ejecutable .NET se mantiene la cabecera PE tras la cual hay un pequeño bootstrapper que se encarga de cargar el CLR, concretamente el loader del CLR que a su vez accede a la sección de texto del ejecutable en la cual está ubicada el código intermedio (más concretamente la versión binaria del código intermedio) de la aplicación. De está forma cuando windows ejecuta el archivo el CLR es invocado automáticamente y se hace cargo de la ejecución del código de forma transparente al usuario.

Mono realiza una función parecida pero puesto que la forma de ejecutar con mono es mono.exe nombre_ejecutable sobra la parte de invocar al CLR de forma que directamente accede a la sección de texto donde está ubicado el código intermedio y realiza su ejecución (esto significa, aunque realmente no estoy seguro por que no lo he probado, que un ejecutable generado con el compilador de mono debe ser ejecutado utilizando mono puesto que no contiene el bootstrapper sino tansolo el código intermedio).

Assemblies

Un assembly es la una unidad lógica básica completa de .NET que encapsula uno o más modulos de código. Digo completa por que en realidad puede generarse una estructura asociada a un módulo de código (y que recibe el mismo nombre) y que no podrá ejecutarse ni utilizarse de ninguna forma excepto para ser compilada dentro de un assembly.

Cada assembly contiene una serie de metadatos que definen la información contenida en él. Dichos metadatos incluyen información tal como los assemblies externos que utiliza, las clases, tipos y metodos que están definidos. Un assembly puede englobar a varios modulos de código englobando funcionalidad que, por razones semánticas podemos querer tener en varios archivos dentro de un mismo ejecutable.

La única diferencia (en Windows) entre un .exe y un .dll (en .NET) consiste en que el primero contiene un punto de entrada que debe ser único y que define el lugar en el que comenzará la ejecución así como un bootstrapper que se encarga de cargar el CLR.

Referencias (Carga estática de assemblies)

Cuando programamos en .NET a veces necesitamos utilizar clases y funcionalidad ubicada en algún assembly externo (de hecho siempre utilizamos como minimo el assembly mscorlib que contiene el System.Object asi como gran parte de las librerías básicas de .NET). Una referencia no es más que la forma de indicar ese uso y de hecho se traducen a CIL como una entrada .assembly extern mscorlib y podremos ver instrucciones como isinst [mscorlib]System.Object.

JIT Compilation

Como ya he mencionado el código que .NET genera (excepto alguna excepción) es código intermedio. Este código intermedio no es interpretado durante la ejecución sino que es compilado cuando es necesario. Dicha compilación se produce de forma vaga (en ingles lazyness compilation), lo que significa que tan solo se realiza cuando (y si) el código se va a utilizar.

El mecanismo de funcionamiento, a grandes rasgos, consiste en que, durante la primera ejecución la tabla de metodos de las clases contienen referencias a pequeños trozos de código del JIT. Cuando se realiza una llamada al metodo de una clase la dirección de llamada que se obtiene no es realmente el código ensamblador de dicho metodo sino una referencia de activación al JIT que en ese momento realiza la compilación del código intermedio. Una vez compilado modifica la dirección en la tabla de metodos de forma que las llamadas subsiguientes ejecuten directamente el código ya compilado.

System.Reflection

Al igual que existe la posibilidad de cargar assemblies de forma estática tenemos la opción de realizar la carga de assemblies de forma dinámica mediante la librería System.Reflection.

Con las dll tradicionales lo único que teníamos era una tabla de direcciones que contenía los offsets de las funciones que la dll exportaba... y eso era todo, no podíamos saber que parametros esperaba la dll lo cual, entre otras cosas significaba que los parametros que pasarmos quedaban codificados como simples direcciones de pila que, posiblemente, podrían ser ejecutadas por la dll o quedar corrompidas o mil cosas más. Además, al no haber propiamente un versionado de dlls podías estar llamando a una versión posterior o anterior cuya especificación no se correspondía en absoluto con lo que uno esperaba. A todo esto terminó conociendoselo como "the dll hell" (el infierno de las dll) debido a los múltiples problemas que ocasinaba.

En primer lugar hay que entender que gracias a la estructura de un assembly, que contiene toda la metainformación sobre lo que este contiene, no solo tenemos la posibilidad (como ocurría con las dll) de invocar determinadas funciones sino que podemos obtener una funcionalidad mucho más amplia.

Al disponer de toda esa información el CLR puede administrar las llamadas al código del assembly de forma que no hagamos cosas extrañas ni "acabemos en medio de memoria incorrecta" (The IT Crowd) ;).

Por otro lado podemos examinar el assembly para descubrir y analizar la funcionalidad que proporciona, buscando por ejemplo clases que implementen un determinado interface, creando instancias dichas clases y tratandolas como si pertenecieran a nuestro propio código.

Assembly.Load(ing)

La librería System.Reflection proporciona diversos metodos estáticos dentro de la clase Assembly que nos permiten abrir un ensamblado. Los más comunes son:

public static Assembly Load (
    AssemblyName assemblyRef
)

public static Assembly LoadFile (
    string path
)

que devuelven una instancia de la clase Assembly referida al assembly que acabamos de cargar y que nos permitirá obtener información tal como obtener todos los tipos definidos en el assembly, crear instancias de objetos definidos, obtener una lista de los metodos y parametros de uno de los tipos que ya hemos obtenido ...

System.Reflection.Emit y System.CodeDom

Otra de las cosas que permite .NET es emitir código en tiempo de ejecución, es decir, emitir lineas de código intermedio que serán compiladas por el JIT en tiempo de ejecución y obtendrán código ensamblador que ejecutará como si fuera código fuente que hubieras compilado con el VS.

Para emitir el código fuente .NET nos proporciona varias librerías (a varios niveles). Estás librerías son principalmente System.Reflection.Emit y System.CodeDom. Con System.Reflection.Emit podemos por ejemplo crear un nuevo assembly, asignarle un nombre, definir modulos y tipos, asignar campos a los tipos, definir el constructor, definir metodos ... etc

P/Invoke (Platform Invoke)

Aunque el Framework de .NET es muy amplio y complejo, existen todavía algo de funcionalidad de la API que no es alcanzable. Así mismo, en ocasiones necesitamos acceder a funcionalidad que está ubicada en dlls antiguas (lease no .NET) y que no podemos (o no queremos) suplir con las clases del Framework.

P/Invoke define el servicio de invocación de funciones proporcionados por la plataforma, es decir, funcionalidad de librerías pre-CLR.

El atributo DllImport

El atributo DllImport, ubicado en en namespace System.Runtime.InteropServices nos permite realizar dichas invocaciones, por ejemplo

[DllImport("user32.dll")]
static extern bool CloseWindow(IntPtr hWnd);

La declaración del metodo se realiza mediante la palabra reservada extern que indica que dicho metodo no se encuentra ubicado en el ensamblado actual. Entre parentesis pasaremos el nombre de la librería que contiene el metodo que deseamos utilizar.

DllImport tiene varios parametros:

  • Value: El nombre de la librería que es obligatorio
  • EntryPoint: No es obligatorio que el nombre del metodo al que llamamos se corresponda exactamente con el ubicado en la librería
           [DllImport("user32.dll" EntryPoint = "CloseWindow")]
           static extern bool CierraVentana(IntPtr hWnd);
           
  • SetLastError: Muchas funciones de las librerías de windows utilizan la función de windows SetLastError, para garantizar que el error que recobramos sea correcto este parametro debe ser true.

"The Windows way"

En windows la carga de librerías mediante P/Invoke es un proceso que sigue un estandar muy sencillo, cuando se encuentra un [DllImport] se genera el código correspondiente a un LoadLibrary y un GetProcAdress.

"The Mono way"

En mono la cosa es un poco más complicada. Puesto que mono es multiplataforma, diversas librerías pueden diferir en sus nombres entre una instalación Windows, Linux o MacOSX, asi como pueden diferir su extensión (por ejemplo milib.so.1.2 vs milib.dll). No es el objetivo de este artículo explicar como logra mono realizar de forma transparente el mapeado de los nombres de librería (para más información ver este excelente artículo de Jonathan Pryor sobre P/Invoke) pero baste decir que lo hace de forma que los mismos [DllImport] que funcionan en Windows funcionarán en Linux (incluso en llamadas a librerías del sistema puesto que Mono se encarga en la mayoría de los casos de mapear dicha llamada a otra que realiza la misma función en windows).

0
No votes yet
Your rating: None