Hooks o Ganchos
por Liebesschmerz
 

Tabla de contenidos


Algo para saber

    Este es el primer tutorial o guía que he escrito en mi vida, asi que si encuentran algun error de ortografía, o no les gusta algo que puse, pido disculpas :-). Algo también que me gustaría poner acá es que crecí con los términos en Inglés de la programación, asi que he hecho un esfuerzo en ponerlos en Castellano, aunque estos no tengan un significado claro para mi, como por ejemplo (hilo, cola, pila, etc) suenan muy raros y graciosos jejeje, asi que si encuentran un termino por ahí en Inglés, es porque no se cómo se llama en Castellano.

Introducción

       Aqui traduzco un poco lo que el MSDN nos da a saber acerca de los ganchos en Windows, por ejemplo como instalarlos, desinstalarlos, manejarlos, etc. No es la información completa. Si te gusta leer o no sabes acerca de los ganchos quizás te ayude leer la traducción que puse aquí, para los que ya saben no necesitan leerlo.

Propósito del Tutorial

    Me decidí a escribir este tutorial acerca de los hooks en Windows, porque hace algun tiempo buscaba información sobre este tema, y no encontraba mucha información, y la que encontraba estaba en Inglés y no encontraba nada en Castellano. Tambien la ayuda del MSDN me ayudo, pero el código que ponia como algo de ejemplo es para VC++ :-).

Bueno lo que tratare de mostrar en este tutorial acerca de los ganchos de windows es algo básico de cómo agregar mas funcionalidad (si es que asi se le puede llamar) a programas foraneos (programas ya compilados que no tenemos los source codes y no tienen soporte de plugin claro). Usaremos para nuestro ejemplo el famoso Notepad que viene con Windows, le agregaremos un menú dinamicamente y simplemente mostraremos un messagebox al clickear en un item del menú :-).
Así que este tutorial tendrá información acerca de ganchos, subclassing, y MMF (memory mapped files).

Manos a la obra

    Ok, Voy a agregar un menú dinamicamente a todas las instancias del Notepad, después de haber instalado el gancho. Cada instancia del Notepad tiene un ID de hilo distinto, entonces eso me da a entender, de que el tipo de gancho que instalare sera global o system-wide, y como dice el MSDN debo crear/usar una DLL para ganchos globales, y también dice que debo compartir algunos datos que usaré en otro proceso. Esto es porque cada instancia de un programa o DLL tiene un espacio privado en la memoria, y no puedo accesar a ellas normalmente, entonces las variables en diferentes procesos seran diferentes o simplemente invalidos.
Es por eso que debo compartir por lo menos una variable en todas las instancias de la DLL, la variable que compartire sera el manejador del gancho (HHOOK) que debo pasarle a CallNextHookEx.
Como Delphi según se o_0, no permite asignarle a un segmento el atributo de shared, como lo permite por ejemplo VC++ y MASM32, entonces usare MMF (memory mapped files - archivos mapeados en memoria).
Para eso usaré CreateFileMapping, MapViewOfFile, UnmapViewOfFile, y CloseHandle. Compartiré el HHOOK que es igual a un DWORD, entonces sera 4 bytes los que reservaré.


Declaro las variables y constantes que necesitaré:


const
  szClassName     = 'Notepad';
  szMMFName       = 'szMiMMF :-)';
  WM_STOPSUBCLASS = WM_USER+$10;
  ID_Item1        = WM_USER+$100;
  ID_Item2        = WM_USER+$101;
  ID_Item3        = WM_USER+$102;
  ID_Item4        = WM_USER+$103;
var
  pdwDatos: PDWORD = nil;
  hMap: DWORD = 0;

szClassName: es el nombre de la class del Notepad.
szMMFName: es el nombre que le daré al espacio de memoria y donde tendré acceso de lectura y escritura.
WM_STOPSUBCLASS: es un mensaje que enviaré a las ventanas del Notepad para darles a saber que ya quiero terminar de subclassearlas.
ID_ItemX: son los IDs que le daré a los items de los menues que agregare dinamicamente.
pdwDatos: es un puntero a un DWORD, donde almacenaré el manejador del gancho (HHOOK).
hMap: una simple variable de tipo DWORD, la usaré para guardar el manejador que me devuelve CreateFileMapping.

