miércoles, 3 de junio de 2009

Primeros pasos en C++/CLI

En este articulo voy a hacer una introducción a algunos conceptos básicos de C++/CLI. A fin de que queden algunas cuestiones básicas claras de cara a otros artículos más avanzados que tengo en mente.

C++/CLI es una extensión del lenguaje C++ desarrollada por Microsoft e implementada por primera vez en el compilador Visual C++ incluida con Visual Studio 2008. Es decir, el lenguaje esta soportado a partir de la versión 2.0 del .Net Framework. Debemos entender que C++/CLI no es C++, sino una extensión del lenguaje. Es decir, se trata de C++ más características añadidas. C++, como lenguaje, sigue su propia evolución. De hecho, se espera próximamente una publicación del nuevo estandard del lenguaje.

¿Y que son estas extensiones que Microsoft ha añadido a su compilador de C++, y para que sirven?. Nada más fácil de responder. C++/CLI permite compilar código C++ a código manejado que se ejecuta sobre la plataforma .Net. Es decir, mediante el compilador de Visual C++ podremos desarrollar tanto aplicaciones nativas, aquellas que se ejecutan directamente sobre el S.O., como aplicaciones destinadas a ejecutarse sobre el .Net Framework. Y, lo mas interesante, en una aplicación C++/CLI, podremos mezclar tanto código nativo, como código manejado. Curiosamente, y sin mucho esfuerzo por nuestra parte, ambos mundos van a "entenderse" de una manera pacifica.

¿Que nos permite esto?. El objetivo de Microsoft es brindarnos una herramienta que nos permita enlazar código manejado, con código nativo. La librería de .Net, abarca muchas posibilidades, pero no todas (aunque, claro esta, en cada versión cada vez son menos las cosas que quedan fuera). En ocasiones, nuestra aplicación deberá realizar tareas cuya funcionalidad no esta implementada en la BCL (librería de clases .Net). Pero si que el API de Windows nos proporciona funciones para realizarlas. Una posible solución al problema seria acceder a las funciones del API mediante llamadas pInvoke. El problema de este acercamiento es que, nuestro código, tendría partes nativas, y su ejecución escaparía al control del runtime de .Net. La recomendación, desde Microsoft, es que las llamadas a código nativo queden "envueltas" en clases creadas en C++/CLI, y utilicemos dichas clases en nuestras aplicaciones. Dicho de otra manera, mas sencilla, C++/CLI, nos permite parsear llamadas al API de Windows.

Echa esta introducción, algo extensa, vamos a entrar en faena viendo algún ejemplo.

Vamos a comenzar creando una "tradicional" aplicación nativa. Para simplificar al máximo, vamos a crear una aplicación de consola (es decir, sin interfaz grafica de usuario). Para ello, abrimos Visual Studio (yo utilizo Visual Studio 2005, Visual Studio 2008 es perfectamente valido, pero no Visual Studio 2003). Desplegamos el menú Archivo->Nuevo->Proyecto. En el cuadro de dialogo, en Tipo de proyecto desplegamos la rama de Visual C++, seleccionamos el nodo Win32 y en el recuadro Plantillas seleccionamos Aplicación de consola Win32. Damos el nombre a nuestra solución, indicamos el directorio donde guardarla y pulsamos aceptar. En el asistente que aparece a continuación no tenemos porque tocar nada, las opciones por defecto nos son validas.

Bien, tenemos ya creado nuestro esqueleto de proyecto. Básicamente tenemos tres archivos .cpp, stdafx.cpp y stdafx.h. En mi caso, he llamado al proyecto ejemplo1. Asi que el primer archivo fuente se llama ejemplo1.cpp, a partir de ahora me deferiré a el, por comodidad, con este nombre. Abrimos, el contenido de este fichero, y borramos todo el código que el asistente nos ha generado. En su lugar creamos el código siguiente.

#include "stdafx.h"
#include
using namespace std
int main()
{
cout << "Hola mundo, desde código nativo" <<>
return 0;
}

Desplegamos el menú Generar->Generar Seguidamente abrimos una ventana de comandos y nos posicionamos en el directorio donde hayamos creado nuestra solución, dentro de el, veremos un directorio llamado Debug o Release dependiendo de en que modo hayamos compilado el programa. Entramos en el y veremos el ejecutable correspondiente a nuestra aplicación. Ejecutándolo, veremos el mensaje Hola mundo, desde código nativo, y volveremos al inductor de comandos.

Hasta aquí, lo que hemos echo no es mas que una aplicación (muy sencilla) C++ tradicional, que genera código ensamblador puro para la maquina con la que estemos trabajando. Ahora, vamos a ver de manera sencilla, como podemos generar código IL (Código intermedio o Código manejado) que se ejecuta sobre la maquina virtual del .Net Framework.

