l'assembleur  [livre #4061]

par  pierre maurette



Programmation
Windows

Ce chapitre aborde la programmation Windows directement en assembleur. Il n’est pas dans le propos de cet ouvrage de traiter de la programmation Windows en général, et nous allons rapidement nous rendre compte que si nous excluons l'utilisation d'un EDI puissant, doté de composants tout faits, l'utilisation de l'assembleur est tout à fait comparable à celle d'un compilateur C standard.

Un assembleur intégré, même à un simple compilateur C/C++ en ligne de commande tel C++5.5 de Borland, permet d'écrire du code 95 % assembleur, en réglant une grande partie des problèmes de bibliothèques et de fichiers d'en-tête qui vont nous embêter. Ce dernier point peut faire réfléchir. Mais nous constaterons certains défauts dans cette solution. Ils limitent les choix raisonnables à deux :

  Soit le pur assembleur, pour des applications pas trop lourdes, et en utilisant des techniques favorisant l'écriture et la lisibilité du code : squelettes et copier-coller, directives de choix et de boucles, utilisation de INVOKE et de bibliothèques de fonctions pour la tuyauterie d'entrée/sortie par exemple.

  Soit à l'assembleur intégré, de préférence au sein d'un RAD, et en laissant au langage évolué un maximum de travail quand le temps n'est pas critique et/ou le gain de l'assembleur négligeable. Et justement, le côté Windows de l'application ne fait pas partie à priori des secteurs optimisables.

La seconde solution est traitée par ailleurs, sur la base de Delphi 6. Nous allons aborder ici la programmation Windows en pur assembleur.

Indépendamment de l'assembleur, programmer pour Windows demande essentiellement un effort de documentation. Les ressources d’internet sont à ce sujet pléthoriques, le plus souvent en langue anglaise. Pour des raisons de droits, le contenu du CD-Rom n'est pas entièrement défini au moment de la rédaction. Vous y trouverez, quoi qu'il en soit, quelques fichiers et des liens. Si vous disposez d'un internet haut débit, aucun problème. Commencez par aller faire votre marché sur le site de Microsoft, vous y trouverez votre bonheur, ou au moins your happiness . Dans le cas contraire, sachez que le fichier de Delphi 6 Personnel est une archive SFX, comme beaucoup de fichiers de ce type. Ils s'ouvrent avec Winzip ou Winrar (clic droit, ouvrir avec... ). Donc, même sans installer le produit, vous pouvez y récupérer le contenu du dossier Delphi6PersoFR.exe\\Install\Common\Borland Shared\MSHelp . Ce sont des fichiers d'aide d'origine Microsoft, c'est en anglais, mais c'est très complet.

Quelle que soit son origine, à la source d'une documentation se trouve plus ou moins la documentation officielle de l'éditeur de Windows, parfois agrémentée de quelques points non documentés, ce qui permet de parler des secrets de Windows. Or, la documentation Microsoft a une particularité, largement partagée avec la majorité des constructeurs et éditeurs : elle se base sur le langage C/C++. C'est un choix logique et certainement meilleur qu'un pseudo-code, pour décrire des prototypes de fonctions ou un exemple de code, mais qui débouche parfois sur des ambiguïtés. Prenons un simple exemple.

La première fonction que vous rencontrerez dans un vrai programme Windows est WinMain() . La page consacrée à cette fonction dans le MSDN ou win32.hlp est affirmative : un programme Windows doit commencer (et se terminer, nous y reviendrons) par une fonction WinMain() dont le prototype est fourni. Et nous sommes amenés à penser que c'est le loader de Windows qui va rechercher cette fonction WinMain() et lui passer les arguments qui vont bien. Or, c'est faux, WinMain() est une exigence du compilateur en programmation Windows, comme l'était main() en programmation non Windows. Nous pourrons le vérifier, et vérifier en dumpant l'exécutable que ni l'assembleur ni le lieur n’ajoutent une fonction portant un nom ressemblant à WinMain . Il n'en reste pas moins qu'il est certainement bon de débuter nos programmes Windows comme tout le monde.

Les plates-formes Windows

Il existe fondamentalement cinq types de plates-formes Windows : NT, 2k, Win9x, WinCE, Win32s.

Win32s

Windows 3.1 était la dernière version des premiers vrais Windows. Cette version était lancée à partir d'un boot sous DOS, comme une application 16 bits. Win32s permettait d'utiliser les possibilités des processeurs 32 bits de la génération 386. L'avantage était de pouvoir utiliser de grands espaces mémoire et de lancer simultanément à Windows plusieurs applications DOS. Cette technologie est définitivement obsolète, puisque la seule justification de 3.1 serait son aspect 16 bits/DOS, dans des cas très particuliers.

Windows NT 3 & 4

NT (pour New Technology), un peu comme parfois le Pont Neuf, ne désigne pas obligatoirement le système le plus récent. Ce sigle a au départ désigné une gamme de produits à orientation professionnelle. NT, depuis le début, supporte Unicode en plus d'ANSI et utilise complètement les possibilités de protection des processeurs 32 bits.

NT 3.1 fut la première plate-forme NT, suivie par NT 3.5 encore présent. Une interface à la 3.1 sur un moteur 32 bits.

NT 4 fut une amélioration de la précédente, dotée d'une interface héritée de 95. Cette plate-forme est encore très répandue dans les entreprises (printemps 2003).

Windows NT 5

Windows 2000, ou Win2k, ou NT 5. Déclinée en versions Pro et Serveur, c'est une évolution majeure. Supporte plusieurs systèmes de fichiers, dont NTFS. En terme de positionnement commercial, 2000 est un NT. Mais ce système, comme XP, a des caractéristiques particulières, qui intéressent justement le programmeur en assembleur, en particulier l'impossibilité définitive d'accès au matériel et à l'immense majorité des interruptions du BIOS.

Windows XP est l'application de la technologie 2000 à un système familial, en fin de compte décliné en XP Home Edition et XP Professional. Ce système apparaissait à sa sortie gourmand en mémoire, mais les machines ayant évolué sur ce plan il est devenu l'offre de base de Microsoft. Il "bénéficie" d'un mécanisme évolué de protection contre l'installation illicite. Ce système a, bien entendu, été étudié puis contourné, mais il semble efficace. Disons qu'il y a à la fois moins de XP installés et plus de vendus que si le procédé n'avait pas existé. Internet est tellement intégré à XP que ce système se conçoit mal sans une connexion. Il est savoureux de mettre en balance le système de protection contre son propre piratage et le confort qu'il offre pour le chargement et l'écoute de fichiers audio et vidéo compressés, sous condition d'une connexion ADSL. Il n'est pas impossible que XP préfigure l'arrivée des logiciels au robinet, qui seront facturés à l'heure d'utilisation.

Windows 9x

Plates-formes hybrides 16 et 32 bits, essentiellement ANSI, mais nous verrons que certaines fonctions supportent Unicode, ou plutôt existent en deux versions.

Windows 95 a introduit une interface tout à fait nouvelle en termes d'ergonomie. Il introduit également le système de fichiers VFAT, V comme virtual. Il s'agit d'un système FAT à driver 32 bits, secondé par le cache VCACHE. Incompatible avec les NT de la même époque. Windows 95 OSR2 (OEM System Release 2) initie, quant à lui, le système VFAT 32, plus connu sous le nom de FAT 32, dans lequel le numéro de secteur est codé sur 32 bits, et qui plus efficace.

Windows 98 est une amélioration de 95 OSR2. C'est en fait la version commercialisée de ce dernier système, qui n'était disponible qu'installé sur une machine (OEM). La dernière version valable, qui le restera et qui peut être intéressante, est la 98se, seconde édition. Pour identifier ces quatre moutures, clic droit sur l'icône de Bureau Poste de travail , puis Propriétés . Les numéros sont :

  Windows 95 : 4.00 ;

  Windows 95 OSR2 : 4.00 B ;

  Windows 98 : 4.10 ;

  Windows 98se : 4.10.2222 ou 4.10.2222 A.

Windows ME (Millenium Edition) fut réellement le dernier produit de la génération 9x. Succès moyen et pas mal de problèmes semble-t-il.

Windows CE

Plate-forme 32 bits et Unicode réduite, destinée aux systèmes embarqués, comme les ordinateurs de poche et les téléphones portables

12.1 Principes de base de la programmation Windows

Nous allons maintenant oublier toute référence à DOS, où un programme, seul en mémoire à l'exception de quelques résidents, se déroulait de façon linéaire, devant par exemple aller de temps en temps consulter le tampon du clavier après avoir posé des questions via l'écran. Les explications qui suivent sont suffisamment générales pour s'appliquer à toutes les versions de Windows, du moins depuis la mouture 95.

12.1.1 Objets, instances et handles

Un programme Windows est constitué d'objets, au sens d'entités. La fenêtre principale d'une application inclut d'autres objets, comme des boutons, des barres de menu, des indicateurs de progression, ainsi que d'autres fenêtres constituées elles-mêmes d'objets, le tout dans une relation parent/enfant, en poupées russes. Parmi ces objets, certains sont moins visuels, comme les threads, qui sont des fils de programme indépendants du fil principal.

Ces objets ne sont pas réellement des objets au sens de la programmation orientée objet, POO ou OOP. Néanmoins, une partie du chemin est faite, et des bibliothèques comme VCL ou MFC encapsulent ces entités dans de vraies classes. Indépendamment de ces outils, les objets de Windows partagent avec les classes de la POO la notion d' instanciation et d' instances multiples. Une fois un objet défini, il devra être créé, ou instancié, et pourra l'être plusieurs fois. Dans ce cas, une seule copie du code sera résidente en mémoire, comme en POO, pour l'ensemble des instances de l'objet.