Cada vez que mi DLL sea cargada por un proceso, usare OpenGlobalData para obtener un puntero al espacio creado y cuando se descarge mi DLL usare CloseGlobalData.


(* creamos un espacio de memoria dandole un nombre, y todas las instancias  de la DLL podran escribir/leer a este espacio. La primera vez creare el espacio, las demas veces retornara el manejador del espacio ya creado *).
procedure OpenGlobalData();
begin
  hMap := CreateFileMapping(INVALID_HANDLE_VALUE, nil, PAGE_READWRITE,                                               0, SizeOf(DWORD), szMMFName);
  pdwDatos := MapViewOfFile(hMap, FILE_MAP_ALL_ACCESS, 0, 0, 0);
  if pdwDatos = nil then
    CloseHandle(hMap);
end;

(* cerramos todo *).
procedure CloseGlobalData();
begin
  UnmapViewOfFile(pdwDatos);
  CloseHandle(hMap);
end;


Ahora como se cuando la DLL es cargada/descargada ?
Quizas ya sepas esto, pero en una ayuda de Borland leí que se debía hacer así.


(* punto de entrada de la DLL *)
procedure DllEntryPoint(dwReason: DWORD);
begin
  case dwReason of
    DLL_PROCESS_ATTACH: OpenGlobalData();
    DLL_PROCESS_DETACH: CloseGlobalData();
  end;
end;

(* entrada, llamo a process_attach *)
begin
  DllProc := @DLLEntryPoint;
  DllEntryPoint(DLL_PROCESS_ATTACH);
end.


Ahora solo necesito instalar/desinstalar el gancho de tipo WH_CBT y crear una función filtro.
Windows nos dice que la función filtro de un gancho de tipo WH_CBT puede recibir esto:

nCode: HCBT_ACTIVATE, HCBT_CLICKSKIPPED, HCBT_CREATEWND, HCBT_DESTROYWND, HCBT_KEYSKIPPED, HCBT_MINMAX, HCBT_MOVESIZE, HCBT_QS, HCBT_SETFOCUS, y HCBT_SYSCOMMAND.
wParam: Depende del nCode.
lParam: Depende del nCode.

Estoy intersado en la notificacion HCBT_CREATEWND, que es la que recibire cuando una ventana esté a punto de ser creada y los parámetros wParam y lParam tendrán estos valores.

wParam: Manejador de la ventana que será creada.
lParam: Puntero a una estructura CBT_CREATEWND.

Crearé dos procedimientos para instalar/desinstalar el gancho y serán exportadas, para que un programa pueda llamarlas.


function CBTProc(Code: Integer; wParam: WPARAM; lParam: LPARAM): LRESULT; stdcall;
var
 
szClass: array [0..256] of Char;
 
dwOldWndPtr: DWORD;
begin
  (* veo si la notificacion es igual a la de "ventana creandose" *)
  if Code = HCBT_CREATEWND then
 
begin
   
(* agarro la class de la ventana que se esta creando y la comparo
       con la class del notepad *)
 
  ZeroMemory(@szClass[0], 256);
 
  GetClassName(wParam, @szClass[0], 256);
 
  if szClass = szClassName then
    begin
     
(* subclasseo la ventana y guardo el viejo puntero
         en el USERDATA de la ventana *)
 
    dwOldWndPtr := GetWindowLong(wParam, GWL_WNDPROC);
 
    SetWindowLong(wParam, GWL_WNDPROC, Integer(@WndProc));
 
    SetWindowLong(wParam, GWL_USERDATA, dwOldWndPtr);
    end;
  end;

  (* llamo al siguiente gancho en la cadena *)
  Result := CallNextHookEx(pdwDatos^, Code, wParam, lParam);
end;

(* exportar; instalar el gancho de tipo CBT *)
procedure
StartHook(); stdcall;
begin
  pdwDatos^ := SetWindowsHookEx(WH_CBT, @CBTProc, hInstance, 0);
end;

(* exportar; desinstalar el gancho *)
procedure StopHook(); stdcall;
begin
  RestaurarWndProcs();
  UnHookWindowsHookEx(pdwDatos^);
end;


