Antes de comenzar con el tema tenemos que tener en cuenta que toda aplicación en .Net se ejecuta bajo una porción de memoria administrada por el CLR y no interactúa directamente con la memoria nativa. (Para conocer más, puedes revisar este post: Conceptos de la arquitectura .NET Framework escrito por Mace Windu.)
Ahora que conocemos cómo funciona el framework, podemos entender qué es Marshal. Marshal es una clase que está en el namespace System.Runtime.InteropServices, que casi nadie habla de él y que no muchos han visto, y lo que hace es mediar entre ambos entornos. Imaginen un bar de lujo (Managed Memory), la calle (Unmanaged Memory) y el portero (Marshal), no es más que eso, si quieres entrar o salir del bar, vas a tener que pedirle permiso a Marshal. En pocas palabras marshal nos permite copiar data de un lado a otro.
Les voy a mostrar un cuadro que vi hace tiempo en msdn, y que ilustra muy bien las categorías de los miembros de la clase Marshal:
En este artículo utilizaremos principalmente los miembros de la categoría Data transformation, qué son los que nos permiten pasar data del entorno administrado al nativo y viceversa. Como vemos en la tabla, los métodos son bastante descriptivos, por ejemplo StringToHGlobalAnsi lo que hace es copiar el contenido de un string administrado a la memoria nativa, y te retorna la dirección en donde copió la data.
En esta página de msdn Marshal Members podrán ver bien qué hace cada miembro y si requiere de algún paso adicional, como es el caso de StringToHGlobalAnsi, el cual necesita llamar a la función FreeHGlobal (también de Marshal)
Veamos un ejemplo práctico del uso de Marshal:
Un amigo de acá del foro, [Zero], tiene una clase en C, es una especie de utilitario. Para el ejemplo usaremos la siguiente función del código de [Zero]:
Código
//La funcion lo que hace es terminar un proceso en ejecucion. int cTerminateProcess(char* lpProcessName) { HANDLE hProcess=NULL; HANDLE hSnapshot=NULL; PROCESSENTRY32 *pInfo=NULL; hSnapshot=CreateToolhelp32Snapshot(TH32CS_SNAPPROCESS,0); if(!hSnapshot) return -1; pInfo=(PROCESSENTRY32*)GlobalAlloc(GPTR,sizeof(PROCESSENTRY32)); pInfo->dwSize=sizeof(PROCESSENTRY32); //Obtenemos el PID del proceso Process32First(hSnapshot,pInfo); do { if(!lstrcmpA(pInfo->szExeFile,lpProcessName)) { if(OpenProcess(PROCESS_TERMINATE,FALSE,pInfo->th32ProcessID)) { hProcess=OpenProcess(PROCESS_TERMINATE,FALSE,pInfo->th32ProcessID); } } }while(Process32Next(hSnapshot,pInfo)); GlobalFree(pInfo); if(!hProcess) return -1; if(!TerminateProcess(hProcess,0))return -1; Sleep(100); return 1; }
Supongamos que yo quiero usarlo desde .Net, ¿Cómo sería eso posible? No es muy dificil, ni requiere mucho tiempo. La idea es crear una librería en .Net la cual podamos referencia sin ningún problema, esto se llama wrapper, y lo haremos desde C++ CLI, el cual nos permite exactamente lo que queremos, interactuar entre entornos Administrados y Nativos mucho más fácil que desde C#, aunque sigue siendo posible, pero tendríamos que usar el keyword unsafe. En C++ tenemos mucho más fácil el acceso a la memoria nativa del sistema, y el mismo acceso al framework.
Ahora sigamos los siguientes pasos para crear nuestro Wrapper:
1.- Creamos una Solución en Blanco en Visual Studio
Le ponen un nombre y click en aceptar. Eso es todo.!
2.- Ahora agregamos Un Proyecto C++ (Static Library)
Este proyecto es donde va el código en C, es decir, las funciones a las cuales les queremos hacer el wrapper (en español es algo como envoltorio, ya veremos por qué). Para crear el proyecto le dan botón derecho en la solución, en el explorador de soluciones. Luego Agregar Nuevo Proyecto. En la ventana seleccionen Visual C++ y en el listado escojan Win32 Project, le dan un nombre y Click en Aceptar.
Aquí es donde va la función cTerminateProcess del código de [Zero].
3.- Creamos un Proyecto C++ CLI (Dynamic Library)
Este va a ser nuestro Wrapper, por lo que crearemos un proyecto en C++ CLI de tipo Dynamic Library, es decir una DLL. De la misma forma que con el proyecto anterior, agregamos un nuevo proyecto seleccionamos igual Visual C++ pero esta vez el tipo de Proyecto será Class Library. Nombramos el proyecto y aceptamos.
Ahora tenemos que hacer dos cosas, la primera es agregar el directorio en donde está el .h del proyecto de C, esto se hace yendo a las propiedades del proyecto > Propiedades de Configuración > C/C++ > General, y dónde dice Additional Include Directories agregar el directorio correspondiente. El segundo paso es agregar a la lista de dependencias la dirección en donde está el .lib que es el resultado del static library en C. Pueden hacerlo yendo a Propiedades de Configuración > Linker > Input, agregandolo en donde dice Dependencias Adicionales.
Ya tenemos todo listo para comenzar a hacer el wrapper.
Creamos una clase y le ponemos el nombre que quieras ponerle cuando la vayan a utilizar, por lo que debe ser un nombre que haga referencia a la función del código. En nuestro caso le pondremos DemoClass. Se crearán dos archivos uno .h y otro .cpp, Abrimos el .h para definir la clase y el método que queremos.
El código del .h quedaría así:
Código
// DemoClass.h #pragma once using namespace System; namespace Demo{ public ref class DemoClass { public: int TerminateProcess(String^ ProcessName); }; }
Si recordamos la función en C se llama cTerminateProcess, entonces en nuestro wrapper se llamará simplemente TerminateProcess, los parámetros es algo que hay que tener en cuenta, la funcion original pide char*, en .Net la idea es NO usar punteros por lo que usaremos String, y vale la pena recalcar el uso del simbolo ^ que es simplemente para identificar las referencias a código administrado y código nativo.
Ya tenemos el .h listo, vamos a codificar el cpp:
Código
// This is the main DLL file. #include "stdafx.h" #include <vcclr.h> #include "DemoClass.h" using namespace System; using namespace System::Runtime::InteropServices; namespace Demo{ int DemoClass::TerminateProcess(String^ ProcessName) { //Validamos :) if(!ProcessName) throw gcnew ArgumentNullException; //Utilizamos StringToHGlobalAnsi el cual pide String como parametro //como retorna un puntero, y sabemos que es un string, le hacemos un //cast a char* char* lpProcessName = (char*)(void*)Marshal::StringToHGlobalAnsi(ProcessName); //ejecutamos cTerminateProcess y le pasamos lpProcessName int result = AmC::cTerminateProcess(lpProcessName); //liberamos la memoria usada por lpProcessName , y retornamos Marshal::FreeHGlobal((System::IntPtr)(void*)lpProcessName); return result; } }
Como pueden ver no es nada complicado, aunque hay que hacerlo muy bien para no dejar punteros sin liberar ni funciones sin validar.
3.- Por último agregamos un Proyecto C# en Windows
Usaremos el proyecto para testear la libería. Al Form1 le agregamos un botón y un textbox.
Doble Click al botón para subscribirnos al evento Click, y colocamos esto:
Código
m_amNet.TerminateProcess(m_terminateProcessTextBox.Text);
Simplemente creamos una instancia de la clase DemoClass y llamamos al método TerminateProcess que a su vez llama al cTerminateProcess en la liberia en C.
Presionamos F5 para compilar y ejecutar nuestra solución, ejecutamos la calculadora, colocamos ese nombre calc.exe (el nombre del proceso de la calculadora) en el textbox, por último hacemos click en el botón y si todo sale bien debería cerrarse el proceso.
Espero les sirva de ayuda, si hay algún error me avisas y lo corregimos.
Un saludo!