Cette notion d'instanciation s'applique en particulier à l'application elle-même. Vous pouvez lancer plusieurs instances de la Calculatrice de Windows. En revanche, si Acrobat Reader est déjà ouvert, le lancer à nouveau lui donnera simplement le focus et lancer un fichier  .pdf l'ouvrira dans l'instance unique autorisée. Acrobat Reader refuse d'être instancié plus d'une fois, mais gère dans cette instance unique plusieurs documents, en fait plusieurs instances de l'objet document : nous parlons de MDI (Multiple Documents Interface). C'était le cas d'anciennes versions de Word (et d'Excel). Dans les versions récentes, si vous créez ou ouvrez un nouveau document dans ce programme, il lancera une nouvelle instance de lui-même, chaque instance étant de type SDI (Single Document Interface) conformément aux préconisations de Microsoft. Multi-instance et SDI ne sont pas nécessairement liés, c'est le choix du programmeur. UltraEdit par exemple permet de configurer la possibilité d'ouvrir des instances multiples du programme. Dans tous les cas, il reste MDI.

Chaque objet ou plus exactement chaque instance d'objet Windows possède un handle . Nous conservons ce mot anglais dont la traduction serait poignée. Un handle est un identifiant unique d'instance d'objet, géré par Windows et utilisable par une application pour accéder à cet objet. Ce n'est pas un pointeur ! Si vous créez un objet par une fonction Windows comme CreateCetObjet() , la valeur de retour peut être le handle de cet objet, ou NULL si échec. Dans d'autres cas, c'est une fonction de type GetCetObjetHandle() qui remplira cet office. La possession par une application du handle d'un objet lui permet d'agir sur cet objet. Néanmoins, ce n'est pas exactement un jeton, puisque plusieurs applications peuvent détenir le même handle, ni même une autorisation à agir sur l'objet. À retenir ; ne pas confondre handle et pointeur. Nous pourrions imaginer qu'en interne pour Windows un handle est une entrée dans une table de pointeurs, mais peu nous importe.

12.1.2 La programmation par événements et messages

Nous considérons comme connu le fait que le système Windows gère un certain nombre d'applications et de services, en leur accordant de façon préemptive des tranches de temps pour travailler et que lui seul a accès directement aux périphériques. Le microprocesseur, de par sa structure intime, lui facilite énormément le travail. Toutes ces applications sont actives en même temps, mais une seule a le focus . Intuitivement, c'est celle qui est en avant-plan sur le Bureau. Vous pouvez constater que les fenêtres des applications qui n'ont pas le focus montrent des signes d'activité.

Supposons plusieurs applications standard tournant sur notre machine. À quoi passent-elles la plus grande partie de leur temps ? À ne rien faire. Même pendant la saisie d'un texte ou l'utilisation d'un tableur, l'ordinateur passe beaucoup plus de temps à attendre la frappe d'une touche ou un clic qu'à traiter l'effet de cette action. Naturellement, parfois une de ces actions va déclencher une période d'intense activité, lancer un re-calcul ou une recherche par exemple. Certains domaines, comme les jeux, le traitement lourd sur des données, multimédia ou autres, font exception. Et justement, Windows n'est peut-être pas la meilleure solution dans ce genre de situation. Un système monoprocesseur multitâche ne fonctionnera bien qu'en dessous de sa charge maximale. Si plusieurs tâches demandent le plus souvent toute la puissance pour leur propre usage, l'arbitrage deviendra vite rédhibitoire.

Dans le fonctionnement normal, tous les événements sur les périphériques vont transiter par Windows, qui sait quelles applications sont intéressées. Une frappe clavier normale s'adresse à l'application qui détient le focus, mais si c'est par exemple  ALT  +  TAB  , ou  CTRL  +  ALT  +  SUP  , c'est Windows lui-même qui la prend en compte. Un événement modem va d'abord intéresser Windows, mais une application a pu demander à en être prévenu.

Windows prévient les applications de la survenue de ces événements par un système de messages , qui va plus loin que de servir d'interface entre périphériques et applications : c'est un véritable service postal, qui réunit applications et système, ainsi qu'applications entre elles, mais toujours géré par le système. Il est courant et utile qu'une application s'envoie elle-même des messages.

Logiquement, ces messages ne sont pas toujours traités instantanément. Ils remplissent donc des files d'attente, appelées des queues. Ce sont évidemment des structures FIFO (First In First Out), c’est-à-dire tuyau plutôt que pile d'assiette. Nous avons, à destination de Windows, la queue système, alimentée par les drivers du clavier, de la souris, de la tablette graphique... Chaque application dispose de sa propre queue.

Les files d'attente de messages
figure 12.01 Les files d'attente de messages [the .swf]

Nous pouvons maintenant définir la structure, immuable, d'une application Windows. Ce qui suit peut se lire en suivant le listing sk_win.asm de l'application squelette décrite en fin de chapitre et présente sur le CD-Rom.

Nous entrerons dans le programme Windows proprement dit par une procédure qui se nommera toujours WinMain()  et dont le prototype est :

int WINAPI WinMain(
    HINSTANCE hInstance,     // handle to current instance
    HINSTANCE hPrevInstance, // handle to previous instance
    LPSTR lpCmdLine,         // pointer to command line
    int nCmdShow             // show state of window
   );

Dit autrement, la procédure WinMain() est à elle seule le programme Windows. Voir à ce sujet la remarque en tête de chapitre sur la nécessité de WinMain() . Elle commence par des initialisations, au début desquelles il faut gérer les instances multiples, selon son choix. Ensuite, la fenêtre principale de l'application est créée et affichée si nécessaire. Attention, le mot window ne désigne pas toujours quelque chose de visuel sous Windows. Nous entrons ensuite dans une boucle sans fin au cours de laquelle le programme demande en permanence s'il y a des messages.

Structure de WinMain()
figure 12.02 Structure de WinMain() [the .swf]

GetMessage()  récupère un message dans la queue. Elle ne retourne pas tant qu'un message n'est pas disponible. WM_QUIT  indique au programme qu'il lui est demandé de se terminer. Le prétraitement concerne quelques messages particuliers : TranslateMessage()  pour les codes clavier, TranslateAccelerator()  pour transformer les raccourcis clavier en leur équivalent message de menu par exemple. DispatchMessage()  renvoie le message à Windows. Ceci peut paraître étrange.

L'explication se trouve dans les fonctions de rappel, ou callback, ou procédures de fenêtre. Au moment de la création de la fenêtre, une adresse de fonction, très souvent nommée WndProc() , a été communiquée à Windows pour lui signifier que c'est à cette procédure qu'il doit envoyer les messages concernant la fenêtre. Des fenêtres fille de la fenêtre principale possèdent leur propre procédure de fenêtre. Nous sommes ici au niveau fenêtre, et non plus comme pour WinMain() au niveau application.

Une procédure est un aiguillage de traitement des messages. En C/C++, ce serait :

switch(message){
case message1: (code message1); break;
...
case messageN;(code messageN);break;
default: DefWindowProc(handle, message, params);
}

En macro-assembleur MASM, ce pourrait être :

mov eax, msg
.IF (eax == msg1)
   ;traiter msg1
.ELSEIF (eax == msg2)
   ;traiter msg2
...
.ELSEIF (eax == msgN)
   ;traiter msgN
.ELSE
   invoke DefWindowProc, ...
.ENDIF

DefWindowProc()  renvoie encore une fois à Windows, en réalité à une procédure créée par celui-ci pour traiter les messages non traités par l'application, c’est-à-dire la plus grande partie des messages. En effet, même sans aucun traitement particulier de message, une fenêtre est parfaitement fonctionnelle, elle peut par exemple être redimensionnée ou déplacée. Il n'est d'ailleurs pas interdit de tenir compte d'un message, de modifier éventuellement ses paramètres, puis de le repasser à DefWindowProc() .

Une fois le squelette ou l'interface d'un programme fait, le code créatif sera écrit dans les traitements des divers messages.

Windows sait qu'une application attend un message et n'a rien d'autre à faire quand elle appelle GetMessage() . D'autre part, il sait que la file est vide, si tel est le cas, et donc connaît le moment où il faudra réveiller l'application, c’est-à-dire dès qu'un message lui est adressé. Windows pourra donc accorder zéro en terme de temps processeur à cette application durant sa période d'inactivité. En d'autres termes, une application qui est dans GetMessage() est morte, à moins que le traitement du dernier message ne soit pas terminé. C'est ainsi qu'il sera possible de garder ouverts une floppée d'utilitaires sans consommer de cycles processeur. De plus, si une application inactive depuis un certain temps consomme de la mémoire, il pourra décider de la passer en mémoire virtuelle en attendant le réveil.

Sachez qu'il existe une possibilité pour un programme de vérifier sa boîte aux lettres sans rentrer dans la boucle de messages. C'est ce qui permet, pendant un tri ou une recherche par exemple, de pouvoir déplacer ou minimiser la fenêtre, ou d'arrêter l'opération en cours, et que trop de programmeurs oublient.

Nous venons d'évoquer, sans en dire plus, quelques fonctions de Windows, GetMessage() par exemple. Il est temps de nous pencher sur ces fonctions, et la façon de les appeler à partir de l'assembleur.

12.1.3 Librairies et DLL – les fonctions de Windows

Nous regroupons ces deux notions tout simplement parce que les ressources de Windows que nous devrons utiliser sont fournies justement dans des DLL. C'est à ce seul titre que nous les aborderons.

Au niveau du vocabulaire, les mots librairie et bibliothèque seront utilisés de façon interchangeable dans ce paragraphe. Le second serait peut-être meilleur pour traduire le faux ami anglais library, mais peu importe. En réalité, ce domaine est déjà imprécis en anglais, le mot library étant mis à toutes les sauces et désignant des concepts très variés. Il est nécessaire de se fier au contexte et de ne pas hésiter à utiliser par exemple le mot DLL.

Une librairie, au sens large, est une collection de fonctions et/ou de ressources. Il faut d'abord différencier les librairies statiques  et les librairies dynamiques .

Les premières sont des fichiers sources compilés, contenant des fonctions ou des ressources, donc des fichiers de niveau objet, et qui portent typiquement l'extension  .lib , ce dernier point n'ayant rien d'obligatoire. Ces fichiers sont utilisés par le lieur, en même temps que les fichiers en  .obj , ainsi que les ressources compilées  .res , pour bâtir l'exécutable. Après cette compilation, les fichiers de bibliothèques statiques ne sont plus nécessaires, leur savoir-faire est passé dans l'exécutable. Ces fichiers sont, nous l'avons dit, compilés donc ne sont pas lisibles. Il est d'usage très courant, obligatoire dans le cas de librairies distribuées à des tiers, de les accompagner de fichiers d'en-têtes, .h ou .hpp en C/C++, .inc le plus souvent en assembleur. Le rôle de ces fichiers est de fournir les prototypes des fonctions, c’est-à-dire les paramètres, valeur de retour et convention d'appel, et des formats de données et structures de données enfouies dans la librairie. Bien écrits, ils tiennent lieu de documentation. Une précision, puisque ce point sera source de soucis au paragraphe suivant : si le fichier  .lib est à priori utilisable à partir de différents langages, il n'en va pas de même pour le fichier d'include. En revanche, ce dernier est lisible, et peut donc, automatiquement ou manuellement, être traduit. Par exemple, l'utilitaire h2inc.exe  permet théoriquement de convertir un  .h en .inc .

Les librairies, ou bibliothèques, dynamiques sont tout à fait différentes, à ceci près qu'elles exposent comme les statiques une collection de fonctions et/ou de ressources. Le nom exact devrait être : bibliothèque de fonctions liées dynamiquement . Ces fichiers ne sont plus de niveau objet, mais de niveau exécutable. L'amalgame n'est pas fait pas le lieur, à la compilation, mais ces librairies sont chargées et déchargées par Windows ou par le programme lui-même par l'intermédiaire de fonctions Windows, à l'exécution.

Si elles nous intéressent particulièrement ici, c'est parce que c'est par l'intermédiaire de bibliothèques dynamiques que Windows met à la disposition des applications une énorme quantité de fonctions et autres ressources, comme des curseurs, polices de caractères, etc. L'usage de ces ressources est non seulement pratique, mais le plus souvent obligatoire pour élaborer un programme qui ressemble à quelque chose.

Les fonctions Windows suivent la convention d'appel stdcall : les paramètres sont passés par la pile, ils sont empilés en les lisant de droite à gauche. Contrairement aux apparences, cette façon de faire n'est pas spécialement inversée, puisque le premier paramètre en lecture normale de gauche à droite se retrouve au sommet de la pile. Les paramètres de taille inférieure à 32 bits (16 ou 8 bits) sont quand même empilés sous la taille d'un DWORD. C'est la fonction qui se charge du nettoyage de la pile avant de retourner. Les fonctions Windows renvoient une valeur dans EAX. Il est très courant que parmi les paramètres figurent un ou plusieurs pointeurs sur des structures spécifiques, qui permettront à l'application de récupérer un grand nombre de données en provenance de Windows.

Donc, ces fonctions sont liées à l'application au lancement, ou en cours de fonctionnement du programme. Les bibliothèques dynamiques sont des fichiers exécutables au format PE, ils commencent donc par les deux octets 4Dh 5Ah , soit MZ en texte. Ils peuvent avoir un grand nombre d'extensions, comme  .exe , .drv , ou même  .fon , mais le plus courant est  .dll (dynamic-link library).

Une recherche sur  *.dll dans votre dossier système vous montrera le grand nombre de ces fichiers, la plupart étant d'origine Windows, certains provenant également des fournisseurs de périphériques. Ces DLL du dossier système sont pour leur grande majorité destinées à être utilisées par les programmes qui le désirent. Ce n'est pas le cas de celles installées par les applications elles-mêmes, bien que leur réutilisation ne soit pas impossible, sous réserve de documentation. Les grandes DLL généralistes, très couramment utilisées, sont par exemple user32.dll , shell32.dll ou gdi32.dll . Chaque technologie particulière peut donner lieu à une DLL, par exemple tapi32.dll qui embarque les fonctions de l'API téléphonie.

Nous savons que, sous Windows, les applications sont incapables de se voir entre elles, en d’autres termes ne sont pas dans le même espace d'adressage. C'est très intéressant pour le programmeur, et parfois un peu gênant. Les DLL permettent de contourner ce problème. Si plusieurs programmes, ou processus, utilisent la même DLL, celle-ci n'étant implantée qu'une fois en mémoire, il faut bien qu'elle soit dans l'espace mémoire de chacun de ces processus. Par un ensemble d'opérations relativement complexes, l'espace mémoire est projeté (mapped) dans celui de chaque processus demandeur de la DLL. La DLL est pour l'application un memory-mapped file, ou fichier projeté en mémoire, sous-entendu d'un processus. Ceci a une implication importante : la DLL est le lieu privilégié pour partager entre applications.

Mais tout ceci n'est que de la programmation Windows, non spécifique à l'assembleur. Ce qui nous intéresse, c'est de pouvoir utiliser dans nos programmes écrits en assembleur ces fonctions Windows, qui, nous le savons maintenant, sont présentes sur tous les systèmes sous la forme de DLL. Si par exemple vous développez une application qui utilise des fonctions de la version 3 de TAPI, il faudra vérifier sur la machine cible la présence, en plus de tapi32.dll , de tapi3.dll et éventuellement mettre à jour le système. Une DLL de ce type n'est pas nécessairement autonome, en particulier elle peut nécessiter l’installation de fichiers supplémentaires pour accéder au noyau.

La fourniture de ces fichiers appartient donc au fabricant du système d'exploitation ou de ses extensions pour être complet. Mais cela ne suffit pas au développeur, quel que soit le langage utilisé.

Dans l'en-tête d'une DLL (format PE, rappelons-le) se trouvent des informations sur les fonctions mises à disposition des programmes extérieurs, les fonctions exportées, en particulier un offset définissant le point d'entrée de la fonction. Chaque fonction peut, à ce niveau, être définie simplement par un numéro d'ordre (ordinal) ou par son nom.

Si vous connaissez suffisamment le contenu de la DLL, particulièrement dans le cas où vous l'auriez écrite, vous pouvez la charger et la décharger pendant l'exécution du programme, et vous n'aurez besoin d'aucun fichier supplémentaire. Ceci passe par les fonctions LoadLibrary , GetProcAddress et FreeLibrary de Windows. Mais justement, il ne s'agit pas ici d'appeler nos propres fonctions, mais bien celles de Windows.

Pour que le loader de Windows puisse charger la DLL et surtout la projeter dans l'espace mémoire de votre application, il lui faut un certain nombre d'informations dans l'en-tête de l'exécutable. Cette tâche est faite par le lieur. Pour la mener à bien, vous aurez besoin de lui fournir soit (rarement) un fichier de définition de module  .def , soit le plus souvent un fichier de bibliothèque d'importation  .lib .

Le premier  .def est un fichier texte écrit par le programmeur de la DLL. En voici un exemple :

LIBRARY      essai
 
DESCRIPTION  'DLL de test'
 
EXPORTS
        MaDllFonction1       @1
        MaDllFonction2       @2
        MaDllFonction3       @3

DESCRIPTION est optionnel. Le rôle principal de ce fichier est de définir les fonctions, ou plus généralement les symboles, exportés. De plus, le rapprochement est fait entre l'ordinal et le nom. En revanche, aucune indication sur le nombre et la taille des paramètres à passer à la fonction.

Le second  .lib est un fichier de niveau objet, au format COFF, obtenu par exemple en même temps que la DLL, à partir du précédent et du fichier  .obj de la DLL. D'autres procédés existent pour l'obtenir, les informations des fichiers  .def , .lib , des fichiers sources et objets de la DLL, ainsi que l'en-tête de son exécutable sont partiellement redondantes. Les outils pour obtenir des  .lib et .def sont nombreux, sans parler de ceux permettant la conversion entre les deux principaux formats objet, utilisés dans les .lib  : COFF (Microsoft, donc MASM et VC++) et OMF (Intel, utilisé par Borland). Il faudra d'ailleurs faire attention de ne pas récupérer n'importe quel fichier  .lib .

Des bibliothèques d'importation sont fournies avec les langages C/C++, par exemple C++ 5.5 pour Borland et Visual Studio pour Microsoft. De plus, ils sont fournis, au format COFF, dans les divers SDK/DDK et téléchargeables sur le site de l'éditeur. Ce dernier point confère un certain avantage à MASM sur TASM, dès qu'il s'agira de coller à l'actualité.

Il est évident que les bibliothèques d'importation destinées à VC++ vont fonctionner avec MASM, celles fournies par Borland avec TASM ( tasm32.exe ), puisqu'elles ne sont vues que par le lieur qui est le même.

Donc, le monde est beau ? Pas tout à fait. La programmation Windows de base n'est pas très difficile, mais est vite velue. Sorti de l'exemple basique que nous allons voir, il est rapidement impossible de travailler sans l'aide d'un fichier d'include. Outre les prototypes des fonctions, ces fichiers contiennent un grand nombre de macros, définissant des constantes, comme les innombrables messages de Windows, des structures, etc. Non seulement le listing serait illisible sans ces noms, mais nous verrons que la documentation est muette sur les valeurs numériques. Disons plus précisément que pour documenter une fonction, Microsoft fournit une fiche dans le MSDN, faisant référence à une librairie d'importation et surtout à un fichier  .h , tous deux fournis également. Si à un .dll correspond généralement un .lib de même nom, il n'en est pas de même pour les .h , qui sont plus nombreux et découpés thématiquement. L'ensemble des fichiers d'en-tête gérant la base de Windows s'appelle mutuellement, selon une hiérarchie fonctionnelle mais complexe.

Or, malheureusement, ces fichiers d'en-tête C/C++ sont incompatibles avec leur équivalent en assembleur, les .inc . Il existe bien un utilitaire h2inc.exe  qui, comme son nom l'indique, est théoriquement capable de convertir les  .h en .inc , mais le travail à faire manuellement reste important. Le fichier windows.inc de MASM32 est à lui seul une bonne raison de télécharger cette distribution.

L'assembleur intégré, même à un simple compilateur C/C++ en ligne de commande, permet de s'affranchir de ce problème, mais au prix de certains inconvénients à voir un peu plus loin. Nous avons évoqué ce point en tête de chapitre.

Nous allons maintenant, sur un exemple, voir comment résoudre une partie de ces problèmes.

12.2 Premier programme : MessageBox()

Dans le but de mieux appréhender les problèmes, particulièrement ceux concernant les fichiers de bibliothèque et d'en-tête à approvisionner, en vue de l'utilisation des fonctions Windows, nous allons prendre en exemple MessageBox . Cette démarche sera à faire à chaque fois que vous voudrez utiliser en assembleur une fonction d'un API Windows encore inconnue, pour laquelle vous ne possédez pas tous les fichiers nécessaires, dans MASM32 par exemple. Vous devrez employer les fonctions de Recherche et Recherche dans les fichiers de votre éditeur et de l'Explorateur de Windows, ainsi que celles éqivalentes de Google ou du site MSDN de Microsoft, pour trouver le ou les précieux fichiers.

Les exemples sont sur le CD-Rom, sous la forme la plus simple et donc la plus universelle, c’est-à-dire un fichier source et un fichier batch. Dans ces deux fichiers, tous les chemins sont donnés directement. Il vous appartiendra de les modifier pour votre usage. À toutes fins utiles, les fichiers exécutables et intermédiaires sont également fournis.

L'exemple que nous prenons est à peine une application Windows, en ce sens qu'aucune fenêtre ni boucle de messages n’est créée. Disons que c’est une application Windows qui se termine avant de démarrer. Le but est de sérier les difficultés, de bien mettre à plat les soucis d'environnement et d'approvisionnement de fichiers, avant de passer à une première application réellement Windows, dont le code fera nécessairement plus de dix lignes.

MessageBox() , qui permet d'afficher un message sous la forme d'une petite fenêtre, peut être appelé depuis une application console.

Curiosité linguistique
figure 12.03 Curiosité linguistique

Sorry for my poor english. En plus de la forme de la boîte, le texte en français des boutons sur un programme dans une autre langue doit faire penser à l'utilisation de la fonction MessageBox . C'est la langue du système d'exploitation qui est utilisée, ce qui comme nous allons le voir est normal.

Cette fenêtre est généralement modale. Ce dernier mot signifie que le message doit être acquitté pour que le programme principal puisse continuer. Sous Microsoft Word, À propos de Microsoft Word ou Enregistrer sous... ouvrent une fenêtre modale, pas exactement un MessageBox , qu'il faudra fermer avant de reprendre la main dans Word. Ces fenêtres sont modales pour Word, mais pas pour le système, en ce sens qu'elles n'empêchent pas de continuer à utiliser les autres tâches ou à en lancer de nouvelles. Il existe des commandes modales pour Windows, donc pour toutes les applications, comme la fenêtre qui apparaît avec Démarrer/Arrêter . Aide sur Microsoft Word dans Word ouvre une fenêtre non modale, ce qui permet de continuer son travail en ayant l'aide sous les yeux. Remarquons que ces comportements, et même ces éléments de menus sont plus ou moins standardisés par Microsoft pour avoir droit au logo Windows.

Toute recherche commence par une consultation de l'aide, win32.hlp ou le MSDN par exemple. Nous trouvons MessageBox et MessageBoxEx . Première constatation, quel que soit l'outil de documentation utilisé, c'est bien le langage C qui sert de référence.

int MessageBox(
    HWND hWnd,          // handle of owner window
    LPCTSTR lpText,     // address of text in message box
    LPCTSTR lpCaption,  // address of title of message box
    UINT uType          // style of message box
   );

MessageBoxEx est la même fonction, avec un cinquième paramètre dont le but est de résoudre la curiosité linguistique déjà évoquée. C'est MessageBox qui nous convient. Vous pouvez étudier la fiche d'aide, vous constatez que uType est une variable qui permet de générer un certain nombre de boîtes différentes.

Quelques boîtes MessageBox
figure 12.04 Quelques boîtes MessageBox

Cette fonction, qui ne retourne au programme appelant que quand la boîte est fermée par l'utilisateur, renvoie une valeur correspondant au choix fait par celui-ci, donc au bouton sélectionné.

uType et la valeur retournée sont données symboliquement, et leurs valeurs numériques ne sont données ni dans la page de l'aide, ni même par un lien figurant sur cette page. C'est très bien, la lisibilité du code va y gagner énormément et notre but sera d'arriver au même résultat en assembleur. Malgré tout, ces valeurs numériques nous seront très utiles pour déboguer l'application, pour savoir par exemple ce que retourne la fonction. Nous allons voir comment les obtenir et pour cela tester cette fonction avec un petit programme en C++ 5.5 Borland. L'aide étant d'origine Microsoft, le fonctionnement est garanti sous les produits de même origine, VC++ par exemple, mais C++ 5.5, produit gratuit, fonctionne très bien. Avant de quitter l'aide, n'oublions pas de noter quelques renseignements intéressants : les fonctions du même groupe, les Voir Aussi ou See Also sont utiles dans la préparation d'un projet, ainsi qu'éventuellement les versions de Windows implémentant la fonction. Mais surtout, pour ce qui nous occupe ici, les deux lignes (ou leur équivalent) :

Header
:  Declared in Winuser.h; include Windows.h.
Library
: Use User32.lib.

Le programme d'essai en C++ 5.5 :

 1 #include <windows.h>
 2 
 3 WINAPI WinMain( HINSTANCE hInstance,
 4                 HINSTANCE hPrevInstance,
 5                 LPSTR lpCmdLine,
 6                 int nCmdShow)
 7 {
 8 const char* MsgBoxTexte = "    Mais c'est un peu normal  ";
 9 const char* MsgBoxTitre = "Le C++, c'est bien pour Windows";
10 char* OuiTexte    = "Tu as dit OUI";
11 char* NonTexte    = "Tu as dit NON";
12 char* tempo;
13 
14 if(MessageBox(NULL,
15               MsgBoxTexte,
16               //"Titre",
17               MsgBoxTitre,
18               MB_YESNO) == IDYES)
19   {
20   tempo = OuiTexte;      
21   }
22 else
23   {
24   tempo = NonTexte; 
25   }
26 
27 MessageBox(NULL,
28            tempo,
29            MsgBoxTitre,
30            MB_OK);
31   
32 return 0;
33 }

Ce listing nous permettra plus tard de valider le fait que la programmation Windows en assembleur bien menée ne soit pas vraiment plus compliquée que son pendant en langage évolué brut. En dé-commentant la ligne 16, au détriment de la ligne 17, nous mettons en évidence une facilité qui n'existe pas en assembleur : la création automatique par le compilateur de la chaîne "Titre" .

Pour ceux qui ne seraient pas familier du langage C : chacune des lignes 10 et 11 déclare un pointeur et l'initialise par l'adresse d'une chaîne (avec caractère 0 final). Par contre, la ligne 12 ne fait que déclarer un pointeur et laisse sa valeur indéterminée. C'est aux lignes 20 ou 24 que la valeur du pointeur tempo est initialisée par la valeur du pointeur OuiTexte ou NonTexte . C'est une adresse qui est recopiée d'une case mémoire vers une autre. Il n'y a pas de copie de chaîne.

Le point important est à la ligne 1. En effet, elle seule peut expliquer que le compilateur accepte sans erreur les identificateurs MessageBox , NULL , MB_YESNO , IDYES , MB_OK . Cherchons, trouvons et chargeons windows.h dans notre éditeur. Déception (attendue), ce n'est qu'un petit fichier dont le rôle est, semble-t-il, en grande partie de charger certains fichiers d'include selon des variables d'environnement. Qu'à cela ne tienne, tous les identificateurs se trouvent vraisemblablement définis dans le même fichier, à l'exception peut-être de NULL , nous effectuons une recherche de fichiers de type  *.h* (pour couvrir *.h et *.hpp ) contenant define MB_OK , en limitant la recherche, dans notre cas, aux dossiers de C++ 5.5. Le résultat est un fichier unique, winuser.h , que la documentation MSDN nous donnait, mais pas win32.hlp . Dans windows.h figure bien la ligne #include <winuser.h> , donc tout va bien. Nous y trouvons, par exemple :

/*
 * MessageBox() Flags
 */
#define MB_OK                       0x00000000L
#define MB_OKCANCEL                 0x00000001L
#define MB_ABORTRETRYIGNORE         0x00000002L
#define MB_YESNOCANCEL              0x00000003L
#define MB_YESNO                    0x00000004L
#define MB_RETRYCANCEL              0x00000005L
#if(WINVER >= 0x0500)
#define MB_CANCELTRYCONTINUE        0x00000006L
#endif /* WINVER >= 0x0500 */

Qui se passe de commentaire pour ceux qui ont la moindre notion de langage C. Il ne faut surtout pas modifier les fichiers d'include, à moins exceptionnellement d'en faire une copie portant un autre nom et de modifier cette copie. En revanche, en C/C++ ou en assembleur, il est souvent utile de se fabriquer des fichiers d'information par des copier-coller. Autre chose, peut-être plus intéressant :

WINUSERAPI
int
WINAPI
MessageBoxA(
    IN HWND hWnd,
    IN LPCSTR lpText,
    IN LPCSTR lpCaption,
    IN UINT uType);
WINUSERAPI
int
WINAPI
MessageBoxW(
    IN HWND hWnd,
    IN LPCWSTR lpText,
    IN LPCWSTR lpCaption,
    IN UINT uType);
#ifdef UNICODE
#define MessageBox  MessageBoxW
#else
#define MessageBox  MessageBoxA
#endif // !UNICODE

Après la définition de deux prototypes, MessageBox est redéfini soit en MessageBoxA , c'est notre cas, soit en MessageBoxW . Ce sont ces deux dernières fonctions qui sont réellement proposées par Windows. Il s'agit des versions normales, ou ANSI, les caractères des chaînes étant codés sur 8 bits, et de la version Unicode ou Wide string, dont les caractères sont codés sur 16 bits. Cette dualité intéresse un très grand nombre des fonctions des API Windows. Nous n'en dirons pas plus, consultez l'aide de Windows pour plus de détails.

Nous ne détaillerons pas non plus le développement de la macro (au sens C/C++) WINUSERAPI . C'est facilement faisable en plusieurs étapes, à coups de recherches. Sachez simplement que c'est par son intermédiaire que sera exprimée la nécessité de lier des éléments de user32.lib à notre application.

Tant que nous sommes chez Borland, copions (surtout pas déplaçons !) temporairement les fichiers user32.lib (depuis C:\Borland\BCC55\Lib\PSDK\ dans notre cas) et user32.dll (du dossier système Windows, utilisez au besoin l'outil recherche de l'Explorateur) dans C:\Borland\BCC55\Bin\ . Cette copie n'est pas réellement nécessaire, mais sous Windows ne prend que quelques secondes et elle facilite le travail. Dans une fenêtre type DOS ouverte dans ce dossier, saisissons :

tdump -li user32.lib >userlib.txt
tdump -ea user32.dll >userdll.txt

Les deux fichiers userlib.txt et userdll.txt vont récupérer en la déroutant la sortie écran de la commande tdump . Pensez à consulter l'aide sur ces outils dans C:\Borland\BCC55\Help\bcb5tool.hlp .

Vous pouvez remplacer le user32.lib par sa version issue du monde Microsoft, donc au format COFF. La commande devient :

tdump -li -C user32.lib >userlibcoff.txt

Vous pouvez faire la même chose pour kernel32.lib et kernel32.dll .

tdump -li -C kernel32.lib >kernellibcoff.txt

Tous les fichiers résultats sont sur le CD-Rom. Les outils de type dump sont nombreux. En environnement Microsoft, vous pouvez utiliser dumpbin.exe . En réalité, c'est un simple lanceur de link.exe avec l'option /dump . Pour obtenir les bons résultats, vous saisirez par exemple :

dumpin /exports user32.lib et link /dump /exports user32.lib

Vous avez même un dumpbin.exe avec son source dans MASM32. Mais si TDUMP reconnaît le format COFF, DUMPBIN ignore le format OMF. Vous pouvez donc mettre TDUMP dans votre boîte à outils.

Les recherches dans les fichiers obtenus donnent (extraits) :

user32.dll

DUMPBIN  :

        452  1C3 0002322E MessageBoxA
        .... 
        457  1C8 0003F574 MessageBoxW

TDUMP  :

    0002322E  452 01C3 MessageBoxA
    ....
    0003F574  457 01C8 MessageBoxW

 

user32.lib (OMF)

TDUMP  :

Impdef: (Name)         USER32.????=MessageBoxA
....
Impdef: (Name)         USER32.????=MessageBoxW

 

user32.lib (COFF)

TDUMP  :

COFF Import: USER32.dll._MessageBoxA@16
COFF Import: USER32.dll._MessageBoxExA@20
COFF Import: USER32.dll._MessageBoxExW@20
....
COFF Import: USER32.dll._MessageBoxW@16

 

DUMPBIN  :

                  _MessageBoxA@16
                  _MessageBoxExA@20
                  _MessageBoxExW@20
                  ....
                  _MessageBoxW@16

 

kernel32.lib (COFF)

TDUMP  :

COFF Import: KERNEL32.dll._ExitProcess@4

Quelques conclusions :

La fonction MessageBox n'existe effectivement pas physiquement.

Les bibliothèques d'importation au format COFF permettent d'accéder à la taille du bloc de paramètres à passer à la fonction. Ces paramètres sont passés par la pile, par quantité indivisible d'un DWORD de 32 bits. Nous retrouvons bien les 4 paramètres des fonctions MessageBox et les 5 des fonctions MessageBoxEx , ainsi que le paramètre unique de ExitProcess .

Munis de ces éléments, nous allons maintenant tenter quelques implantations en assembleur. La solution que nous conseillons pour débuter, et plus, en programmation Windows en assembleur étant le téléchargement et l'installation de l'ensemble MASM32, il n'est pas réellement utile de tester toutes les propositions si vous n'avez pas de contraintes particulières.

Les lignes 14 à 30 du listing C++ sont remplacées par de l'assembleur en ligne :

asm
{
push MB_YESNO
push MsgBoxTitre
push MsgBoxTexte
push NULL
call MessageBox
cmp eax, IDYES
jne @non
mov eax, OuiTexte
mov tempo, eax   
jmp @suite
@non:
mov eax, NonTexte
mov tempo, eax
@suite:
push MB_OK
push MsgBoxTitre
push tempo
push NULL
call MessageBox
}

Bien entendu, tout cela fonctionne. De plus, l'utilisation de MessageBox et des constantes comme MB_YESNO ou NULL est immédiate. En revanche, l'impossibilité d'utiliser la directive invoke de MASM ou le CALL ... METHOD ... en TASM rend déjà cet embryon de listing difficile à lire.

Nous testerons TASM de la façon la plus rustique possible, c’est-à-dire sans les bibliothèques et fichiers d'en-tête fournis avec TASM 5, cette distribution étant commerciale et confidentielle. Nous avons donc, sur l'installation de C++ 5.5 et de TD32 (voir CD-Rom), simplement copié le fichier tasm32.exe . Celui-ci est présent par exemple dans certains produits C++Builder. Nous avons testé :

.LISTALL
 
.386
.MODEL FLAT, stdcall
 
MASM    ;facultatif, car par défaut
 
extrn   MessageBoxA:PROC
extrn   ExitProcess:PROC
 
includelib C:\Borland\BCC55\Lib\PSDK\user32.lib
includelib C:\Borland\BCC55\Lib\PSDK\kernel32.lib
 
.const 
NULL        equ  0 
MB_OK       equ  0
MB_YESNO    equ  4 
IDYES       equ  6
 
.data
MsgBoxTexte      db "      La preuve par TASM32  ",0
MsgTitre         db "L'assembleur, c'est bien pour Windows",0
OuiTexte         db "Tu as dit OUI",0
NonTexte         db "Tu as dit NON",0
 
.code
start:
CALL    MessageBoxA  stdcall, NULL, OFFSET MsgBoxTexte, OFFSET MsgTitre, MB_YESNO
.IF eax == IDYES
       mov eax, offset OuiTexte
.ELSE
       mov eax, offset NonTexte
.ENDIF
CALL    MessageBoxA  stdcall, NULL, eax, OFFSET MsgTitre, MB_OK
CALL    ExitProcess  stdcall, 0
end start

Sans oublier le fichier batch :

del msgbox.obj
del msgbox.exe
C:\borland\bcc55\bin\tasm32 /ml /l msgbox.asm
C:\borland\bcc55\bin\ilink32 /aa msgbox.obj 

Nous voyons que nous utilisons le lieur ilink32.exe et les bibliothèques d'importation au format OMF de C++ 5.5. Donc, pas de fichier d'en-tête. Si vous continuiez à travailler de cette façon, il serait opportun d'isoler les quelques equ , extrn et includelib concernant les fonctions Windows dans un fichier Mon_Win.inc par exemple, qui évoluerait peu à peu. C'est déjà une bonne raison d'utiliser includelib  plutôt que de lier la bibliothèque d'importation par le fichier batch. À voir sur le CD-Rom, dossier TASM_INC . L'assemblage est configuré pour générer un fichier listing, que vous trouverez également sur le CD-Rom.

Voyez sur ce fichier msgbox.LST le traitement des .IF , .ELSE , .ENDIF , CALL .. stdcall <paramètres> . Nous observons que ces facilités améliorent du tout au tout la lisibilité et la facilité d'écriture, mais que cela reste de l'assembleur.

Si nous enlevons le paramètre MB_OK à la fin du second CALL , ce qui est une erreur, l'assemblage se déroulera sans problème. Le programme fonctionnera plus ou moins, mais il aurait fort bien pu planter, puisque nous n'empilons que 3 paramètres avant l'appel à MessageBoxA , qui va nous renvoyer une pile décalée. Ce comportement de l'assembleur n'est pas très satisfaisant, ce type de message d'erreur étant très utile au programmeur.

Passons maintenant à MASM :

.LISTALL ; pour lister le résultat des .IF .. .ELSE .. .ENDIF
 
.386
.model flat, stdcall
option casemap:none
 
 
includelib c:\masm\lib1\kernel32.lib
includelib c:\masm\lib1\user32.lib
 
;includelib C:\Lib\kernel32.lib ;de VC++6
;includelib C:\Lib\user32.lib   ;de VC++6
 
;includelib F:\SDK\Lib\kernel32.lib ;du SDK
;includelib F:\SDK\Lib\user32.lib   ;du SDK
 
ExitProcess PROTO ,:DWORD
MessageBoxA PROTO ,:DWORD, :DWORD, :DWORD, :DWORD 
 
.const 
NULL        equ  0 
MB_OK       equ  0
MB_YESNO    equ  4 
IDYES       equ  6
 
.data
MsgBoxTexte      db "      La preuve par MASM  ",0
MsgTitre         db "L'assembleur, c'est bien pour Windows",0
OuiTexte         db "Tu as dit OUI",0
NonTexte         db "Tu as dit NON",0
 
.code
start:
 invoke MessageBoxA, NULL, addr MsgBoxTexte, addr MsgTitre, MB_YESNO 
 .IF eax == IDYES
         mov eax, offset OuiTexte
 .ELSE
         mov eax, offset NonTexte
 .ENDIF
 invoke MessageBoxA, NULL, eax, addr MsgTitre, MB_OK
 invoke ExitProcess, 0
end start

et le fichier batch :

del msgbox.obj
del msgbox.exe
c:\masm\bin\ml /c /coff /Cp /Fl msgbox.asm
c:\masm\binr\link32 /SUBSYSTEM:WINDOWS msgbox.obj

Trouver les fichiers user32.lib et kernel32.lib au format COFF ne devrait pas poser de problèmes, les sources sont multiples, SDK ou MASM32 par exemple. ml.exe peut être n'importe quelle version récente, 6.15 par exemple. Le lieur doit être le lieur incrémental 32 bits, qu'il se nomme link.exe ou link32.exe . Attention, pour utiliser le lieur de Visual Studio 6 ou .NET sans en utiliser l'environnement, il faut peut-être copier dans le même dossier mspdb60.dll ou mspdb70.dll .

Dans le fichier batch, vous pouvez essayer /SUBSYSTEM:CONSOLE en option du lieur. Si vous lancez l'exécutable à partir de l'Explorateur, vous verrez apparaître une fenêtre type DOS, mais pour le reste, tout fonctionnera. Il est donc bien possible d'appeler un MessageBox à partir d'une application console.

Si nous créons une erreur en enlevant le dernier paramètre du second invoke , un message d'erreur apparaît :

msgbox.asm(40) : error A2137: too few arguments to INVOKE

Ceci constitue un gros avantage.

Mais le plus simple est d'utiliser MASM32. Syntaxiquement, c'est du MASM. Si vous utilisez QEDITOR, il suffit d'inclure systématiquement le bloc suivant en tête de listing (à la place des include , includelib et PROTO ) :

.NOLIST
include \masm32\include\windows.inc
include \masm32\include\kernel32.inc
include \masm32\include\user32.inc
includelib \masm32\lib\user32.lib
includelib \masm32\lib\kernel32.lib

Pour travailler à partir d'une autre partition, vous aurez peut-être, comme dans l'exemple du CD-Rom, à modifier ces lignes pour indiquer le chemin réel de MASM32 :

.NOLIST
include c:\masm32\include\windows.inc
include c:\masm32\include\kernel32.inc
include c:\masm32\include\user32.inc
includelib c:\masm32\lib\user32.lib
includelib c:\masm32\lib\kernel32.lib

Nous fournissons sur le CD-Rpm une version de monbuild.bat , modifiée pour pouvoir être appelée depuis n'importe quel endroit et pour générer un listing. Ce fichier peut, soit être placé dans le dossier des sources, soit dans celui des binaires de MASM32. Dans ces conditions, le batch se limite à :

c:\masm32\bin\monbuild msgbox

ou :

monbuild msgbox

Bien entendu, dans ce dernier cas, autant saisir cette ligne directement.

Nous sommes maintenant en possession de tous les éléments pour choisir et utiliser un environnement de développement pour programmer sous Windows en pur assembleur. Nous avons confirmé que MASM32 est un excellent choix, en ce sens qu'il permet effectivement de démarrer sans chercher des outils à droite et à gauche.

Attention

Noms de fichiers

Le monde Windows s'accommode de noms de fichiers interminables et de profondeurs de dossiers abyssales. Ce n'est pas le cas de tous les outils que vous allez utiliser, et les erreurs générées sont parfois sournoises. De plus, vous allez saisir vos commandes au clavier. Donc, choisissez en début de travail des noms de fichiers et de dossiers au format 8.3 et placez le dossier projet à la racine du disque ou très près. Vous pourrez toujours modifier cela à l'archivage ou à l'installation.

Avant de présenter l'application squelette SK_WIN , il nous faut dire un mot des ressources en général, et sous Windows en particulier.

12.3 Les fichiers de ressources

L'utilisation des fichiers de ressources est un moyen efficace de gérer et d'inclure dans l'exécutable, de type .exe , .dll ou toute autre extension, des ressources textes et graphiques des types suivants :

  Icônes ;

  Curseurs ;

  Images de type bitmap ;

  Chaînes de caractères ;

  Menus ;

  Boîtes de dialogue ;

  Raccourcis clavier ;

  Ressource personnalisée.

Il n'est jamais absolument obligatoire d'utiliser cette technique : les chaînes de caractères peuvent être déclarées dans le programme, les menus, boîtes de dialogue, raccourcis clavier peuvent être créés par le programme à partir de chaînes constantes déclarées dans le code source, les icônes, curseurs et bitmaps peuvent être chargés à partir du disque. Mais dans chaque catégorie, la solution du fichier de ressources apporte un plus certain. Pour les ressources graphiques, c'est la nécessité de les livrer à part en l'absence de ressources qui peut poser problème, encore que dans ce cas les deux solutions ont leurs avantages. Pour les chaînes de caractères, les ressources facilitent la production de plusieurs versions linguistiques. Pour les objets Windows, menus et boîtes de dialogue particulièrement, c'est le travail qui est réellement simplifié.

Au départ est un script de ressource, en .rc , sur lequel nous allons nous pencher. C'est un code source issu d'un éditeur de texte, qui demande à être compilé en un fichier objet, afin d'être lié aux fichiers de même type des modules du projet, ainsi que les bibliothèques. Des compilateurs de ressources existent dans chaque environnement. Chez Borland, nous trouvons brc32.exe et brcc32.exe . Chez Microsoft, rc.exe permet de passer du script en  .rc au format de ressources compilées en  .res , qui est transformé en  .obj par cvtres.exe . Le lieur accepte de lier des fichiers  .res directement, mais il utilise cvtre.exe qui doit se trouver dans le même dossier. C'est à cause du fichier objet que le fichier de ressources ne peut porter le même nom que celui de l'application, sous peine d'écraser le premier fichier projet.obj créé par le second. C'est un peu embêtant, puisque conduisant souvent à nommer tous les fichiers de script de ressource resource.rc .

Le problème n'est généralement pas de compiler le script de ressource, mais bien de l'écrire. La documentation sur ce langage semble rare. Il paraît que nombre de programmeurs utilisent des outils de génération ou alors font du copier-coller modifier sur de l'existant. Mais joie, une fois n'est pas coutume, le fichier d'aide rc.hlp du compilateur de ressources contient la définition complète du langage. Il est possible de trouver ce fichier un peu partout, dans MASM32, dossier bin ou dans le dossier MSHelp évoqué en tout début de chapitre.

À vous de choisir votre stratégie pour élaborer le script de ressource. Pour les petits fichiers et les corrections, il est clair que le mieux, y compris pour la sérénité, est de travailler directement sur le fichier texte, qui sera ajouté au projet si vous gérez cette notion, avec récupération de l'existant par copier-coller. Pour les boîtes de dialogue et les menus, vous pouvez utiliser un éditeur graphique de ressources. Nous allons voir ce que nous pouvons faire avec Visual Studio, version .NET ou version 6, qui semble plus pratique. Par ailleurs, des produits un peu anciens et rustiques peuvent être meilleurs pour un travail orienté assembleur.

Trois versions du projet SK_WIN sont fournies sur le CD-Rom. Dans le dossier sk_win_1 , un fichier de script de ressource est simplement fourni et les EQU sont inclus dans le fichier source. Vous pouvez les éditer à la main ou selon la méthode de votre choix. Si vous ne souhaitez pas utiliser Visual Studio, vous pouvez passer maintenant à la section suivante, la présentation de l'application SK_WIN .

Le dossier sk_win_2 propose une solution intermédiaire, basée sur l'utilisation de VC++ 6. Voici la procédure générale pour en arriver au contenu de ce dossier : d'abord, et c'est indispensable, créez un dossier spécifique pour les fichiers Visual Studio dans le dossier du projet. Les problèmes de nom de dossier 8.3 ne se posent pas vraiment à ce niveau. Vous pouvez mettre dans ce dossier une copie des fichiers h2inc.exe et h2inc.err .

Lancez VS .NET, puis Fichier / Nouveau / Fichier... , puis dans la catégorie Général , choisissez Modèle de ressource ou VC++ 6, et Fichier / Nouveau , catégorie Fichiers , puis Script de ressource . Travaillez votre fichier de ressources comme vous en avez l'habitude, n’hésitez pas à créer vos icônes et bitmaps directement sous VS, éventuellement en dessinant de simples lettres pour les identifier en phase de développement. Vous aurez bien le temps ensuite d'en dessiner ou d'en trouver de plus jolis. Sauvez votre travail dans le dossier créé à cet effet. Si vous êtes en .NET, sauvez également sous le type script de ressource .

Vous aurez ainsi dans un dossier les fichiers graphiques créés, éventuellement des fichiers de type  .aps ou .rct qui vous serviront à reprendre le projet sous VS et surtout un fichier proj1.rc , proj1 étant remplacé par le nom que vous aurez choisi et un fichier resource.h .

Il serait intéressant de tester les fichiers  .res issus directement de VS .NET, mais ce n'est pas l'option qui a été retenue. De plus, au moment des tests, nous avions des problèmes avec VS .NET, problèmes certainement indépendants des scripts de ressource.

Sous Invite de commandes, saisissez h2inc /Ht /WIN32 resource.h . Vous vous retrouvez en possession d'un fichier resource.inc et du fichier proj1.rc , contenant les éléments de vos ressources, noyés dans des lignes de code inutile. Vous pouvez ajouter directement include resource.inc dans votre fichier proj1.asm ou proj1.inc si vous en gérez un. Mais le plus simple est de procéder par copier-coller, ce qui permettra facilement d’ajouter des éléments.

Une troisième solution, plus complexe, qui fonctionne avec VC++ 6, est proposée dans le dossier sk_win_3  : d'abord, remarquons qu'il est possible de récupérer pour modifications un script de ressource, pour peu que les fichiers attachés, icônes et bitmaps en particulier, soient présents dans le même dossier. Que vous partiez d'un fichier existant ou de rien, les fichiers sauvés doivent l'être dans un dossier réservé.

La ligne suivante a été ajoutée à ..\SK_WIN_3\VS6\resource.rc  :

#include "c:\masm32\include\resource.h"

Modifiez-la au besoin selon vos dossiers. Il faut inclure un fichier équivalent ou alors les lignes suivantes :

#define DS_MODALFRAME 0x00000080L
#define WS_SYSMENU    0x00080000L
#define WS_CAPTION    0x00C00000L
#define WS_POPUP      0x80000000L

Une fois que vous avez modifié le fichier de ressources dans Visual Studio, copiez les fichiers resource.rc et resrc1.h dans le fichier du projet en écrasant les anciens et effacez les lignes suivantes dans la copie de resource.rc  :

#ifdef _WIN32
LANGUAGE LANG_FRENCH, SUBLANG_FRENCH
#pragma code_page(1252)
#endif //_WIN32

Attention, si vous modifiez par mégarde le fichier original ..\SK_WIN_3\VS6\resource.rc , Visual Studio va perdre la cohérence, recréer un fichier  .h , etc. En bref vous risquez de devoir repartir de la version du CD-Rom. Ceci s'applique à toute modification manuelle dans le dossier ..\SK_WIN_3\VS6\ , qui entraîne généralement des problèmes au rechargement de resource.rc dans Visual Studio.

Les fichiers graphiques moi.bmp et sk_win.ico sont nécessaires pendant la construction, mais peuvent ensuite être retirés du dossier du projet. La taille des fichiers le montre clairement, ils ne sont que référencés dans resource.rc , mais sont déjà présents en binaire dans resource.res , et à fortiori, dans resource.obj et dans sk_win.exe , dont ils constituent plus de la moitié du volume, mais pas dans sk_win.obj .

Cette dernière version de sk_build.bat présente un défaut : le travail sur les ressources sera fait à chaque construction, alors que dans de nombreux cas ce ne sera pas nécessaire. Ceci n'est pas gênant du tout sur un petit projet compilé sur une machine simplement normale, mais dans d'autres circonstances peut justifier l'écriture d'un makefile, que vous trouverez sur le CD-Rom.

12.4 Application squelette : SK_WIN

Les environnements graphiques et les outils de développement évolués ont modifié profondément la façon de travailler. Si la phase d'analyse demeure, la phase de codage peut très bien commencer par la programmation de l'interface, du cadre dans lequel les fonctions nobles de l'application vont tourner.

De la même façon, avant de décortiquer SK_WIN , application sans fonction noble, il est utile de savoir ce qu'elle fait.

12.4.1 Présentation de SK_WIN

C'est une simple fenêtre Windows, dotée d'un menu de base gérant des raccourcis clavier, de trois boutons-outils (tool bar) ave des info-bulles, d'une barre d'état affichant des textes d'aide.

Les menus de SK_WIN
figure 12.05 Les menus de SK_WIN

Les menus ne font pas grand-chose. Toutefois, ils sont fonctionnels, tout comme les raccourcis clavier. Pour simuler une gestion de fichiers, les boîtes de dialogue d'ouverture et de sauvegarde de fichier sont lancées.

Ouverture de fichier
figure 12.06 Ouverture de fichier

Le titre de la fenêtre suit la simulation d'ouverture de fichier et repasse à Document-1 quand Nouveau est choisi dans le menu.

Les deux rubriques du menu Aide ouvrent la boîte À propos .

Who ?
figure 12.07 Who ?

Enfin, l'application se lance au centre de l'écran et la boîte de dialogue À propos s'ouvre au centre de la fenêtre SK_WIN . Il faudra donc une fonction de centrage.

Nous venons de voir avec les fichiers de ressources qu'il existe trois versions de ce projet sur le CD-Rom. Le plus simple est peut-être de commencer la lecture à partir du projet du dossier sk_win_1 . Vous trouvez comme d'habitude dans le Lise & Moi.txt les informations sur les modifications à faire en fonction de l'emplacement de vos outils, les versions de ceux-ci, etc.

La lecture pourra se faire en manipulant ou en consultant le code source et en lançant l'exécutable.

Lancer l'exécutable est réellement utile, pour faire le rapprochement entre le code et le comportement. Or, une application Windows même dépouillée est vite riche de petits détails, textes d'aide dans une barre d'état, bulles d'information...

Le code source complet, d'un bloc, ne figure pas dans ces pages. Il est donc intéressant de pouvoir le parcourir, de préférence dans un bon éditeur avec coloration syntaxique. De plus sur le CD-Rom figurent les fichiers annexes de l'application, utiles à consulter.

Peut-être le plus utile, une documentation sur les fonctions Windows. Le fichier win32.hlp fera parfaitement l'affaire.

La construction se fera en ligne de commande par ..\SK_WIN_3>sk_build sk_win DEBUG ou ..\SK_WIN_3>sk_build sk_win FINAL . FINAL n'est pas important, il suffit de saisir autre chose que DEBUG pour compiler une version normale. Pour connaître la différence entre les deux commandes, vous pouvez étudier le fichier sk_build.bat et dans le code source les séquences ressemblant à IFDEF DEBUG ... ENDIF .

12.4.2 Initialisations

Nous avons évoqué le fait qu'il est théoriquement possible d'entamer un programme Windows de diverses façons. Mais nous avons également convenu d'utiliser comme en C/C++, et comme tout le monde en assembleur, la fonction WinMain() , en remarquant que cette fonction n'est pas une fonction Windows et qu'il nous appartient d'en définir (librement) le prototype. En voici la version standard :

WinMain     PROTO   STDCALL hInstance:     HINSTANCE, 
                            hPrevInstance: HINSTANCE,
                            pCmdLine:      LPSTR,
                            CmdShow:       DWORD

La présentation sur plusieurs lignes est courante pour les fonctions et structures de Windows. Les noms des variables n'ont aucune utilité, ce sont en fait des commentaires. La ligne suivante fonctionnerait ici sans problème :

WinMain PROTO STDCALL :HINSTANCE, :HINSTANCE, :LPSTR, :DWORD

Il ne semble, par exemple, pas poser de problème d'enlever toute référence aux paramètres passés par la ligne de commande, dans le prototype et le bout de code qui suit. Vous trouverez dans les fichiers du CD-Rom des éléments vous permettant de faire des tests si vous le souhaitez : toutes les références à WinMain sont dans un seul fichier, vous pouvez donc en changer le nom aisément dans votre éditeur. Vous pouvez aussi changer le prototype. Vous pouvez tester, dans sk_build.bat , /ENTRY:WinMain et /ENTRY:Start (ou rien). Des séquences IFDEF DEBUG ... ENDIF vous permettent de voir où commence réellement le programme.

Start:  
IFDEF DEBUG
invoke  MessageBox, NULL, ADDR szMessageStart, ADDR szMessageStart, MB_OK
ENDIF
invoke  GetModuleHandle, NULL
mov     hInstance_appli, eax
invoke  GetCommandLine
mov     lpCmdLine, eax
mov     nCmdShow, SW_SHOWDEFAULT
; ici autres pré-initialisations
invoke  WinMain, hInstance_appli, 0, lpCmdLine, nCmdShow
invoke  ExitProcess, eax

La documentation nous apprend que la fonction GetModuleHandle()  renvoie le handle d'un module spécifié par un pointeur sur son nom, si ce module est mappé dans l'espace du processus appelant. Le pointeur NULL , soit 0, identifie le module appelant lui-même. La fonction renvoie NULL en cas d'échec. La raison de l'échec peut s'obtenir en appelant GetLastError() . Ce squelette d'application se soucie peu de gestion d'erreur. Ici, il serait possible d'écrire :

invoke  GetModuleHandle, NULL
.IF(eax == NULL)
invoke GetLastError
jmp return
.ENDIF
mov hInstance_appli, eax
...
return:
invoke ExitProcess, eax

Ainsi, le programme s'arrêterait et le code de retour, dans EAX, serait le code d'erreur renvoyé par GetLastError .

C'est ensuite le paramètre pointeur lpCmdLine qui est initialisé par le résultat de GetCommandLine() , et nCmdShow par la valeur SW_SHOWDEFAULT . Ce dernier paramètre sera passé plus tard à une fonction ShowWindow() . Le paramètre hPrevInstance de WinMain est toujours à NULL sous Win32.

12.4.3 WinMain() première partie

La fonction WinMain est finalement appelée. Elle ne retournera que pour que le programme se termine. Le code de retour de WinMain()  sera envoyé à ExitProcess() . Cette dernière fonction est plus complexe qu'il n'y paraît, en particulier pour des applications multithreads. Reportez-vous à la documentation.

Voici le code de WinMain()  :

WinMain PROC    STDCALL, hInstance:HINSTANCE,
                         hPrevInstance:HINSTANCE,
                         pCmdLine:LPSTR,
                         CmdShow:DWORD
 
LOCAL   msg:MSG
 
IFDEF DEBUG
invoke  MessageBox, NULL, ADDR szMessageWMain, ADDR szMessageWMain, MB_OK
ENDIF
call    InstanceUnique  ;Si une seule instance autorisée
.IF (eax)
    call    Initialise_fenetre
    .IF (eax)
        .IF (hAccel)
            .WHILE TRUE
                invoke  GetMessage, ADDR msg, NULL, 0, 0
                .BREAK .IF (!eax)
                invoke  TranslateAccelerator, msg.hwnd, hAccel, ADDR msg
                .CONTINUE .IF (eax) ; skip if we handled
                invoke  TranslateMessage, ADDR msg
                invoke  DispatchMessage,  ADDR msg
            .ENDW
            mov eax, msg.wParam     ; wParam == Exit Code
        .ENDIF
    .ENDIF
.ENDIF
ret
WinMain ENDP

Tout d'abord, dans cet exemple, nous avons décidé qu'une seule instance de SK_WIN ne doit tourner. Si la fonction InstanceUnique détecte une autre instance, elle renvoie faux, soit 0 dans EAX. Dans ce cas, WinMain() se termine, en renvoyant 0 dans EAX. C'est conforme à la documentation, qui stipule que WinMain() renvoie 0 si le programme n'est pas rentré dans la boucle de messages. Voyons comment est détectée cette autre instance. Vous pouvez, en première lecture, passer directement à l'étude de Initialise_fenetre .

Les minuscules macros SautSiFaux et MetAZero sont dans le fichier sk_win.inc et n'ont rien de rare.

InstanceUnique  PROC    STDCALL
 
LOCAL   hSemaphore, hTempo:HANDLE
 
invoke  CreateSemaphore, NULL, 0, 1, ADDR szClassName
SautSiFaux eax, Return
mov hSemaphore,eax
invoke  GetLastError
.IF (eax == ERROR_SUCCESS)
    mov eax, TRUE
    ret
.ENDIF
 
.IF (eax == ERROR_ALREADY_EXISTS)
    invoke  CloseHandle, hSemaphore
.ENDIF
 
invoke  FindWindow, ADDR szClassName, NULL
SautSiFaux eax, Return
 
invoke  GetLastActivePopup, eax
mov hTempo, eax
 
invoke  IsIconic, hTempo
.IF (eax)
    invoke  ShowWindow, hTempo, SW_RESTORE
    jmp RetourneNul
.ENDIF
invoke  SetForegroundWindow, hTempo
 
RetourneNul:    
MetAZero    eax
Return:     
ret
 
InstanceUnique  ENDP

Le sémaphore est un objet de synchronisation proposé par Windows. En très gros, un sémaphore est un objet qui détient un certain nombre de jetons à distribuer. La fonction WaitForSingleObject() , parmi d'autres, permet à un thread d'attendre la disponibilité d'un jeton, quand il est rendu par un autre thread par exemple.

Un sémaphore est détruit par Windows au moment de la fermeture du processus qui l'a créé. Parcourez la documentation, en cherchant sur CreateSemaphore et synchronisation , tout en sachant qui si tout n'est pas clair immédiatement, ce n'est pas grave pour notre utilisation. Si le paramètre lpName de CreateSemaphore()  n'est pas NULL , le sémaphore devient un objet nommé. Et c'est simplement d'un objet nommé dont nous avons besoin, sémaphore, mutex ou autre. Le nom est libre, nous avons choisi arbitrairement le nom de la classe de l'application, SK_WIN .

Nous appelons CreateSemaphore() avec le nom SK_WIN en paramètre. Si erreur inattendue, indiquée par EAX à NULL ou FALSE , qui peut être due à l'existence d'un objet de même nom mais d'un autre type que sémaphore, le programme va s'arrêter, par retour à WinMain() en renvoyant NULL .

Qu'un sémaphore de ce nom existe ou pas, la fonction renverra un handle sur l'objet existant ou créé. Pour en savoir plus, il faut appeler GetLastError()  immédiatement après.

Si la réponse dans EAX est ERROR_SUCCESS , alors aucun sémaphore nommé SK_WIN n'existe, donc le programme peut continuer sa route, en laissant le sémaphore nommé à l'attention d'une éventuelle nouvelle tentative de lancement. Donc retour à WinMain() avec la valeur TRUE , c’est-à-dire 1, ou plus exactement pas 0.

Ensuite, en vérifiant si la réponse est bien ERROR_ALREADY_EXISTS , le handle est libéré par CloseHandle() , ce qui a pour effet de libérer le jeton.

invoke  FindWindow, ADDR szClassName, NULL
SautSiFaux eax, Return

FindWindow() retrouve le handle de la fenêtre principale identifiée ici par son nom de classe. Si la fonction retourne 0, c'est qu'il n'en existe pas, donc retour à WinMain() avec la valeur FALSE .

invoke  GetLastActivePopup, eax
mov hTempo, eax

La fonction FindWindow()  renvoie toujours le handle de la fenêtre parent. Si, au moment du lancement de la nouvelle instance, une fenêtre fille est ouverte, comme la boîte À propos ou une boîte de dialogue d'ouverture de fichier, le handle que nous possédons ne conviendra pas, le code qui suit donnera le focus à la fenêtre parent, sous la fenêtre fille, qui de plus est modale. GetLastActivePopup()  va nous renvoyer le handle sur cette fenêtre fille, ou celui de la fenêtre principale si aucune fenêtre secondaire n'est active. Vous pourrez mettre la première de ces deux lignes en commentaire et manipuler le programme obtenu pour constater l'effet. Mais auparavant, voyons quoi faire de ce handle.

invoke  IsIconic, hTempo
.IF (eax)
    invoke  ShowWindow, hTempo, SW_RESTORE
    jmp RetourneNul
.ENDIF
invoke  SetForegroundWindow, hTempo

Muni de l'aide Windows, ces lignes doivent maintenant être claires : IsIconic()  renvoie TRUE si la fenêtre dont le handle hTempo est iconisée, c’est-à-dire réduite à une icône dans la barre des tâches. La fonction ShowWindow() avec le paramètre SW_RESTORE la sort de cet état en l'affichant comme juste avant l'iconisation. Si la fenêtre n'est pas iconisée, SetForegroundWindow()  la mettra au premier plan, lui donnera le focus.

Au retour dans WinMain() , InstanceUnique renvoie FALSE si une autre instance a été détectée, dans ce cas le programme se termine, avec un code de sortie à 0. Dans le cas contraire, la construction de la fenêtre principale de l'application peut commencer.

12.4.4 Initialisation de la fenêtre

La construction de la fenêtre principale de l'application se fait donc dans la procédure Initialise_fenetre()  :

;=================================================================
; Procédure Initialise_fenetre
;-----------------------------------------------------------------
; Rôle du bloc : 
;               Initialisations
;               Appel de WinMain
;               Fin du programme 
;-----------------------------------------------------------------
Initialise_fenetre  PROC    STDCALL
 
LOCAL   wc:WNDCLASSEX
 
mov wc.cbSize, SIZEOF WNDCLASSEX
mov wc.style, CS_HREDRAW or CS_VREDRAW
mov wc.lpfnWndProc, OFFSET WndProc
MetAZero    eax
mov wc.cbClsExtra, eax
mov wc.cbWndExtra, eax
 
MMOV    wc.hInstance, eax, hInstance_appli
 
invoke  LoadIcon, hInstance_appli, IDI_ICON
mov wc.hIcon, eax
 
invoke  LoadCursor, NULL, IDC_ARROW
mov wc.hCursor, eax
 
mov wc.hbrBackground, COLOR_HIGHLIGHTTEXT + 1
mov wc.lpszMenuName,  IDM_MENU
mov wc.lpszClassName, OFFSET szClassName
 
invoke  LoadImage, hInstance_appli, IDI_ICON, IMAGE_ICON, 16, 16, NULL
mov wc.hIconSm, eax
 
invoke  RegisterClassEx, ADDR wc
.IF (eax)
    invoke  CreateWindowEx,NULL, 
                           ADDR szClassName,
                           ADDR szClassName,
                           WS_OVERLAPPEDWINDOW,
                           0,
                           0,
                           LARGEUR_INIT,
                           HAUTEUR_INIT,
                           NULL,
                           NULL,
                           hInstance_appli,
                           NULL
    .IF (eax)
        mov hMainWnd,eax
        invoke  ShowWindow, hMainWnd, nCmdShow
        invoke  UpdateWindow, hMainWnd
    .ENDIF
.ENDIF
 
ret
 
Initialise_fenetre  ENDP

 

MMOV est une macro qui transfère le troisième paramètre dans le premier, par deux MOV , en passant par le deuxième paramètre.

Nous voyons que nous initialisons une structure, wc , avant d'appeler la fonction RegisterClassEx() . Que nous dit l'aide à son propos ? Que cette fonction enregistre la classe de notre fenêtre au sens de Windows, c’est-à-dire une entité pas nécessairement graphique, susceptible d'envoyer des messages, pour usage ultérieur.

Par cette action, elle devient en quelque sorte un objet de Windows, que celui-ci va pouvoir instancier plusieurs fois sans duplication de code, si le programmeur ne l'interdit pas comme c'est le cas de SK_WIN . Cet enregistrement cesse à la fin du programme qui l'a sollicité. La valeur de retour est un atome , qui désigne de façon unique la classe enregistrée.

Précisons bien ce point, sous peine de voir le raisonnement partir dans une fausse direction. Le programme, ou processus, existe depuis que le loader de Windows l'a installé en mémoire et l'a lancé. Il est désigné par Windows par un handle, que nous avons demandé, obtenu et sauvé dans la variable hInstance_appli . Ce nom serait peut-être mal choisi dans le cadre d'une application multithread et/ou multimodule, hInstance_main serait plus juste. Même la fonction WndProc existe, simplement elle n'est connectée à rien pour l'instant.

Maintenant, le programme va communiquer à Windows quelques éléments pour décrire un nouvel objet ou fenêtre personnalisés, que Windows va enregistrer dans sa liste de plans, les classes. C'est RegisterClass() . Dans les maigres renseignements fournis figure l'adresse de la fonction qui devra traiter les messages concernant cette fenêtre, quelques éléments que Windows ne détient pas, comme les icônes, curseurs personnalisés, structure et textes du menu.

Voilà, Windows a tout ce qu'il faut pour bosser, le programme lui demande de lui fabriquer une fenêtre conforme aux plans, en lui communiquant quelques détails supplémentaires. C'est CreateWindowEx() . Windows se met au travail, en quelque sorte il fabrique l'objet fenêtre chez lui, puisque c'est lui qui va le gérer. Un peu avant la fin de ce travail, il va envoyer à WndProc un message WM_CREATE . Ceci permettra au programme de faire les finitions à sa guise, d’ajouter boutons, barres d'état, etc. à la fenêtre, ou plus exactement de demander à Windows d’ajouter ces éléments. Attention, cette phase n'est pas visible dans la fonction Initialise_fenetre . Elle se déroule pendant la durée de la fonction CreateWindowEx() . Il est ensuite possible de demander à Windows d'afficher la fenêtre s'il y a lieu, par ShowWindow() et UpdateWindow() .

RegisterClassEx() ne prend qu'un seul argument, c'est un pointeur vers une grosse structure de type WNDCLASSEX . Voici, extrait de win32.hlp , la définition de cette structure :

typedef struct _WNDCLASSEX { // wc  
    UINT    cbSize; 
    UINT    style; 
    WNDPROC lpfnWndProc; 
    int     cbClsExtra; 
    int     cbWndExtra; 
    HANDLE  hInstance; 
    HICON   hIcon; 
    HCURSOR hCursor; 
    HBRUSH  hbrBackground; 
    LPCTSTR lpszMenuName; 
    LPCTSTR lpszClassName; 
    HICON   hIconSm; 
} WNDCLASSEX;

Des structures telles celle-ci sont d'un usage très courant en programmation Windows, c'est la première que nous rencontrons dans ce chapitre, détaillons donc les éléments :

cbSize contient la taille, en octets, de la structure WNDCLASSEX . Cette façon de faire, à savoir que le premier champ d'une structure est la longueur en octets de la structure, est très courante. Cette donnée peut permettre à Windows de discriminer les différentes versions d'API. Elle est initialisée par SIZEOF WNDCLASSEX . Il est important de bien voir ce qui se passe quand nous saisissons ces deux mots. Le trajet de l'information, en quelque sorte. L'opérateur SIZEOF renvoie une constante que MASM a calculé à partir de la déclaration trouvée dans un fichier d'include, comme cet extrait de windows.inc de MASM32 :

WNDCLASSEX STRUCT
  cbSize            DWORD      ?
  style             DWORD      ?
  lpfnWndProc       DWORD      ?
  cbClsExtra        DWORD      ?
  cbWndExtra        DWORD      ?
  hInstance         DWORD      ?
  hIcon             DWORD      ?
  hCursor           DWORD      ?
  hbrBackground     DWORD      ?
  lpszMenuName      DWORD      ?
  lpszClassName     DWORD      ?
  hIconSm           DWORD      ?
WNDCLASSEX ENDS

SIZEOF WNDCLASSEX vaut donc 12 x 4 = 48 octets.

style détermine le style et les comportements de la fenêtre. Ce sont des constantes comme CS_HREDRAW et CS_VREDRAW . Elles signifient que l'ensemble de la fenêtre doit être redessiné si respectivement la largeur et la hauteur de la zone client de la fenêtre changent. Ces constantes ont des valeurs particulières 0, 1, 2, 4, 8, 16, etc. pour lesquelles un seul des 32 bits est à 1, ce qui permet de déclarer plusieurs comportements en les mettant en OU bit à bit.

lpfnWndProc est un pointeur sur la fonction de callback WndProc , chargée de traiter les messages.

cbClsExtra et cbWndExtra indiquent le nombre d'octets supplémentaires à allouer respectivement à la classe et à l'instance.

hInstance est le handle de l'instance contenant WndProc .

hIcon et hCursor sont les handles sur l'icône et le curseur de la classe.

hbrBackground représente la couleur de la zone client.

lpszMenuName pointe vers une chaîne de caractères représentant le nom de la ressource menu dans le fichier de ressources.

lpszClassName pointe vers une chaîne de caractères représentant le nom de la classe ou bien est un atome. Dans ce cas, 16 bits suffisent, les 16 autres seront mis à 0.

hIconSm est un handle sur une petite icône. C'est la nouveauté de WNDCLASSEX par rapport à WNDCLASS .

Vous pouvez consulter la documentation pour les fonctions LoadIcon() , LoadCursor()  et LoadImage() . Rien de bien compliqué arrivé à ce stade. Ces consultations de la documentation sont une bonne partie de l'activité de programmation Windows.

Si l'enregistrement s'est bien passé, c’est-à-dire si EAX est non nul, il est temps d'instancier la classe SK_WIN enregistrée, c’est-à-dire demander la fabrication d'un objet selon le modèle. C'est ce que fait CreateWindowEx() , pendant laquelle WndProc est sollicitée avec le message WM_CREATE pour ajouter des éléments à la fenêtre. L'étude de CreateWindowEx() peut vous occuper un petit moment, à cause pas tant du nombre de paramètres que de leur grande variété de valeurs possibles. C'est typiquement le genre de fonction dont vous pouvez imprimer la documentation et la glisser dans un classeur.

Récapitulons :

La classe de la fenêtre a été enregistrée.

Une instance (notre fenêtre) a été créée. En deux temps, puisque nous savons que du travail a été fait dans le traitement du message WM_CREATE . Nous savons également que ce travail est terminé quand CreateWindowEx() retourne.

Créée ne signifie pas visible. ShowWindow()  va maintenant afficher la fenêtre, puisque nous savons qu'elle est terminée quand CreateWindowEx() rend la main. Pour interpréter le paramètre nCmdShow , il faut commencer par remonter à la première ligne du programme et ce n'est pas le bout de la route. Il y a plus important pour l'instant, le résultat est que la fenêtre sera visible.

Ce n'est pas encore terminé. Dans une fenêtre Windows, une grande partie de la gestion graphique est assurée par Windows, une autre par l'application, dont c'est parfois le métier. Nous n'allons pas entrer dans les détails, la GDI (Graphics Device Interface) justifiant plusieurs livres à elle seule. Disons que quand l'application elle-même ou un événement comme un changement de taille, une mise au premier plan, un scrolling, etc. demande à ce que l'affichage d'une fenêtre soir rafraîchi, un message WM_PAINT est envoyé. UpdateWindow()  envoie justement ce message, ici pour dessiner la fenêtre une première fois. Dans SK_WIN , le message est traité à blanc, c’est-à-dire qu'il est prêt à recevoir du code quand des objets graphiques auront été ajoutés à l'application :

;----------------------------------------------------
; WM_PAINT
;----------------------------------------------------
.ELSEIF (eax == WM_PAINT)
    invoke  BeginPaint, hWnd, ADDR pStruct
    mov hdc, eax
    ;Dessiner ici la zone client
    invoke  EndPaint,   hWnd, ADDR pStruct
;----------------------------------------------------

En l'état actuel (squelettique !) du programme, le traitement de WM_PAINT dans WndProc peut sans problème être effacé, mais surtout pas UpdateWindow() . Son emploi est aussi systématique que celui de ShowWindow() .

Voilà, nous voyons la fin du lancement de SK_WIN . Le flux de programme va plonger dans la boucle de messages pour ne plus en sortir, alors que la procédure WndProc a commencé à travailler. Nous n'allons naturellement pas voir l'ensemble du programme de façon détaillée. Il reste plusieurs points très intéressant à étudier, mais l'ordre chronologique n'existe plus. Nous devons voir des parties de code qui se sont déjà déroulées. Le reste peut être lu dans le désordre, bien que le traitement de WM_CREATE soit plus intéressant à chaud, dans la foulée de la création de la fenêtre. Voici les points traités d'ici à la fin du chapitre :

  WM_CREATE  ;

  Une procédure : CentrerFenetre()  ;

  La boucle de messages de WinMain()  ;

  Structure de  WndProc  ;

 

12.4.5 WM_CREATE

Rappelons que ce message est traité en fin de CreateWindowEx() , en finalisation de la création de la fenêtre. La fenêtre a été créée avec icône et menus, mais il reste à installer les objets supplémentaires, en fait des fenêtres fille, comme des boutons (il n'y en a pas pour l'instant), une toolbar, une statusbar et une table de raccourcis clavier.

C'est également ici, puisque la fenêtre est créée mais pas encore visible, que vont être calculées ses coordonnées, à l'aide de la procédure CentrerFenetre() . Elle pourra ainsi apparaître au centre de l'écran, quelle que soit sa résolution.

Nous allons suivre ce code ligne par ligne.

 

;---------------------------------------------------------
; WM_CREATE
;---------------------------------------------------------
.ELSEIF (eax == WM_CREATE)

C’est le test de la boucle de WndProc . Un sous-chapitre est réservé à cette boucle.

    invoke    GetDesktopWindow

Cette fonction renvoie (dans EAX, bien entendu) le handle sur le Bureau (Desktop windows). Le Bureau est une fenêtre Windows qui occupe toujours toute la surface de l’écran.

    invoke    CentrerFenetre, hWnd, eax

Cette fonction est détaillée au sous-chapitre suivant. Elle centre la fenêtre SK_WIN (handle hWnd) par rapport à l’écran (Bureau, handle eax).

    invoke    InitCommonControls

Enregistre et initialise les contrôles Windows de COMCTL32.DLL . C’est une vérification.

    invoke    CreateStatusWindow, WS_CHILD OR WS_BORDER OR WS_VISIBLE,
        ADDR szStatusBar, hWnd, ID_STATUSBAR

Grosse fonction classique à éplucher dans la documentation. Ce qu’elle fait est évident. Remarquez les paramètres de style. Ce sont comme souvent une série de symboles d’attributs dont un seul bit est à 1. Ce qui se reconnaît bien dans les fichiers .h ou . inc en hexadécimal : il n’y a que des 0, 1, 2, 4 et 8. Ainsi, nous pouvons transmettre une liste d’attributs, en les mettant en OU bit à bit. Certains utilisent le +, mais c’est illogique et dangereux : Attr1 OR Attr1 = Attr1 , alors que Attr1 + Attr1 = Attr2 , si par exemple Attr1 = 0010b et Attr2 = 0100b .

La fonction CreateStatusWindow() n’est plus conseillée, au profit de CreateWindows() .

    mov    hStatusBar,eax

Récupère le handle de la barre d’état pour usage dans le programme. Si EAX est nul, c’est un message d’erreur et c’est testé à la ligne suivante.

    .IF (eax)

Continuer seulement si la fonction précédente n’a pas échoué. Sinon le programme s’arrête.

        invoke    CreateToolbarEx, hWnd,
            WS_CHILD OR WS_BORDER OR WS_VISIBLE OR TBSTYLE_TOOLTIPS,
            ID_TOOLBAR, 0Bh, HINST_COMMCTRL, IDB_STD_SMALL_COLOR,
            ADDR tbButtons, 3h, 10h, 10h, 10h, 10h, SIZEOF(TBBUTTON)
        mov    hToolBar,eax
        SautSiFaux eax,@F  

Observez les trois lignes de commentaires précédents, y compris pour CreateWindow(). La macro SautSiFaux (voir SK_INC.inc sur le CD-Rom) n’est pas réellement indispensable. La toolbar est définie essentiellement par tbButtons, dans la section .const , qui décrit textuellement les boutons et leur comportement.

        call    C_IDM_NEW

C_IDM_NEW est l’endroit où doit se traiter la commande Nouveau , c’est-à-dire le menu Fichier / Nouveau , le raccourci  Ctrl  +  N  et le bouton Nouveau de la toolbar. Il ne s'agit dans cette version que d'une simulation, à savoir l'écriture de Document-1 dans la barre de titre. C'est bien entendu un élément facultatif.

        invoke    LoadAccelerators, hInstance_appli, IDA_ACCEL

Installe la table des raccourcis clavier. C'est à partir de là, et de la constante tbButtons , que l'application ne reçoit que des messages de commande, et non des événement menus, des événements toolbar, des frappes clavier.

        mov        hAccel,eax

Comme pour hStatusBar .

        SautSiFaux eax,@F         
    .ELSE
        MetAZero    eax
@@:    dec    eax
        ret
    .ENDIF
;---------------------------------------------------------

12.4.6 Une procédure : CentrerFenetre()

Scope of work, but de la procédure :

  Nous disposons simplement du handle de deux fenêtres Windows, Fille et Parent.

  Nous ne supposons rien sur les tailles et positions initiales des deux fenêtres.

  La position de Fille doit être modifiée pour que Fille soit centrée par rapport à Parent.

  La modification ne doit pas faire sortir Fille de l'écran, même partiellement.

 

Nous avons besoin de :

  Coordonnées des fenêtres : GetWindowRect(handle) , c'est fait pour ça.

  Dimensions de l'écran : GetWindowRect(GetDesktopWindow() ) , c’est-à-dire les coordonnées de l'écran dans l'écran.

  Déplacer la fenêtre fille : SetWindowsPos() , c'est fait pour ça.

 

Les coordonnées écran partent de l’angle en haut à gauche (top-left) et donc croissent en x vers la droite, en y vers le bas.

Nous utilisons la structure RECT , définie en C :

typedef struct _RECT { 
    LONG left; 
    LONG top; 
    LONG right; 
    LONG bottom; 
} RECT;

soit en MASM :

RECT STRUCT
  left   SWORD      ?
  top    SWORD      ?
  right  SWORD      ?
  bottom SWORD      ?
RECT ENDS

 

Un rectangle à l'écran
figure 12.08 Un rectangle à l'écran [the .swf]

 

En géométrie, il existe plusieurs façons de raisonner, et nous avons le choix. Par exemple, raisonner de façon géométrique, visuelle, puis si ça devient confus, poser des x et des y.

Rien ne dit que Fille est plus petite que Parent. Il est même possible qu'elle soit plus petite en x et plus grande en y. Nous voulons seulement que les centres coïncident.

Il est possible, et même bien préférable, de raisonner en x et en y indépendamment. Quand nous saurons faire en x, il suffira de changer left en top, right en bottom. Nous raisonnons donc en x. Sans équation. Pour que Fille et Parent soient centrés, il suffit que la différence de largeur entre les deux soit également répartie à droite et à gauche. Donc, left de Fille est à [moitié de différence de largeur] de distance de left de Parent. Tout est dit. Il est souvent plus facile de raisonner avec Fille plus petite que Parent. Il suffit de faire de cette façon et de vérifier ensuite que tout fonctionne dans le cas inverse.

Ce qui nous intéresse, c'est left de Fille, puisque c'est elle qui sera déplacée. left de Fille est donc égale à left de Parent + la moitié de la différence de largeur Parent - Fille. Et si Fille est plus large que Parent, ne rien changer, puisque la différence est négative.

Il nous faut donc :

Calculer largeur Parent : (rightP - leftP) ;

Calculer largeur Fille : (rightF - leftF) ;

Prendre la différence : (rightP - leftP) - (rightF - leftF) = rightP – leftP – rightF + leftF ;

Prendre la moitié ;

Ajouter à leftP.

Le mieux est de pseudo-coder cela (en partant de l'égalité de la ligne 3) sur un bout de papier et de s'en servir de canevas ensuite. Il est immédiat d'en faire mécaniquement une version en y.

mov eax, rightP
sub eax, leftP
sub eax, rightF
add eax, leftF
shr eax, 1 (c'est la division par 2)
add eax, leftP

Il est possible de remarquer que leftP intervient deux fois. Il est donc possible de simplifier :

mov eax, rightP
add eax, leftP
sub eax, rightF
add eax, leftF
shr eax, 1 (c'est la division par 2)

Cette simplification est parfaitement discutable. En effet, pour 5 octets et une paire de ns (nano !), les deux premières lignes commencent par additionner rightP et leftP, ce qui n'a physiquement aucun sens. Nous conserverons donc la première version. Nous pourrions aller un peu plus loin dans ce sens, en suivant les points 1 à 5 :

mov eax, rightP
sub eax, leftP  ; largeur de P
mov ebx, rightF
sub ebx, leftF  ; largeur de F
sub eax, ebx    ; différence de largeur 
shr eax, 1      ; demi différence de largeur
add eax, leftP  ; nouveau leftF

Il faut mettre au frais ce résultat, le temps de calculer le nouveau topF exactement de la même façon. Mais ce résultat peut être négatif. Si c'est le cas, nous le rendrons nul, c’est-à-dire que nous collerons Fille sur la gauche de l'écran. La dernière instruction ayant positionné les flags, il suffit de :

jns @F
xor eax, eax
@@:

Fille peut encore déborder par la droite (ou par le bas). Nous avons obtenu la largeur de l'écran, par la même fonction donc sous la même forme que les fenêtres : rightE, par exemple. Si leftF + largeurFille est supérieur à rightE, alors leftF est mis à rightE - largeurFille. La largeur de Fille largeurFille est toujours dans EBX :

        jns @F
        xor eax, eax
@@
        add ebx, eax
        cmp ebx, rightE
        jle @F
        mov eax, rightE
        sub eax, rightF
        add eax, leftF
@@:
        mov X, eax

Munis de ces brouillons, il ne reste plus qu'à coder :

;=================================================================
; Procédure CentrerFenetre()
;-----------------------------------------------------------------
; Rôle du bloc :
;               Centrer une fenêtre par rapport à une autre,
;               ces fenêtres sont désignées par leur handle.
;               Il est donc possible de centrer une fenêtre
;               par rapport à l'écran
;-----------------------------------------------------------------
CentrerFenetre  PROC    STDCALL, hFille:HWND, hParent:HWND
 
LOCAL   RectParent:RECT, RectFille:RECT, RectEcran:RECT,
        hEcran:HWND,
        X:DWORD, Y:DWORD
 
invoke  GetDesktopWindow
mov hEcran, eax
invoke  GetWindowRect, hEcran , ADDR RectEcran
invoke  GetWindowRect, hParent, ADDR RectParent
.IF (eax)
    invoke  GetWindowRect, hFille, ADDR RectFille
    .IF (eax)
;-------------------------------------------------
        mov eax, RectParent.right
        sub eax, RectParent.left  ; largeur de Parent
        mov ebx, RectFille.right
        sub ebx, RectFille.left   ; largeur de Fille
        sub eax, ebx              ; différence de largeur
        shr eax, 1                ; demi différence de largeur
        add eax, RectParent.left  ; nouveau left Fille
        jns @F
        xor eax, eax
@@:
        add ebx, eax
        cmp ebx, RectEcran.right
        jle @F
        mov eax, RectEcran.right
        sub eax, RectFille.right
        add eax, RectFille.left
@@:
        mov X, eax
;-------------------------------------------------
        mov eax, RectParent.bottom
        sub eax, RectParent.top   ; hauteur de Parent
        mov ebx, RectFille.bottom
        sub ebx, RectFille.top    ; hauteur de Fille
        sub eax, ebx              ; différence de hauteur
        shr eax, 1                ; demi différence de hauteur
        add eax, RectParent.top   ; nouveau top Fille
        jns @F
        xor eax, eax
@@:
        add ebx, eax
        cmp ebx, RectEcran.bottom
        jle @F
        mov eax, RectEcran.bottom
        sub eax, RectFille.bottom
        add eax, RectFille.top
@@:
        mov Y, eax
 
 
        invoke  SetWindowPos, hFille, NULL, X,
                Y, 0, 0, SWP_NOSIZE + SWP_NOZORDER
    .ENDIF
.ENDIF
ret
CentrerFenetre  ENDP
;-----------------------------------------------------------------

Peu de commentaires supplémentaires : GetDesktopWindow()  renvoie un handle sur le Desktop, dont la fenêtre occupe tout l'écran. Ce qui nous permet de récupérer le RECT de cet écran. Ensuite, c'est au tour des RECT des fenêtres Fille et Parent. Nous fabriquons x et y comme nous l'avons vu. Enfin, l'utilisation de SetWindowPos()  sur fenêtre Fille.

12.4.7 La boucle de messages de WinMain()

Reprenons le listing complet de WinMain()  :

WinMain PROC  STDCALL, hInstance:HINSTANCE,
             hPrevInstance:HINSTANCE,
             pCmdLine:LPSTR,
             CmdShow:DWORD
 
LOCAL msg:MSG
 
IFDEF DEBUG
invoke  MessageBox, NULL, ADDR szMessageWMain, ADDR szMessageWMain, MB_OK
ENDIF
call  InstanceUnique  ;Si une seule instance autorisée
.IF (eax)
  call  Initialise_fenetre
  .IF (eax)
    .WHILE TRUE
      invoke  GetMessage, ADDR msg, NULL, 0, 0
      .BREAK .IF (!eax)
      invoke  TranslateAccelerator, msg.hwnd, hAccel, ADDR msg
      .CONTINUE .IF (eax)
      invoke  TranslateMessage, ADDR msg
      invoke  DispatchMessage,  ADDR msg
    .ENDW
    mov eax, msg.wParam
  .ENDIF
.ENDIF
ret
WinMain ENDP

Pratiquement tout a déjà été dit sur le sujet. Nous avions laissé ce programme dans Initialise_fenetre . Il va en revenir avec un code d’erreur dans EAX. Si ce code, par la valeur FALSE ou 0, indique que l’initialisation n’a pas pu se dérouler correctement, le programme se termine immédiatement. Sinon, nous entrons dans la boucle infinie qu’est la boucle de messages. Rappelons simplement ce qui a déjà été vu :

GetMessage ne retourne que quand un message est disponible. Dans le cas particulier d’un WM_QUIT , il retourne 0. Nous ne traitons pas le cas d’une erreur (retourne alors -1). Les seules sources d’erreurs semblent être un pointeur msg invalide, ce qui paraît ici improbable, ou plus fréquemment un hWnd invalide. Ici, nous utilisons NULL pour récupérer l’ensemble de toutes les fenêtres (au sens composant) et threads de l‘application.

Dans le cas d’un WM_QUIT , le programme stoppe immédiatement. C’est d’ailleurs sa seule porte de sortie. Il récupère l’exitcode dans le wParam de msg et le renvoie dans EAX.

Ensuite, le message renvoyé par GetMessage est envoyé à TranslateAccelerator . Il va être comparé à la table de raccourcis clavier de l’application. En cas de correspondance, le message traduit, un WM_COMMAND ou WM_SYSCOMMAND , est envoyé directement par TranslateAccelerator . Dans ce cas, que TranslateAccelerator signale en renvoyant une valeur non nulle, donc TRUE, la directive .CONTINUE fait remonter en début de boucle, au GetMessage .

Ensuite, TranslateMessage transforme un éventuel message clavier en message caractère. C’est-à-dire en gros un WM_KEY?? en WM_CHAR?? , dans le même ordre d’idées qu’une conversion scancode vers ASCII.

Le message est ensuite renvoyé à Windows, c’est-à-dire à notre WndProc en final.

Et ainsi de suite, GetMessage

12.4.8 Structure de WndProc

Un message est constitué d’un code, ou numéro, de message uMsg et de deux paramètres, wPARAM et lPARAM .

Dans la boucle de messages de WinMain() , les messages sont des structures. Mais WinMain() manipule un pointeur. WndProc reçoit des valeurs. Et envoyer une structure par valeur ou envoyer ses membres ne fait pas de différence.

La signification de wPARAM et lPARAM dépend bien entendu du message, donc de uMsg . Il est par exemple clair qu’un message clavier contiendra dans un des paramètres le code de la touche.

Dans certains cas particuliers, un des paramètres se comportera lui-même comme un code de message. C’est le cas de WM_COMMAND . Ce message concerne les commandes d’un programme, qu’elles proviennent d’un menu, du clavier, d’une toolbar. Par exemple, nous avons vu que, si TranslateAccelerator reçoit un raccourci clavier valide, il va envoyer un WM_COMMAND et tuer le message clavier. Dans le cas donc du WM_COMMAND , le paramètre wPARAM contient le code de la commande. Nous avons ainsi à faire une analyse à deux niveaux du message. Voici un extrait fortement dégraissé de WndProc  :

;=================================================================
; Procédure WndProc()
;-----------------------------------------------------------------
; Rôle du bloc : 
;       Boucle d'analyse et de traitement des messages
;
;-----------------------------------------------------------------
WndProc PROC    STDCALL, hWnd:HWND, uMsg:UINT, wParam:WPARAM, lParam:LPARAM
mov     eax, uMsg
;-----------------------------------------------------------------
; WM_MENUSELECT
;-----------------------------------------------------------------
 
.ELSEIF (eax == WM_DESTROY)
;
;-----------------------------------------------------------------
; WM_NOTIFY
;-----------------------------------------------------------------
.ELSEIF (eax == WM_NOTIFY)
;
;-----------------------------------------------------------------
; WM_COMMAND
;-----------------------------------------------------------------
.ELSEIF (eax == WM_COMMAND)
        mov     eax,wParam
        and     eax,0FFFFh
;========IDM_NEW
        .IF (eax == IDM_NEW)
;
;========IDM_EXIT
        .ELSEIF (eax == IDM_EXIT)
;
;========IDM_ABOUT
        .ELSEIF (eax == IDM_ABOUT)
;
;========IDM_HELPTOPICS
        .ELSEIF (eax == IDM_HELPTOPICS)
;
                
        .ELSE ;Autre commande
                jmp     DefProc
        .ENDIF
;-----------------------------------------------------------------
.ELSE ; WM_???? (MEssage non traité)
;-----------------------------------------------------------------
 
DefProc:        
invoke  DefWindowProc, hWnd, uMsg, wParam, lParam
ret
.ENDIF
RetourneNul:
MetAZero eax
ret

Pour l’écriture d’une boucle de messages, il faut certainement être rigoureux plus que performant. L’usage des macros est fortement conseillé. L’idéal est une structure à base de . ELSEIF , sans aucune imagination. Il est dommage que nous ne disposions pas de directives de type CASE en assembleur.

De plus, s’il est fait usage de fichiers de symboles  .inc , ce n’est pas pour traiter les valeurs numériques directement.

Vous pouvez tenter une macro comme :

MINMAXLIST  MACRO param1, param2, args
   local p1, p2
   p1 = 0
   FOR p2, <args>
      IF p2 GT p1
         p1 = p2
      ENDIF
      ENDM
   param1 EQU p1
 
   p1 = MAXDWORD
   FOR p2, <args>
      IF p2 LT p1
         p1 = p2
      ENDIF
      ENDM
   param2 EQU p1  
   
   ENDM

Et l’appeler (dans le fichier include également) :

MINMAXLIST  WM_MAXI, WM_MINI, <\
            WM_PAINT,   WM_SIZE, WM_CREATE,\
            WM_CLOSE,   WM_DESTROY, WM_NOTIFY,\
            WM_COMMAND, WM_MENUSELECT  >

Si vous faites bien attention à modifier la liste des messages à chaque fois que vous en ajoutez ou retirez un de WndProc , alors vous aurez deux constantes symboliques qui vous permettront de placer en début de boucle :

.IF ((eax > WM_MAXI)||(eax < WM_MINI))
        jmp     DefProc
.ENDIF

Mais il n’est même pas certain qu’il y ait beaucoup à y gagner.

Envoyer un message à l'auteur

se connecter pour pouvoir écrire

Ce site est créé et maintenu (laborieusement) par Pierre Maurette
hébergé par 1 & 1