WinRT ¿Cómo hemos llegado aquí?

Windows 8, Windows Phone, Windows RT, cada uno diferente y sin embargo todos se apoyan en WinRT… pero ¿qué es realmente WinRT y por qué lo necesitábamos?

Introducción

Antes de hablar de WinRT vamos a explicar qué nos ha traído hasta aquí.

Win32

Windows expone a las aplicaciones una gran cantidad de APIs para realizar multitud de funcionalidad de bajo nivel. Estas APIs reciben el nombre de Windows API, pero existen diferentes implementaciones dependiendo de la arquitectura de la máquina. Tenemos Win16 para sistemas de 16 bits, Win32 para sistemas de 32 bits, Win64 para sistemas de 64 bits y WinCE para Windows CE.

Estas APIs las proporciona el sistema operativo mediante librerías del sistema (e.g. kernel32.dll, user32.dll, etc…). Su misión es proporcionar a las aplicaciones acceso a las funciones del hardware que puedan necesitar. Sin embargo, no facilitan ningún tipo de comunicación entre procesos, para eso tenemos COM.

COM

COM nace como una forma de hacer comunicación interprocesos en 1992. Esto permitía que varias aplicaciones pudieran trabajar juntas e incluso una embebiera parte de la funcionalidad de otra.

Un ejemplo de esta colaboración es el componente COM de Internet Explorer, que permite que cientos de aplicaciones hagan renderizado web de forma sencilla.

COM fue creado casi una década antes del lanzamiento de .NET, por lo que se trata de código no manejado. Cuando una aplicación .NET quiere hacer uso de un componente COM, debe hacerlo a través de un Runtime Callable Wrapper. Básicamente es una técnica que permite llamar a código no manejado desde código manejado dejando que el objeto RCW haga de proxy y se encargue del marshaling. Llamámos marshaling al proceso por el cual la representación en memoria de un objeto se recoloca de otro modo que nos permita trabajar con él de forma adecuada, en este caso, desde código manejado.

Del mismo modo, un componente COM puede hacer uso de código manejado mediante un COM Callable Wrapper, que hace el trabajo inverso del RCW.

Es posible usarlas desde código administrado mediante DllImport:

    1: [DllImport("kernel32.dll")]
    2: public static extern Int32 CloseHandle(IntPtr hObject);
    3: [DllImport("kernel32.dll")]
    4: public static extern IntPtr OpenProcess(UInt32 dwDesiredAccess, Int32 bInheritHandle, UInt32 dwProcessId);
    5: [DllImport("kernel32.dll")]
    6: public static extern Int32 WriteProcessMemory(IntPtr hProcess, IntPtr lpBaseAddress, uint[] lpBuffer, UInt32 nSize, IntPtr lpNumberOfBytesWritten);
    7:  
    8: private void WriteAddress(IntPtr address, uint value)
    9: {
   10:     var process = GetProcess();
   11:     var pHandle = OpenProcess(0x1F0FFF, 0, (UInt32)process.Id);
   12:     WriteProcessMemory(pHandle, address, new uint[] { value }, 4, (IntPtr)0);
   13:     CloseHandle(pHandle);
   14: }

Esta invocación de código no administrado es bastante simple porque el código objetivo está en una DLL y no espera el paso de ningún struct. Si necesitáramos usar aritmética de punteros o enviar structs, nos veríamos obligados a controlar el layout de memoria que el CLR esté usando.

Las Windows API permiten que nuestras aplicaciones interactúen con el sistema operativo, pero no que se comuniquen con otras aplicaciones. Para eso fue desarrollado COM.

Cuando venimos del mundo .NET esperamos que una aplicación C# se ejecute en código administrado, es decir, que su compilación produzca un código MSIL que sea interpretado por un CLR que sea el encargado de traducir eso a código nativo para la arquitectura sobre la que se esté ejecutando.

image

La importancia de la arquitectura

Como puede verse en el esquema, el Sistema Operativo no es consciente en ningún momento del código MSIL generado por el compilador de C#, sino que es el CLR quien lo interpreta y lo traduce a código nativo que pueda ser ejecutado en la máquina. Pero, si esto es cierto ¿por qué Visual Studio nos deja compilar nuestros programas C# para AnyCPU, x86 y x64? Respuesta corta: Interop.

