1. Introducción
Este artículo analizará el uso de enlaces de sistema globales en aplicaciones .NET. Para hacer esto, desarrollé una biblioteca de clases reutilizable y creé un programa de muestra correspondiente (ver imagen a continuación).
Es posible que hayas notado otro artículo sobre el uso de enlaces del sistema. Este artículo es similar pero tiene diferencias importantes. Este artículo analizará el uso de enlaces del sistema global en .NET, mientras que los otros artículos solo analizan los enlaces del sistema local. Las ideas son similares, pero los requisitos de implementación son diferentes.
2. Antecedentes
Si no estás familiarizado con el concepto de ganchos del sistema de Windows, déjame darte una breve descripción: Un gancho del sistema te permite insertar una función de devolución de llamada: intercepta ciertos mensajes de Windows (por ejemplo, mensajes relacionados con el mouse). Un enlace del sistema local es un enlace del sistema: solo se llama cuando el mensaje especificado es manejado por un solo hilo. Un enlace del sistema global es un enlace del sistema: se llama cuando cualquier aplicación en todo el sistema procesa el mensaje especificado.
Hay varios buenos artículos que presentan el concepto de ganchos de sistema. En lugar de recordar esta información introductoria, simplemente remito al lector al siguiente artículo para obtener información general sobre los enlaces del sistema. Si está familiarizado con los conceptos de enlace del sistema, podrá extraer todo lo que pueda de este artículo. Conocimiento sobre ganchos en la biblioteca MSDN. "Ganchos de vanguardia para Windows en .NET Framework" por Dino Esposito. "Uso de ganchos en C#" de Don Kackman.
Lo que vamos a discutir en este artículo es ampliar esta información para crear un enlace de sistema global, que pueden utilizar las clases .NET. Desarrollaremos una biblioteca de clases usando C# con una DLL y C++ no administrado; juntos lograrán este objetivo.
3. Uso del código
Antes de sumergirnos en el desarrollo de esta biblioteca, echemos un vistazo rápido a nuestros objetivos. En este artículo, desarrollaremos una biblioteca de clases que instala ganchos del sistema global y expone los eventos manejados por estos ganchos como un evento .NET en nuestra clase de gancho. Para ilustrar el uso de esta clase de enlace del sistema, crearemos un enlace de evento de mouse y un enlace de evento de teclado en una aplicación de Windows Forms escrita en C#.
Estas bibliotecas se pueden utilizar para crear cualquier tipo de enlace del sistema, incluidos dos enlaces precompilados: MouseHook y KeyboardHook. También hemos incluido versiones específicas de estas clases, llamadas MouseHookExt y KeyboardHookExt respectivamente. Según el modelo establecido por estas clases, puede crear fácilmente enlaces del sistema, para cualquiera de los 15 tipos de eventos de enlace en la API de Win32. Además, hay un archivo de ayuda HTML compilado en esta biblioteca de clases completa, que archiva estas clases. Asegúrese de leer este archivo de ayuda si decide utilizar esta biblioteca en su aplicación.
El uso y ciclo de vida de la clase MouseHook es bastante simple. Primero, creamos una instancia de la clase MouseHook.
mouseHook = new MouseHook();//mouseHook es una variable miembro
A continuación, vinculamos el evento MouseEvent a un método de nivel de clase.
mouseHook.MouseEvent+=new MouseHook.MouseEventHandler(mouseHook_MouseEvent);
// ...
private void mouseHook_MouseEvent(MouseEvents mEvent, int x, int y ){
string msg =string.Format("Evento del mouse:{0}:({1},{2}).",mEvent.ToString(),x,y
AddText(msg);//Agregar un mensaje al cuadro de texto
}
Para comenzar a recibir eventos del mouse, simplemente instale el siguiente enlace.
mouseHook.InstallHook();
Para dejar de recibir eventos, simplemente desinstale el gancho.
mouseHook.UninstallHook();
También puedes llamar a Dispose para desinstalar este gancho.
Es importante desinstalar este gancho cuando se cierra la aplicación. Dejar los ganchos del sistema instalados ralentizará el procesamiento de mensajes para todas las aplicaciones del sistema. Incluso puede hacer que uno o más procesos sean muy inestables. Por lo tanto, asegúrese de quitar los ganchos del sistema cuando termine de usarlos. Decidimos eliminar este gancho del sistema en nuestra aplicación de muestra, agregando una llamada Dispose en el método Dispose del formulario.
anulación protegida void Dispose(bool disposing) {
if (eliminación) {
if (mouseHook != null) {
mouseHook.Dispose();
mouseHook = null;
}
// ...
}
}
Este es el caso cuando se utiliza esta biblioteca de clases. Hay dos clases de enlaces de sistema en esta biblioteca y son bastante fáciles de ampliar.
4. Construyendo la biblioteca
Esta biblioteca tiene dos componentes principales. La primera parte es una biblioteca de C#; puede usarla directamente en su aplicación. La biblioteca, a su vez, utiliza internamente una DLL de C++ no administrada para administrar los enlaces del sistema directamente. Primero discutiremos el desarrollo de esta parte de C++. A continuación, analizaremos cómo utilizar esta biblioteca para crear una clase de enlace general en C#. Así como analizamos la interacción C++/C#, prestaremos especial atención a cómo los métodos y tipos de datos de C++ se asignan a los métodos y tipos de datos de .NET.
Quizás te preguntes por qué necesitamos dos bibliotecas, especialmente una DLL de C++ no administrada. También puede observar que los dos artículos de referencia mencionados en la sección de antecedentes de este artículo no utilizan ningún código no administrado. A eso, mi respuesta es: "¡Sí! Es exactamente por eso que escribí este artículo". Cuando piensas en cómo los enlaces del sistema implementan realmente su funcionalidad, es importante que necesitemos código no administrado. Para que funcione un enlace del sistema global, Windows inserta su DLL en el espacio de proceso de cada proceso en ejecución. Dado que la mayoría de los procesos no son procesos .NET, no pueden ejecutar directamente ensamblados .NET. Necesitamos un proxy de código no administrado, uno que Windows pueda insertar en todos los procesos que se van a conectar.
El primero es proporcionar un mecanismo para pasar un proxy .NET a nuestra biblioteca C++. De esta forma, definimos la siguiente función (SetUserHookCallback) y el puntero de función (HookProc) en lenguaje C++.
int SetUserHookCallback(HookProc userProc, UINT hookID)
typedef void (CALLBACK *HookProc)(código int, WPARAM w, LPARAM l)
SetUserHookCallback's El segundo El parámetro es el tipo de gancho; este puntero de función lo utilizará. Ahora, tenemos que definir los métodos y servidores proxy apropiados en C# para usar este código. Así es como lo asignamos a C#.
SetCallBackResults externo estático privado
SetUserHookCallback(HookProcessedHandler hookCallback, HookTypes hookType)
delegado protegido void HookProcessedHandler(int code, UIntPtr wparam, IntPtr lparam)
enum público HookTypes {
JournalRecord = 0,
JournalPlayback = 1,
// ...
KeyboardLL = 13,
MouseLL = 14
};
Primero, usamos el atributo DllImport para importar la función SetUserHookCallback como estática de nuestra clase de enlace base abstracta. Método externo SystemHook. Para hacer esto tenemos que mapear algunos tipos de datos externos. Primero, debemos crear un proxy que sirva como nuestro puntero de función. Esto se logra definiendo HookProcessHandler arriba. Necesitamos una función cuya firma C++ sea (int,WPARAM,LPARAM). En el compilador de Visual Studio .NET C++, int es el mismo que en C#. En otras palabras, int es Int32 en C++ y C#. No siempre fue así. Algunos compiladores tratan los enteros de C++ como Int16. Insistimos en utilizar el compilador Visual Studio .NET C++ para implementar este proyecto, por lo que no tenemos que preocuparnos por definiciones adicionales causadas por diferencias entre compiladores.
A continuación, debemos pasar los valores WPARAM y LPARAM usando C#. De hecho, estos son punteros que apuntan a los valores UINT y LONG de C ++ respectivamente. En términos de C#, son punteros a uints e ints. Si aún no está seguro de qué es WPARAM, puede consultarlo haciendo clic derecho en su código C++ y seleccionando "Ir a definición". Esto le llevará a la definición en windef.h.
//De windef.h:
typedef UINT_PTR WPARAM;
typedef LONG_PTR LPARAM
Por lo tanto, elegimos System.UIntPtr; y System.IntPtr como nuestros tipos de variables: corresponden a los tipos WPARAM y LPARAM respectivamente, tal como se usan en C#.
Ahora, veamos cómo la clase base de enlace utiliza estos métodos importados para pasar una función de devolución de llamada (proxy) a C++, lo que permite a la biblioteca de C++ llamar directamente a instancias de las clases de enlace de su sistema. Primero, en el constructor, la clase SystemHook crea un proxy para el método privado InternalHookCallback, que coincide con la firma del proxy HookProcessedHandler.
Luego pasa este proxy y su HookType a la biblioteca C++ para registrar la función de devolución de llamada utilizando el método SetUserHookCallback, como se analizó anteriormente. La siguiente es su implementación de código:
public SystemHook(HookTypes type){
_type = type;
_processHandler = new HookProcessedHandler(InternalHookCallback
SetUserHookCallback(_processHandler, _type);
}
La implementación de InternalHookCallback es bastante simple. InternalHookCallback solo pasa llamadas al método abstracto HookCallback mientras lo envuelve con un bloque try/catch general. Esto simplificará la implementación en clases derivadas y protegerá el código C++. Recuerde, este gancho de C++ llamará a este método directamente una vez que todo esté listo.
[MethodImpl(MethodImplOptions.NoInlining)]
private void InternalHookCallback(int code, UIntPtr wparam, IntPtr lparam){
try { HookCallback(code, wparam) , lparam); }
catch {}
}
Hemos agregado un atributo de implementación del método: le indica al compilador que no incluya este método. Esto no es opcional. Al menos, eso era lo que se necesitaba antes de agregar try/catch. Parece que, por alguna razón, el compilador está intentando incorporar este método, lo que causará todo tipo de problemas al agente que lo empaqueta. La capa C++ volverá a llamar y la aplicación fallará.
Ahora, echemos un vistazo a cómo una clase derivada usa un HookType específico para recibir y manejar eventos de enlace. La siguiente es la implementación del método HookCallback de la clase virtual MouseHook:
protected override void HookCallback(int code, UIntPtr wparam, IntPtr lparam){
if (MouseEvent == null ) { return; }
int x = 0, y = 0
MouseEvents mEvent = (MouseEvents)wparam.ToUInt32();
switch(mEvents) ) {
p>
case MouseEvents.LeftButtonDown:
GetMousePosition(wparam, lparam, ref x, ref y
break
; p>// .. .
}
MouseEvent(mEvent, nuevo punto(x, y));
Primero, preste atención a la definición de esta clase. Un evento MouseEvent: esta clase activa este evento cuando se recibe un evento de enlace. Esta clase convierte datos de los tipos WPARAM y LPARAM en datos significativos de eventos del mouse en .NET antes de activar sus eventos. Esto libera a los consumidores de la clase de tener que preocuparse por interpretar estas estructuras de datos. Esta clase utiliza la función GetMousePosition importada, que definimos en la DLL de C++ para convertir estos valores. Para ello, consulte la discusión en los siguientes párrafos.
En este método, comprobamos si alguien está escuchando este evento. En caso contrario, no es necesario continuar con el incidente. Luego, convertimos WPARAM en un tipo de enumeración MouseEvents. Nos hemos encargado de construir la enumeración MouseEvents para que coincida exactamente con sus constantes correspondientes en C++. Esto nos permite convertir simplemente valores de puntero en tipos de enumeración. Sin embargo, tenga en cuenta que esta conversión se realizará correctamente incluso si el valor de WPARAM no coincide con un valor de enumeración. El valor de mEvent simplemente no estará definido (no será nulo, simplemente no estará dentro del rango de valores de enumeración). Para ello, analice en detalle el método System.Enum.IsDefined.
A continuación, después de determinar el tipo de evento que recibimos, la clase activa el evento y notifica al consumidor el tipo de evento del mouse y la posición del mouse durante el evento.
Nota final, respecto a la conversión de valores WPARAM y LPARAM: los valores y significados de estas variables son diferentes para cada tipo de evento. Por tanto, en cada tipo de gancho, debemos interpretar estos valores de forma diferente. Elegí implementar esta conversión en C++ en lugar de intentar imitar estructuras y punteros complejos de C++ en C#. Por ejemplo, la clase anterior usa una función de C++ llamada GetMousePosition. Aquí está este método en una DLL de C++:
bool GetMousePosition(WPARAM wparam, LPARAM lparam, int amp; x, int amp; y) {
MOUSEHOOKSTRUCT * pMouseStruct = (MOUSEHOOKSTRUCT * )lparam;
x = pMouseStruct->pt.x;
y = pMouseStruct->pt.y
devuelve verdadero; p>}
En lugar de intentar asignar el puntero de estructura MOUSEHOOKSTRUCT a C#, simplemente lo pasamos temporalmente de vuelta a la capa de C++ para extraer el valor que necesitamos. Tenga en cuenta que, dado que necesitamos devolver algún valor de esta llamada, pasamos nuestro número entero como variable de referencia. Esto se asigna directamente a int* en C#. Sin embargo, podemos anular este comportamiento e importar este método seleccionando la firma correcta.
private static extern bool InternalGetMousePosition(UIntPtr wparam,IntPtr lparam, ref int x, ref int y)
Al definir el parámetro entero como ref int, obtenemos la referencia de C++ pasada a Nuestros valores. También podemos usar int si queremos.
5. Limitaciones
Algunos tipos de ganchos no son adecuados para implementar ganchos globales. Actualmente estoy pensando en una solución alternativa: permitiría el uso de tipos de ganchos restringidos. Por ahora, no vuelva a agregar estos tipos a la biblioteca, ya que provocarán que la aplicación falle (a menudo, una falla catastrófica en todo el sistema). La siguiente sección se centrará en las razones detrás de estas limitaciones y las soluciones a ellas.
HookTypes.CallWindowProcedure
HookTypes.CallWindowProret
HookTypes.ComputerBasedTraining
HookTypes.Debug
HookTypes.ForegroundIdle
HookTypes.JournalRecord
HookTypes.JournalPlayback
HookTypes.GetMessage
HookTypes.SystemMessageFilter
6. Tipos de ganchos
En esta sección, intentaré explicar por qué algunos tipos de ganchos están restringidos a ciertas categorías mientras que otros no. Por favor, perdónenme si uso terminología que está un poco fuera de lugar. No encontré ninguna documentación sobre esta parte del tema, así que inventé mi propio vocabulario. Además, si cree que estoy fundamentalmente equivocado, dímelo.
Cuando Windows llama a la función de devolución de llamada pasada a SetWindowsHookEx(), se llamarán de manera diferente para diferentes tipos de ganchos. Básicamente, existen dos casos: ganchos que cambian el contexto de ejecución y ganchos que no cambian el contexto de ejecución. Para decirlo de otra manera, es decir, el caso en el que la función de devolución de llamada de enlace se ejecuta en el espacio de proceso de la aplicación enlazada y el caso en el que la función de devolución de llamada de enlace se ejecuta en el espacio de proceso de la aplicación enlazada.
Los tipos de ganchos, como los ganchos de mouse y teclado, cambian de contexto antes de ser llamados por Windows. El proceso completo es aproximadamente el siguiente:
1. La aplicación X tiene foco y se ejecuta.
2. El usuario presiona una tecla.
3. Windows toma el contexto de la aplicación X y cambia el contexto de ejecución a la aplicación enlazada.
4. Windows llama a la función de devolución de llamada de enlace con los parámetros clave del mensaje en el espacio de proceso de la aplicación enlazada.
5. Windows toma el contexto de la aplicación enganchada y cambia el contexto de ejecución nuevamente a la aplicación X.
6. Windows coloca el mensaje en la cola de mensajes de la aplicación X.
7. Un poco más tarde, cuando se ejecuta la aplicación X, toma el mensaje de su cola de mensajes y llama a su controlador de clave interna (o soltar o presionar).
8. La aplicación X continúa ejecutándose...
Los tipos de ganchos como los ganchos CBT (creación de ventanas, etc.) no cambian de contexto. Para este tipo de ganchos, el proceso es aproximadamente el siguiente:
1. La aplicación X tiene foco y se ejecuta.
2. La aplicación X crea una ventana.
3. Windows llama a la función de devolución de llamada de enlace con los parámetros del mensaje de evento CBT en el espacio de proceso de la aplicación X.
4. La aplicación X continúa ejecutándose...
Esto debería explicar por qué ciertos tipos de ganchos funcionan con esta estructura de biblioteca y otros no. Recuerde, esto es exactamente lo que hace esta biblioteca. Después de los pasos 4 y 3 anteriores, inserte los siguientes pasos:
1. Windows llama a la función de devolución de llamada.
2. La función de devolución de llamada de destino se ejecuta en una DLL no administrada.
3. La función de devolución de llamada de destino busca su agente de llamada administrado correspondiente.
4. El agente administrado se ejecuta con los parámetros apropiados.
5. La función de devolución de llamada de destino regresa y ejecuta el procesamiento de enlace correspondiente al mensaje especificado.
Los pasos 3 y 4 están condenados al fracaso debido a los tipos de gancho que no cambian. El tercer paso fallará porque la función de devolución de llamada administrada correspondiente no estará configurada para la aplicación. Recuerde, esta DLL utiliza variables globales para rastrear estos agentes administrados y la DLL de enlace se carga en cada espacio de proceso. Pero este valor sólo se establece en el espacio de proceso de la aplicación que coloca el gancho. Para otros casos, todos son nulos.
Tim Sylvester señaló en su artículo "Otros tipos de ganchos" que usar una sección de memoria compartida resolverá este problema. Esto es cierto, pero también, como señaló Tim, esas direcciones proxy alojadas no tienen sentido para ningún proceso que no sea la aplicación que coloca el gancho. Esto significa que no tienen sentido y no se pueden llamar durante la ejecución de la función de devolución de llamada. Eso causaría problemas.
Entonces, para utilizar estas funciones de devolución de llamada con tipos de enlace que no realizan cambios de contexto, necesita algún tipo de comunicación entre procesos.
He experimentado con esta idea: usar un objeto COM fuera de proceso en una función de devolución de llamada de enlace DLL no administrada para IPC. Si puede hacer que este enfoque funcione, me encantaría conocerlo. En cuanto a mis intentos, los resultados no fueron los ideales. La razón básica es que es difícil inicializar correctamente la unidad COM para varios procesos y sus subprocesos (CoInitialize(NULL)). Este es un requisito básico antes de poder utilizar objetos COM.
No tengo ninguna duda de que debe haber una manera de solucionar este problema. Pero aún no los he probado porque creo que tienen un uso limitado. Por ejemplo, el gancho CBT le permite cancelar la creación de una ventana si lo desea. Puedes imaginar lo que pasaría para que esto funcione.
1. La función de devolución de llamada de enlace comienza a ejecutarse.
2. Llame a la función de devolución de llamada del enlace correspondiente en la DLL del enlace no administrado.
3. La ejecución debe enrutarse de regreso a la aplicación de enlace principal.
4. La aplicación deberá decidir si permite esta creación.
5. La llamada debe enrutarse de regreso a la función de devolución de llamada de gancho que aún se está ejecutando.
6. La función de devolución de llamada del enlace en la DLL del enlace no administrado recibe la acción que se debe realizar desde la aplicación del enlace principal.
7. La función de devolución de llamada del enlace en la DLL del enlace no administrado toma la acción adecuada en respuesta a la llamada del enlace CBT.
8. Complete la ejecución de la función de devolución de llamada del gancho.
Esto no es imposible, pero no es bueno. Espero que esto elimine el misterio que rodea a los tipos de ganchos permitidos y restringidos en la biblioteca.
7. Otra documentación de la biblioteca: hemos incluido documentación de código relativamente completa para la biblioteca de clases ManagedHooks. Esto se convierte en XML de ayuda estándar a través de Visual Studio.NET al compilar con la configuración de compilación "Documentación". Finalmente, hemos utilizado NDoc para convertirlo en ayuda HTML compilada (CHM). Puede ver este archivo de ayuda simplemente haciendo clic en el archivo Hooks.chm en el Explorador de soluciones del programa o localizando el archivo ZIP descargable asociado con el artículo. IntelliSense mejorado: si no está familiarizado con cómo Visual Studio.NET utiliza archivos XML compilados (salida anterior a NDoc) para mejorar IntelliSense para proyectos de biblioteca de referencia, permítame explicarle brevemente. Si decide utilizar esta biblioteca en su aplicación, puede considerar copiar una versión estable de la biblioteca a una ubicación donde desee hacer referencia a ella. Al mismo tiempo, copie el archivo del documento XML (SystemHooks\MangedHooks\bin\Debug\Kennedy.MangedHooks.xml) en la misma ubicación. Cuando agrega una referencia a la biblioteca, Visual Studio.NET leerá automáticamente el archivo y lo usará para agregar documentación de IntelliSense. Esto es muy útil, especialmente para bibliotecas de terceros como esta. Pruebas unitarias: creo que todas las bibliotecas deberían tener pruebas unitarias correspondientes. Esto no debería sorprender a nadie, ya que soy socio e ingeniero de software en una empresa que realiza pruebas unitarias de software para entornos .NET. Por lo tanto, encontrará un proyecto de prueba unitaria en la solución llamado ManagedHooksTests. Para ejecutar esta prueba unitaria, necesita descargar e instalar HarnessIt; esta descarga es una versión de prueba gratuita de nuestro software de prueba unitaria comercial.
En esta prueba unitaria, presté especial atención a esto: aquí, los argumentos no válidos de un método pueden provocar una excepción de memoria de C++. Aunque la biblioteca es bastante simple, las pruebas unitarias me ayudaron a detectar algunos errores en situaciones más sutiles. Depuración administrada/no administrada: una de las cosas más complicadas de las soluciones híbridas (como el código administrado y no administrado de este artículo) es el problema de la depuración. Si desea recorrer el código C++ o establecer puntos de interrupción en el código C++, debe habilitar la depuración no administrada. Esta es una configuración de proyecto en Visual Studio.NET. Tenga en cuenta que puede recorrer los niveles administrados y no administrados sin problemas; sin embargo, la depuración no administrada ralentiza considerablemente el tiempo de carga y la ejecución de su aplicación durante la depuración.
8. Advertencia final
Obviamente, los ganchos del sistema son bastante poderosos, sin embargo, este poder debe usarse con responsabilidad. Cuando los enlaces del sistema fallan, no sólo hacen que su aplicación caiga. Pueden desactivar todas las aplicaciones que se ejecutan en su sistema actual. Pero la posibilidad de alcanzar este nivel es generalmente muy pequeña. No obstante, aún debe volver a verificar su código cuando utilice enlaces del sistema.
Descubrí una técnica útil para desarrollar aplicaciones: utiliza enlaces del sistema para instalar una copia de su sistema operativo de desarrollo favorito y Visual Studio.NET en una Microsoft Virtual PC. Luego podrá desarrollar su aplicación en este entorno virtual. De esta manera, cuando ocurra un error en sus aplicaciones enganchadas, solo saldrán de la instancia virtual de su sistema operativo y no de su sistema operativo real. Tuve que reiniciar mi sistema operativo real cuando este sistema operativo virtual falló debido a un error de enlace, pero eso no es frecuente.