Volvamos a nuestro proyecto. Desplegamos el menú Proyecto->Propiedades de . En el cuadro de dialogo, a la izquierda, desplegamos la rama Propiedades de configuración y allí seleccionamos General. En el recuadro de la derecha, veremos una opción llamada Compatibilidad con el Common Language Runtime. Pinchando a la derecha de esta opción, vemos que se nos despliega una lista, con varias opciones. En este momento teniamos seleccionada la opción No es compatible con Common Language Runtime. Lo cual, hacia que nuestro compilador generase codigo ensamblador puro, codigo nativo. El resto de opciones, le diran al compilador que debe generar código IL, código manejado, compatible con el .Net Framework. Seleccionamos la segunda opción Compatible con Common Language Runtime (/clr). Esta opción permitirá que en nuestro programa se puedan realizar llamadas tanto a código nativo, como a código manejado. Aceptamos el cuadro de dialogo, y, ahora, si compilamos nuestra aplicación, estaremos generando código que se ejecutaría sobre el .Net Framework. Aunque realmente, no estamos haciendo uso de ningún elemento .Net. Vamos a cambiar esto.

En nuestro programa, para mostrar la salida de texto por consola, hemos usado la clásica clase C++ cout. En el mundo .Net, para mostrar texto por consola, utilizamos el método WriteLine de la clase Console. Bien, vamos a modificar nuestro código, para que nos muestre otro texto, pero a través de las clases de .Net. Nuestro código, quedaría como sigue.

#include "stdafx.h"
#include
using namespace std
int main()
{
cout << "Hola mundo, desde código nativo" << endl;
System::Console::WriteLine("Hola mundo, desde codigo manejado");
return 0;
}

Compilamos nuestro programa y lo ejecutamos. Veremos que el programa nos muestra, sin problemas, dos líneas de texto en pantalla. La primera línea la esta mostrando mediante llamadas a código nativo, mientras que la segunda, esta utilizando código manejado.

Vamos a ir un paso más allá. Vamos a crear un par de variables a fin de alojar valores dentro del conjunto de los enteros. En el mundo nativo, disponemos del tipo int. Mientras que en el mundo .Net, se nos proporciona el tipo System.Int32. Aunque normalmente al crear una variable de este tipo utilizamos un alias, int en C# o Integuer en Visual Basic. Vamos a crear una variable de cada tipo, dejando nuestro código como sigue.

#include "stdafx.h"
#include
using namespace std
int main()
{
int intValor1;
System::Int32 intValor2;
cout << "Hola mundo, desde código nativo" << endl;
System::Console::WriteLine("Hola mundo, desde codigo manejado");
return 0;
}

Tenemos que tener algo claro. Nuestro programa esta creando dos variables. Ambas variables son capaces de contener dos valores numéricos dentro del rango de los enteros de 32 bits. Pero, ¿donde se alojan dichas variables?. Esa es la clave. intValor1 es una variable "nativa". Con lo cual se creara en la pila (stack). Mientras que intValor2 es una variable "administrada". Es decir, una variable controlada por el .Net Framework. Con lo cual dicha variable se creara en la pila administrada, una memoria gestionada por el .Net Framework.

A todas luces, son variables cuya memoria ha sido reservada en "memorias" distintas. Imaginemos que queremos sumar el contenido de dichas variables, en principio, parecería imposible. Bien, pues no hay ningún problema, C++/CLI, se encargara de realizar, por nosotros, proxies entre una y otra zona de memoria de forma que podamos operar entre variables nativas y administradas.

Vamos a modificar nuestro programa, de forma que nos quede como sigue.

#include "stdafx.h"
#include
using namespace std;


int main()
{
System::Int32 intValor1;
int intValor2;
cout << "Hola Mundo, desde codigo nativo" << endl;
System::Console::WriteLine("Hola mundo, desde codigo manejado");
cout << "\n\rIntroduce un numero: ";
cin >> intValor1;
System::Console::Write("Introduce un segundo numero: ");
intValor2 = System::Int32::Parse(System::Console::ReadLine());
cout << intValor1 + intValor2 << endl;
System::Console::WriteLine(intValor1 + intValor2);
return 0;
}

Este rebuscadísimo programa. Solicita dos números, los almacena en sendas variables, las suma y muestra el resultado por pantalla. Seria el programa más simple del mundo, salvo el interés de que estamos haciendo esto con dos variables muy diferentes. Una variable nativa y una manejada. Además, para solicitar datos al usuarios, estamos usando tanto código nativo (cuando empleamos la clase cin) como código manejado (cuando llamamos al metodo ReadLine de la clase Console). Fijémonos, además, que cuando pedimos un valor para almacenarlo en intValor1, estamos utilizando código nativo, y la variable es manejada. Lo contrario hacemos con la variable nativa intValor2, que estamos solicitando un valor por pantalla utilizando código manejado. Por ultimo, mostramos el resultado en pantalla tanto usando código manejado como nativo, y sumamos el contenido de ambas variables sin problemas. Insisto en que todo este trabajo lo realiza el compilador de C++/CLI "por debajo", de forma transparente para nosotros.

Bueno, hasta aquí esta introducción a C++/CLI. Podéis dejar cualquier comentario referente a cualquier error que haya podido cometer, o para consultar cualquier duda

No hay comentarios:

Publicar un comentario