image

Cuando queremos acceder a una librería que ha sido compilada para x86, nuestra aplicación debe estar también compilada para x86. ¿Qué plataforma es entonces AnyCPU? Depende de la arquitectura real de la máquina local. Si ejecutamos nuestra aplicación en una máquina x86, el CLR convertirá el código MSIL en código nativo x86 y no tendremos problemas en llamar a una dll de 32 bits. Sin embargo, también podría ocurrir que nuestra aplicación fuera ejecutada en un sistema x64. Dado que nuestra aplicación es AnyCPU, el CLR convertirá el código MSIL a código x64 nativo y seremos incapaces de cargar la dll de 32 bits.

Este problema puede solucionarse diciéndole al compilador de C# que la plataforma objetivo es x86. De este modo el CLR generará código de 32 bits aunque estemos ejecutándonos en una máquina x64, haciendo que la carga de la librería de 32 bits se produzca con WOW64 y no tengamos ningún problema.

Fíjate que en todo este proceso nuestro código MSIL no se ve afectado. Simplemente le decimos al CLR que debe iniciar nuestra aplicación en un entorno de 32 o de 64 bits.

Podemos verificar si un assembly ha sido compilado para x86 o x64 con el mandato:

    1: PS C:\Program Files (x86)\Microsoft Visual Studio 12.0\VC\bin> .\dumpbin.exe /headers ConsoleApplication.exe | grep machine
    2: 8664 machine (x64)

¿Qué ocurre con C++?

Las aplicaciones C++ son código no administrado. No se apoyan en el Framework .NET y no necesitan de ningún CLR, ya que el compilador genera directamente código nativo.

image

Esto quiere decir que las aplicaciones C++ están completamente desligadas del mundo .NET, donde el código era administrado y todo era paz y armonía gracias a cosas como el Garbage Collector. Para tratar de tener un puente entre ambos mundos y posibilitar a los desarrolladores de C++ el acceso al mundo .NET, se creó C++/CLI.

Esta mezcla permite a los desarrolladores de C++ llamar a cuantos métodos de .NET quieran sin tener que recurrir a invocaciones COM. Por ejemplo, podríamos tener el siguiente código C++/CLI atacando a stdio y System:

    1: int main(array<System::String ^> ^args)
    2: {
    3:     char s1[MAX_PATH + 52] = "Escribiendo con stdio y obteniendo el Path de .NET ";
    4:     strcat(s1, msclr::interop::marshal_as< std::string >(Environment::CurrentDirectory).c_str());
    5:     printf(s1);
    6:     Console::WriteLine(L"\nEscribiendo con .NET");
    7:     return 0;
    8: }

Si no habéis trabajando con C++/CLI quizá os estén extrañando los ^, que indican una referencia a un objeto manejado (estilo *, pero del mundo .NET). Por supuesto Visual Studio sigue admitiendo proyectos C++, pero está la posibilidad de crearlos C++/CLI.

¿Por qué nos importa el código no administrado?

El código administrado nos proporciona un importante incremento de productividad y facilita enormemente el mantenimiento del código, pero hay ocasiones en las que necesitaremos hacer uso de código no manejado creado por terceros, o hacer uso de APIs ajenas al Framework .NET. En esos momentos es cuando necesitamos construir un puente entre ambos mundos.

¿Qué es WinRT?

WinRT es una API que proporciona Windows. No es un reemplazo de Win32 per se, sino otra API que podemos usar. Al contrario de lo que ocurre con Win32, WinRT tiene en mente una programación más pragmática, destinada a acelerar el desarrollo de Apps. Las llamadas son simples, con acceso fácil a sensores, sistema de ficheros, etc.

La filosofía de WinRT no es facilitar a las Apps el acceso a bajo nivel del hardware, sino la de abstraerlas de la complejidad y proporcionar la información relevante. Teniendo en mente la naturaleza responsive de las Apps, todas las APIs que tarden más de 50 milisegundos, son asíncronas, forzando a que las Apps sean programadas de forma fluida.

