Mensajes en Windows NT.
El sistema de mensajes de Windows NT está muy asociado a
las ventanas. El sistema está continuamente mandando mensajes
a las ventanas de las aplicaciones: cuando se mueve el ratón,
cuando pasan a primer plano, cuando se pulsa sobre ellas,
cuando se selecciona un menú, etc. En la aplicación dueña
de la ventana existe una función especial que se encargará
de atender y procesar estos mensajes.
Las aplicaciones que estamos realizando son aplicaciones
de consola. Difieren de las aplicaciones normales de Windows
en que no tienen una entrada/salida asociada a una ventana.
Es por eso que no veremos los mensajes de Windows NT en su
conjunto. Sólo
veremos un par de funciones que permitirán mandar mensajes
entre hilos que no posean ventanas.
Los mensajes de Windows NT tienen un tipo de mensaje que
identificará cuál es la razón del mensaje y dos argumentos,
un argumento de 16 bits y otro de 32 bits. Podemos mandar
un mensaje a un hilo con la función:
BOOL PostThreadMessage( DWORD idThread, UINT Msg, WPARAM wParam, LPARAM lParam);
En los parámetros se especificará el identificador del
hilo receptor del mensaje, el tipo de mensaje, el parámetro de
16 bits y el parámetro de 32 bits. En el tipo de mensaje,
podemos usar el WM_USER
y siguientes pues están
reservados para aplicaciones de usuario. El envío que realiza
esta función es un envío no bloqueante en el sentido visto
en teoría. Existe otra función SendMessage
que
no veremos que realiza envíos bloqueantes (la función no
retorna hasta que el destinatario haya recibido el mensaje).
Para que un hilo pueda recibir un mensaje es necesario que
el hilo tenga una cola de recepción de mensajes. Los hilos
con ventanas a su cargo automáticamente la tienen. Como
nuestros hilos pueden no tenerlas, es necesario que el hilo
haya realizado antes una llamada:
PeekMessage(&mensaje, NULL, WM_USER, WM_USER, PM_NOREMOVE);
antes de poder recibir ningún mensaje.
Esta última función sirve para poder espiar para ver si
hay algún mensaje en la cola. Su prototipo es:
BOOL PeekMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax,
UINT wRemoveMsg);
En el primer parámetro se depositará la información del
mensaje espiado, el segundo lo dejaremos a NULL
.
En el tercero y el cuarto especificaremos qué rango de
tipo de mensajes queremos comprobar. El último parámetro
servirá para indicar si queremos que el mensaje desaparezca
de la cola o no, después de espiado.
Para conseguir el mensaje, se usa la función:
BOOL GetMessage( LPMSG lpMsg, HWND hWnd, UINT wMsgFilterMin, UINT wMsgFilterMax);
igual que PeekMessage
con la diferencia de que
el mensaje siempre desaparece y de que la función se queda
bloqueada si no hay mensajes de los tipos solicitados en la cola.
Podemos usar WaitMessage
para ver si hay mensajes de
cualquier tipo en la cola.
Los campos de la estructura MSG que devuelven estas funciones
se pueden consultar en el resumen.
Bibliotecas de enlace dinámico. Construcción.
Para concluir esta parte de Windows NT vamos a ver una parte
muy importante de él: las bibliotecas de enlace dinámico,
o DLLs. En teoría vimos en qué consiste el enlazado dinámico.
En Windows NT se usa profusamente. De hecho, las funciones
de las diferentes APIs se encuentran en bibliotecas DLL.
En resumen, se trata de que el código de ciertas funciones
venga en unos ficheros especiales llamados bibliotecas.
Cuando un programa necesite dicho código, lo cargará de
la biblioteca. Así se logra que el código de funciones
muy generales no esté innecesariamente repetido en todos
los ejecutables. También, con las bibliotecas de enlace
dinámico resulta más fácil actualizar a la vez muchos
programas.
Para construir una biblioteca de enlace dinámico, nos podemos
ayudar de las facilidades que nos ofrece el compilador
Visual C++. Para ello, abriremos un nuevo proyecto de tipo
"Win32 Dynamic-Link Library":
Cuando se nos pida escoger, tomaremos "A DLL that exports
some symbols":
Observaremos que el entorno de desarrollo ha sustituido
nuestra vieja conocida función main
por:
BOOL APIENTRY DllMain( HANDLE hModule,
DWORD ul_reason_for_call,
LPVOID lpReserved
)
{
switch (ul_reason_for_call)
{
case DLL_PROCESS_ATTACH:
case DLL_THREAD_ATTACH:
case DLL_THREAD_DETACH:
case DLL_PROCESS_DETACH:
break;
}
return TRUE;
}
Cada vez que un proceso quiera usar la DLL que vamos a crear,
se hará automáticamente una llamada
a DllMain
. La diferencia estará en el parámetro
ul_reason_for_call
. Cuando hagamos
LoadLibrary
, valdrá DLL_PROCESS_ATTACH
.
Cuando hagamos FreeLibrary
, valdrá
DLL_PROCESS_DETACH
. Si se crea o muere un hilo
entre ambos instantes, se llamará con los valores
DLL_THREAD_ATTACH
y DLL_THREAD_DETACH
respectivamente. Esta llamada nos servirá
para poder hacer tareas de reserva inicial o limpieza de la DLL.
Haremos que la función devuelva TRUE si nos parece bien
la petición o FALSE, si no.
En el código fuente también vienen definidas una clase,
una función y una variable que exportará la DLL. En cuanto
a la clase, la podéis borrar directamente (tanto del .cpp
como del .h) pues no vamos a usar C++. Respecto a la
función y la variable, ahí podéis ver cómo se pueden definir
en la DLL para que luego la usen otros procesos. Compilad
la DLL y buscad, en el directorio "Debug" desde fuera del
compilador el fichero que se ha generado. Pulsando sobre el
fichero con el botón derecho del ratón sobre el menú
"Vista rápida", se os ofrecerá mucha información acerca
de la DLL. En particular, las funciones y variables que
exporta. Esto fue lo que me dio a mí:
Si la función se llamaba en mi caso fnBibliot
,
¿por qué dice que la función exportada se llama
?fnBibliot@@YAHXZ
? La razón está en el
enlazador de C++. Las funciones de C++ cuando se guardan
en los módulos objeto cambian (o revisten) su nombre en
parte debido a que en C++ dos funciones diferentes pueden
tener el mismo nombre en el código fuente. Esto hace que
usar la función así exportada sea un verdadero infierno.
Para volver a sentir las piernas, es necesario incluir
la opción de enlazado literal de C. Sustituid en el fichero
de cabecera el prototipo de la función exportada por:
extern "C" BIBLIOT_API int fnBibliot(void);
El nombre de la función cambiará en vuestro caso. Ahora ya
podéis comprobar en la vista rápida de la DLL (una vez
recompilada) que el nombre que aparece es el correcto.
La macro BIBLIOT_API
está definida como
__declspec(dllexport) y lo podéis usar mejor.
Si queréis exportar variables o funciones de la DLL, con
estos ejemplos deberíais poder hacerlo.
DLLs. Utilización por un programa.
Llegó la hora de usar nuestra flamante DLL. Para poder
usarla, hay dos posibilidades cargarla al inicio del
programa o cargarla cuando se necesite. Nosotros
veremos la segunda opción.
Lo primero que tenemos que hacer es cargar la DLL en
el espacio de memoria del proceso. Lo haremos con:
HINSTANCE LoadLibrary( LPCTSTR lpLibFileName);
En lpLibFileName
indicaremos el
camino completo de la DLL. Se nos devolverá un
manejador de la DLL o NULL
, si la llamada fracasó.
Además, la llamada hará que se invoque la función
DllMain
de la DLL con el parámetro
ul_reason_for_call
igualado a la macro
DLL_PROCESS_ATTACH
.
Para poder usar las funciones hay que obtener un puntero a ellas.
Lo hacemos con:
FARPROC GetProcAddress( HMODULE hModule, LPCSTR lpProcName);
En hModule
va el handle de la DLL y, como
segundo parámetro, el nombre de la función. En mi caso,
yo hice:
FARPROC funciOn;
[...]
funciOn=GetProcAddress(hbiblio,"fnBibliot");
if (funciOn==NULL)
{
PERROR("GetProcAddress"); return 1;
}
printf("El valor de la función es %d.\n",funciOn());
También hay que acordarse de liberar la DLL cuando se haya
acabado de usar con FreeLibrary
.
Si queremos escribir en la pantalla dentro de una función
de una DLL puede ser que tengamos que recurrir a escribir
directamente con un manejador de la entrada, salida o
error estándar:
HANDLE GetStdHandle( DWORD nStdHandle);
Puede ser el parámetro: STD_INPUT_HANDLE, STD_OUTPUT_HANDLE ó
STD_ERROR_HANDLE.
BOOL SetStdHandle( DWORD nStdHandle, HANDLE hHandle);
¿Sois capaces de manejarlas sin nada más y escribir
algo en la pantalla dentro de una DLL? Si es así,
habéis aprovechado el curso.
Práctica.
La práctica de la última sesión es como sigue:
Haced una DLL que exporte las funciones:
VOID Poner_a_cero(VOID);
VOID Incrementa(VOID); y
LONG Mostrar(VOID).
La DLL mantendrá un contador interno que será puesto
a cero, incrementado o devuelto su valor con las
funciones que exporta.
Para probar la DLL, haced un programa de nombre
carrera
. Este programa creará cuatro
hilos y pondrá sus identificadores por la pantalla.
Los hilos se quedarán esperando por un objeto de
tipo evento.
El hilo principal pondrá el contador de la DLL a cero.
A continuación,
señalará el evento para que todos comiencen. Cada
hilo incrementará sin pausas el contador de la DLL
un millón de veces. Mientras, el hilo primero esperará
a que vayan acabando los hilos corredores e imprimirá
sus identificadores según vayan llegando.
Cuando hayan llegado todos, imprimirá el valor del
contador de la DLL y acabará con limpieza.
Para saber más
¿Habéis aprendido a valeros por vosotros mismos con
esta asignatura? ¿No importa lo que os pidan porque aunque
no lo sepáis sabréis cómo buscarlo? Esta puede ser
la prueba de fuego.
Construid un ejecutable que mediante llamadas a la API de
Windows logre apagar acabar con la sesión actual de Windows
NT y apagar el ordenador automáticamente. En los ordenadores
más modernos el sistema realmente se apagará. En los más
antiguos, se pedirá al usuario que lo realice por sí mismo.
El programa en sí no debería ocupar más de 30 líneas y es
una aplicación de consola.
Aplicaciones relacionadas.
-
-
Funciones de biblioteca relacionadas.
-
-
LPEs.
- Sigo los pasos para construir la .dll de ejemplo y
no me da ningún error pero sin embargo no me crea
el fichero .dll, sino uno acabado en .lib.
Pista: El "Gran Hermano" cuida de nosotros.
Solución.