UIInspector, reserva dinámica de memoria y aritmética de punteros

Hola comunidad,

Hace un par de días regresé de Melbourne para presentar en DDD Sydney, sin embargo, una noche que me quedé en el hotel porque estaba lloviendo, me pusé a escribir un demo para mi charla y escribí el UIInspector, una clase que permite extraer información de los elementos hijos  de una ventana   (por ejemplo, un cuadro de texto o una etiqueta). A continuación les muestro como funciona

   1: public static List<UIElement> GetUIElements(string pidOrImageName) {

   2:             int testPid = 0;

   3:             bool hasData = false;

   4:             string xmlAsString = string.Empty;

   5:             List<UIElement> retval = new List<UIElement>();

   6:             IntPtr uiXml = Marshal.AllocHGlobal(XML_ALLOCATED_BYTES);

   7:             GCHandle pinnedPtr = GCHandle.Alloc(uiXml, GCHandleType.Pinned);

   8:             EnumChildProc enumCallback = new EnumChildProc(EnumChildWindowProcedure);

   9:             GCHandle safeFunctor = GCHandle.Alloc(enumCallback);

  10:             WriteMemory(XML_START, uiXml);

  11:  

  12:             if (int.TryParse(pidOrImageName, out testPid) &&

  13:                 Process.GetProcesses().AsQueryable().Where(x => x.Id.Equals(testPid)).Count() > 0) {

  14:                 using (Process targetProcess = Process.GetProcessById(testPid))

  15:                     EnumChildWindows(targetProcess.MainWindowHandle, (EnumChildProc)safeFunctor.Target, uiXml.ToInt32());

  16:                 hasData = true;

  17:             } else {

  18:                 if (!string.IsNullOrEmpty(pidOrImageName)) {

  19:                     var targetProcess = Process.GetProcessesByName(pidOrImageName).DefaultIfEmpty();

  20:  

  21:                     if (targetProcess.Any()) {

  22:                         EnumChildWindows(targetProcess.First().MainWindowHandle, (EnumChildProc)safeFunctor.Target, uiXml.ToInt32());

  23:                         hasData = true;

  24:                     }

  25:                 }

  26:             }

  27:  

  28:             xmlAsString = Marshal.PtrToStringAnsi(uiXml);

  29:             pinnedPtr.Free();

  30:             safeFunctor.Free();

  31:             Marshal.FreeHGlobal(uiXml);

  32:  

  33:             if (!string.IsNullOrEmpty(xmlAsString) && hasData)

  34:                 retval = ConvertToList(xmlAsString);

  35:  

  36:             return retval;

  37:         }

El método antes mostrado, espera recibir una cadena con una representación númerica (Identificador del proceso) o el nombre del proceso a inspeccionar, dicho método después retorna una lista de UIElement (Estructura con información del elemento hijo encontrado) la cuál puede filtrarse con una consulta de LINQ.

El primer punto de intéres en dicho método es que usamos el método Marshal.AllocHGlobal que al mismo tiempo llama a la función VirtualAlloc, para así reservar dinámicamente “tantos bytes” de memoria (búfer) para así no incurrir en una posible “excepción C0000005 – Violación de Acceso”, lo siguiente es utilizar la estructura GCHandle para evitar que el GC mueva esa dirección de memoria previamente reservada y haga lo mismo con el delegado que apunta al método responsable por enumerar los elementos (ventanas) hijas.

Como es sabido por todos, un puntero es una dirección en memoria y esta corresponde a un valor númerico; por lo que es válido manipular esa memoria a través de la aritmética de punteros, como mostramos con la siguiente fórmula:

  • (a_pointer + (a_offset * sizeof(*apointer)))

La cual permite escribir en la próxima ubicación de memoria que es dada por un puntero, en C# en “teoría” no se tienen punteros, sin embargo en muchas ocasiones es necesario la manipulación directa de la memoria, como se muestra a continuación

   1: private static void WriteMemory(string stringToWrite, IntPtr memoryAddress) {

   2:     int offset = 0;

   3:  

   4:     if (!string.IsNullOrEmpty(stringToWrite)) {

   5:         stringToWrite.AsEnumerable().ToList().ForEach(x => {

   6:             Marshal.WriteByte(new IntPtr(memoryAddress.ToInt64() + (offset * Marshal.SizeOf(typeof(byte)))), (byte)x);

   7:             offset += 1;

   8:         });

   9:     }

  10: }

El método responsable de extraer el texto de los elementos hijos es el siguiente, en donde se envía un mensaje WM_GETTEXT, y el contenido de dicho elemento es copiado en un  búfer para después convertirla en una cadena Unicode mediante la función Marshal.PtrToStringUni

   1: private static string GetControlOrWindowText(IntPtr hWnd) {

   2:     string retval = string.Empty;

   3:     IntPtr buffer = Marshal.AllocHGlobal(MAX_TEXT_SIZE);

   4:     SendMessage(hWnd, WM_GETTEXT, new IntPtr(MAX_TEXT_SIZE), buffer);

   5:     retval = Marshal.PtrToStringUni(buffer);

   6:     Marshal.FreeHGlobal(buffer);

   7:  

   8:     return retval;

   9: }

El código mostrado se puede descargar completo desde acá

Saludos,

Angel

Leave a Reply

Your email address will not be published. Required fields are marked *