Todas las APIs expuestas por WinRT se ejecutan en una sandbox, por lo que las Apps no pueden acceder a todas las funciones disponibles sino a aquellas a las que se les conceda permiso.

¿Por qué necesitábamos otra API?

La programación COM no es especialmente cómoda, y la invocación de código no administrado desde código administrado no ayuda demasiado a acelerar el desarrollo de Apps. Necesitábamos una forma cómoda de poder interactuar con el sistema.

Un paso más en la simplicidad: Proyecciones

La llamada de código administrado desde código no administrado y viceversa estaba solucionado con CCW y RCW, pero no era cómoda. Las proyecciones simplifican este problema.

Cuando llamamos a código nativo desde código no manejado, uno de los primeros problemas que podemos encontrar es la conversión de tipos. Si una API espera la llegada de un tipo que no existe exactamente en nuestro lenguaje, nos toca a nosotros realizar la conversión para poder trabajar contra esa API. Aquí es donde entran en juego las proyecciones.

WinRT exponen a cada lenguaje a través de las proyecciones. Cada lenguaje implementa esas proyecciones como prefiera, permitiendo que esas proyecciones sean iguales a la exposición original o actuar de intermediario. En el caso de C#, si una API espera una cadena de texto, será de tipo HSTRING, lo cual rompería la homogeneidad del código. Gracias a las proyecciones, el compilador de C# puede mapear automáticamente el tipo HSTRING al tipo String de C#.

La magia de las proyecciones es que pueden realizar bastante trabajo por nosotros. Al hacer una llamada a WinRT, el compilador puede crearnos los objetos proxy necesarios, abstrayéndonos totalmente de los CCW y los RCW.

C++/CX

Para llevar C++ a código manejado, se creó C++/CLI. WinRT es código nativo, por lo que C++ puede acceder a él. Sin embargo, esto nos deja en el primer caso, teniendo que lidiar con objetos COM a mano. C++/CX lleva a C++ un cambio de sintaxis para permitirle al compilador manejar por nosotros los objetos COM, pero haciendo target a código nativo, sin que el CLR o el Framework .NET tengan algo que ver. Esto también quiere decir que no existe recolector de basura, sino que se hace un conteo de referencias.

¿Entonces WinRT es una API más?

Es un paso más en la simplificación y el aumento de productividad, pero es una API más y puede ser consumida en conjunción con otras.

¿Realmente puedo usar WinRT desde una aplicación de escritorio?

Sí, puedes. Imagina que tienes una aplicación de Windows Forms y quieres acceder a los datos del acelerómetro. Puede hacerse de otras formas, pero WinRT te proporciona acceso en una sola línea. Podríamos incluir esta instrucción para obtener el valor del acelerómetro respecto del eje X:

    1: var x = Windows.Devices.Sensors.Accelerometer.GetDefault().GetCurrentReading().AccelerationX.ToString();

Visual Studio 2013 no soporta la creación de aplicaciones Windows Forms con soporte a WinRT, pero podemos solucionarlo incluyendo en el .csproj de nuestro proyecto el siguiente PropertyGroup

    1: <PropertyGroup>
    2: <targetplatformversion>8.0</targetplatformversion>
    3: </PropertyGroup>

Tras esto, Visual Studio nos dejará añadir como referencia WinRT:

image

Como los ingenieros somos de naturaleza escéptica, decidí ponerlo en práctica durante el BcnDevCon. El proyecto consistía en mover uno de los robotitos de Pep Lluis mediante el acelerómetro de una Surface Pro. El programa era una aplicación Windows Forms que hacía uso de WinRT para acceder de forma sencilla al acelerómetro de la Surface y controlar el Gadgeteer en función del movimiento:

Controlando nuestro Gadgeteer con una Surface Pro gracias a WinRT

Conclusión

WinRT es una nueva API que soluciona muchos de los problemas que hacían incómoda la programación COM al tiempo que permite el desarrollo simple desde múltiples lenguajes de programación, ya sean nativos o administrados, sin romper la sintaxis de ninguno de ellos.