Subclasseo la ventana, osea cambio su función que procesa los mensajes de Windows, y le doy una nueva. Windows llamará primero a mi funcion y luego yo llamaré a la función vieja, de esta forma puedo atrapar los mensajes que generará mi menú dinámico :-).
Como ves también guardo el puntero a la función WNDPROC vieja en la misma ventana, así no tengo que crear un array dinámico, y me ahorro mucho trabajo. Si usara un array dinámico de punteros, tendría que compartirlo entre todos los procesos también, así como comparto el valor de pdwDatos^.

Función nueva que procesa los mensajes


function WndProc(Handle: HWND; uMsg: UINT; wParam: WPARAM; lParam: LPARAM): LRESULT;   stdcall;
var
  hMnu, hNewMnu: HMENU;
  dwOldWndPtr: DWORD;
begin
  (* obtengo el viejo puntero que guarde aqui *)
  dwOldWndPtr := GetWindowLong(Handle, GWL_USERDATA);
  case uMsg of
    (* creo el menu cuando recibo el mensaje de mostrar ventana, y
       lo agrego al menu actual del notepad  *)

    WM_SHOWWINDOW:
    begin
      hNewMnu := CreatePopupMenu();
      hMnu := GetMenu(Handle);
      AppendMenu(hNewMnu, MF_STRING, ID_Item1, 'Item 1');
      AppendMenu(hNewMnu, MF_STRING, ID_Item2, 'Item 2');
      AppendMenu(hNewMnu, MF_STRING, ID_Item3, 'Item 3');
      AppendMenu(hNewMnu, MF_STRING, 0, nil);
      AppendMenu(hNewMnu, MF_STRING, ID_Item4, 'Item 4');
      AppendMenu(hMnu, MF_POPUP, hNewMnu, 'Demo!');
      DestroyMenu(hNewMnu);
    end;
    (* aqui recibo el mensaje del menu, simplemente muestro
       un messagebox indicando que item fue clickeado *)

    WM_COMMAND:
    case wParam of
      ID_Item1: MessageBox(Handle, 'Item 1 ha sido clickeado!', szClassName, 0);
      ID_Item2: MessageBox(Handle, 'Item 2 ha sido clickeado!', szClassName, 0);
      ID_Item3: MessageBox(Handle, 'Item 3 ha sido clickeado!', szClassName, 0);
      ID_Item4: MessageBox(Handle, 'Item 4 ha sido clickeado!', szClassName, 0);
    end;
    (* este mensaje lo cree yo, simplemente es un número alto para
       evitar que exista, y sirve para decirle que restaure el viejo
       puntero del WNDPROC, si no hago esto el programa hará crash *)

    WM_STOPSUBCLASS:
      SetWindowLong(Handle, GWL_WNDPROC, dwOldWndPtr);

  end;
 
(* no recibí ningún mensaje que buscaba, y llamo a su viejo WNDPROC *)
  Result := CallWindowProc(Pointer(dwOldWndPtr), Handle, uMsg, wParam, lParam);
end;

Solo me falta mostrar lo que RestaurarWndProcs es.
Este procedimiento lo llamo cuando desinstalo el gancho. Este procedimiento simplemente enumera las ventanas buscando por una ventana la cual tenga su class igual a la class del Notepad, y si la encuentra le envía el mensaje WH_STOPSUBCLASS. Si el Notepad fue creado antes de que el gancho sea instalado, igualmente recibirá el mensaje, pero no lo entendera y no sucederá nada.


function EnumWinProc(Handle: HWND; lParam: LPARAM): Boolean; stdcall;
var
  szClass: array [0..256] of Char;
begin
 
(* pongo el buffer en ceros *)
  ZeroMemory(@szClass[0], 256);
  GetClassName(Handle, @szClass[0], 256);
  if szClass = szClassName then
   
(* comparo las class, son iguales ?, entonces envio el mensaje *)
    SendMessage(Handle, WM_STOPSUBCLASS, 0, 0);

  Result := True;
end;

procedure RestaurarWndProcs();
begin
  EnumWindows(@EnumWinProc, 0);
end;

Código fuente del proyecto DLL.

Click aquí para descargar los fuentes de ejemplo de este tutorial.

 

Bueno, eso cubre todo creo o_0.
chao
Liebesschmerz--