Discussion de l'article "Guide d'écriture d'une DLL pour MQL5 en Delphi"

 

Un nouvel article Guide d'écriture d'une DLL pour MQL5 en Delphi a été publié :

L'article examine le mécanisme de création d'un module DLL, en utilisant le langage de programmation populaire d' ObjectPascal, dans un environnement de programmation Delphi. La documentation, fournie dans cet article, est conçue pour cibler principalement les programmeurs débutants, qui travaillent avec des problèmes, qui dépassent les limites du langage de programmation intégré de MQL5, en connectant les modules DLL externes.

Le résultat du travail de l'indicateur est la création d'un canal de régression bleu, tel que indiqué en figure 6. Pour vérifier l'exactitude de la construction du canal, le graphique montre un « canal de régression », issu de l'arsenal d'instruments d'analyse technique de MetaTrader 5, marqué en rouge.

Comme on peut le voir sur la figure, les lignes centrales du canal se confondent. Pendant ce temps, il y a une légère différence dans la largeur du canal (quelques points), qui sont dues aux différentes approches dans son calcul. 

Figure 6. Comparaison des canaux de régression

Auteur : Andrey Voytenko

 
J'attendais un article comme celui-ci depuis longtemps. Merci à l'auteur.
 
DC2008:
J'attendais un article comme celui-ci depuis longtemps. Merci à l'auteur.

Merci à l'auteur. Vous n'êtes pas le premier à me remercier. Je serai heureux d'écouter tous les souhaits et commentaires critiques sur le contenu de l'article.

A l'avenir, j'aimerais développer le sujet de la programmation en Delphi pour MT5, en ajoutant de nouvelles informations sur le site.

 
Vous devriez ajouter au moins un paragraphe sur le débogage. L'article mentionne une situation dans laquelle AV peut se produire, mais même en laissant de côté la multitude d'autres sources potentielles d'erreurs, essayer de rechercher manuellement (à l'œil ou mentalement) l'emplacement de l'erreur peut prendre beaucoup de temps et s'avérer infructueux.
 

Je pense qu'il s'agit d'un article utile pour de nombreuses personnes. Quelques commentaires :

1. les unités SysUtils et Classes auraient dû être laissées dans le projet. Malgré le fait que leur présence "gonfle" quelque peu le projet, elles ont de nombreuses fonctions petites mais importantes. Par exemple, la présence de SysUtils ajoute automatiquement le traitement des exceptions au projet. Comme vous le savez, si une excepcion n'est pas traitée dans la dll, elle est transmise à mt5, où elle provoque l'arrêt de l'exécution du programme mql5.

2. Vous ne devez pas utiliser toutes sortes de procédures dans DllEntryPoint (alias DllMain). Comme Microsoft l'indique dans ses documents, cela entraîne divers effets désagréables. Voici une petite liste d'articles à ce sujet :

Best Practices for Creating DLLs by Microsoft - http://www.microsoft.com/whdc/driver/kernel/DLL_bestprac.mspx.

DllMain et la vie avant l'accouchement - http://transl-gunsmoker.blogspot.com/2009/01/dllmain.html.

DllMain - une histoire à dormir debout - http://transl-gunsmoker.blogspot.com/2009/01/dllmain_04.html

Quelques raisons de ne rien faire d'effrayant dans votre DllMain - http://transl-gunsmoker.blogspot.com/2009/01/dllmain_05.html

D'autres raisons de ne rien faire d'effrayant dans votre DllMain : verrouillage accidentel -

http://transl-gunsmoker.blogspot.com/2009/01/dllmain_7983.html

 

J'ai déjà donné un extrait d'un article inachevé quelque part, je crois sur le forum quad. Je vais le répéter ici.

début...fin

Lors de la création d'un projet Delphi destiné à la compilation de DLL, la section begin...end apparaît dans le fichier de projet .DPR. Cette section est toujours exécutée lorsque la DLL est projetée pour la première fois dans l'espace d'adressage du processus. En d'autres termes, elle peut être considérée comme une sorte de section d'initialisation que toutes les unités possèdent. À cet endroit, vous pouvez effectuer certaines actions qui doivent être réalisées au tout début et une seule fois, pour le processus en cours. Lors du chargement de la DLL dans l'espace d'adressage d'un autre processus, cette section sera exécutée à nouveau. Mais comme les espaces d'adressage des processus sont séparés les uns des autres, l'initialisation dans un processus n'affectera en rien l'autre processus.

Cette section présente certaines limitations que vous devez connaître et prendre en compte. Ces limitations sont liées aux subtilités du mécanisme de chargement des DLL de Windows. Nous les aborderons plus en détail ultérieurement.

 

initialisation/finalisation

Chaque unité Delphi comporte des sections spéciales, appelées sections d'initialisation et de finalisation. Dès qu'une unité est connectée au projet, ces sections sont connectées au mécanisme spécial de chargement et de déchargement du module principal. Ces sections sont exécutées avant que la section principale begin...end ne commence son travail et après que le travail soit terminé. Ceci est très pratique, car il n'est pas nécessaire d'écrire l'initialisation et la finalisation dans le programme lui-même. En même temps, la connexion et la déconnexion sont effectuées automatiquement, il suffit de connecter ou de déconnecter l'unité au projet. Et cela se produit non seulement dans les fichiers EXE traditionnels, mais aussi dans les DLL. L'ordre d'initialisation de la DLL, lors de son "chargement" dans la mémoire, est le suivant : toutes les sections d'initialisation de l'unité sont d'abord exécutées, dans l'ordre où elles sont marquées dans les utilisations du projet, puis la section début...fin est exécutée. La finalisation se fait dans l'ordre inverse, sauf qu'il n'y a pas de fonction de terminaison spécialement conçue dans le fichier de projet DLL. C'est, en général, une autre raison pour laquelle il est recommandé de séparer les projets DLL en un fichier de projet et une unité d'utilisation.

 

DllMain

Il s'agit du point d'entrée de la DLL. En effet, Windows a parfois besoin de signaler à la DLL elle-même tout événement survenant dans le processus. Pour ce faire, il existe un point d'entrée. Il s'agit d'une fonction spécialement prédéfinie que chaque DLL possède et qui peut traiter les messages. Et bien que nous n'ayons pas encore vu cette fonction dans une DLL écrite en Delphi, elle dispose néanmoins d'un tel point. Le mécanisme de son fonctionnement est voilé, mais il est toujours possible d'y accéder. La réponse à la question - est-ce vraiment nécessaire ? - n'est pas aussi évidente qu'il n'y paraît.

Essayons d'abord de comprendre ce que Windows essaie de dire à la DLL. Il existe au total 4 messages que le système d'exploitation envoie à la DLL. Le premier, la notification DLL_PROCESS_ATTACH, est envoyé lorsque le système attache une DLL à l'espace d'adressage du processus appelant. Dans le cas de MQL4, il s'agit d'un chargement implicite. Peu importe que cette DLL ait déjà été chargée dans l'espace d'adressage d'un autre processus, le message sera quand même envoyé. Et peu importe que Windows ne charge qu'une seule fois une DLL particulière dans la mémoire, tous les processus souhaitant charger cette DLL dans leur espace d'adressage ne reçoivent qu'un reflet de cette DLL. Il s'agit du même code, mais les données que la DLL peut contenir sont propres à chaque processus (bien qu'il soit possible que des données communes existent). Le second, la notification DLL_PROCESS_DETACH, indique à la DLL de se détacher de l'espace d'adressage du processus appelant. En fait, ce message est reçu avant que Windows ne commence à décharger la DLL. En fait, si la DLL est utilisée par d'autres processus, il n'y a pas de déchargement, Windows "oublie" simplement que la DLL existait dans l'espace d'adressage du processus. Deux autres notifications, DLL_THREAD_ATTACH etDLL_THREAD_DETACH sont reçues lorsque le processus qui a chargé la DLL crée ou détruit des threads au sein du processus. Il existe quelques problèmes subtils liés à l'ordre dans lequel les notifications de threads sont reçues, mais nous ne les examinerons pas.

Voyons maintenant comment les DLL écrites en Delphi sont organisées et ce qui est généralement caché aux programmeurs. Une fois que Windows a "projeté la DLL dans l'espace d'adressage du processus appelant", ou plus simplement chargé la DLL en mémoire, il y a un appel à la fonction située au point d'entrée et la transmission de la notification DLL_PROCESS_ATTACH à cette fonction. Dans une DLL écrite en Delphi, ce point d'entrée contient un code spécial qui fait beaucoup de choses différentes, y compris le démarrage de l'initialisation des unités. Il se souvient que l'initialisation et la première exécution de la DLL ont été effectuées et exécute begin...end du fichier de projet principal. Ainsi, ce code de chargement initial n'est exécuté qu'une seule fois, tous les autres appels de Windows au point d'entrée sont faits à une autre fonction, qui gère les notifications ultérieures - en fait, elle les ignore, à l'exception du message DLL_PROCESS_DETACH, qui finalise l'unité. Voici comment se présente, en termes généraux, le mécanisme de chargement d'une DLL écrite en Delphi. Dans la plupart des cas, il suffit d'écrire et d'utiliser des DLL en MQL4.

Si vous avez toujours besoin d'une DllMain exactement comme en C, il n'est pas difficile de l'organiser. Cela se fait de la manière suivante. Lors du premier chargement d'une DLL, entre autres, le module System (il est toujours présent dans un programme ou une DLL) crée automatiquement une variable procédurale globale DllProc, qui est initialisée avec nil. Cela signifie qu'aucun traitement supplémentaire des notifications DllMain, autre que celui qui existe, n'est nécessaire. Dès que l'adresse de la fonction est assignée à cette variable, toutes les notifications pour les DLL de Windows iront à cette fonction. C'est ce qui est demandé au point d'entrée. Toutefois, la notification DLL_PROCESS_DETACH sera toujours suivie par la fonction de terminaison de la DLL, comme auparavant, afin de permettre la finalisation.

procedureDllEntryPoint(Reason : DWORD) ;

begin

case Reason of

DLL_PROCESS_ATTACH : ;// " Processus de connexion processus'

DLL_THREAD_ATTACH : ;//'Connexion du thread thread'

DLL_THREAD_DETACH : ;//'Déconnexion d'un thread'. stream'

DLL_PROCESS_DETACH : ;//'Déconnexion du processus '. processus'

end;

end;

begin

if not Assigned(DllProc) then begin

DllProc :=@DllEntryPoint;

DllEntryPoint (DLL_PROCESS_ATTACH) ;

fin;

fin.

Si nous ne sommes pas intéressés par les notifications de threads, tout ceci n'est pas nécessaire. Il suffit d'organiser les sections d'initialisation et de finalisation en unités, car les événements de connexion et de déconnexion du processus seront suivis automatiquement.

 

La perfidie et la trahison de DllMain

Il est peut-être temps maintenant d'aborder un sujet qui est étonnamment peu traité dans la littérature de programmation. Ce sujet ne concerne pas seulement Delphi ou C, mais tout langage de programmation capable de créer des DLL. Il s'agit d'une propriété du chargeur de DLL de Windows. Dans la littérature traduite, sérieuse et répandue, sur la programmation dans l'environnement Windows, un seul auteur a réussi à trouver une mention à ce sujet, et ce dans les termes les plus vagues. Il s'agit de J. Richter, et on lui pardonne, car son merveilleux livre a été publié en 2001, à une époque où Windows 32 bits n'était pas encore très répandu.

Il est intéressant de noter que MS elle-même n'a jamais caché l'existence du problème avec DllMain et a même publié un document spécial, quelque chose comme "La meilleure façon d'utiliser DllMain". Il y explique ce qui peut être fait dans DllMain et ce qui n'est pas recommandé. Et il a souligné que les choses non recommandées conduisent à des erreurs difficiles à voir et incohérentes. Ceux qui souhaitent lire ce document peuvent le consulter ici. Un résumé plus populaire de plusieurs traductions de plusieurs rapports alarmistes sur le sujet est présenté ici.

L'essence du problème est très simple. Le fait est que DllMain, en particulier lors du chargement d'une DLL, est un endroit spécial. Un endroit où il ne faut rien faire de compliqué et d'exceptionnel. Par exemple, il n'est pas recommandé de créer un processus (CreateProcess) ou de charger une bibliothèque (LoadLibrary ) d'autres DLL. Il n'est pas non plus recommandé de créer un thread ou de co-initialiser un COM. Et ainsi de suite.

Vous pouvez faire les choses les plus simples. Sinon, rien n'est garanti. Par conséquent, ne mettez rien d'inutile dans DllMain, sinon vous serez surpris de voir les applications utilisant votre DLL se planter. Il vaut mieux être sûr et créer des fonctions exportées spéciales d' initialisation et de finalisation, qui seront appelées par l'application principale au bon moment. Cela permettra au moins d'éviter les problèmes avec DllMain.
 

ExitProc, ExitCode,MainInstance,HInstance....

Le module System, qui est toujours intégré à votre DLL au moment de la compilation, possède quelques variables globales utiles que vous pouvez utiliser.

ExitCode, - une variable dans laquelle vous pouvez mettre un nombre différent de 0 au chargement, ce qui a pour effet d'arrêter le chargement de la DLL.

ExitProc, - une variable procédurale qui peut stocker l'adresse de la fonction qui sera exécutée à la sortie. Cette variable est une relique d'un passé lointain, elle ne fonctionne pas dans les DLL et, de plus, les développeurs de Delphi ne recommandent pas de l'utiliser dans les DLL en raison de problèmes éventuels.

HInstance, - variable dans laquelle est stocké, après chargement, le descripteur de la DLL elle-même. Elle peut être très utile.

MainInstance, - descripteur de l'application qui a chargé la DLL dans son espace d'adressage.

IsMultiThread, - une variable qui prend automatiquement la valeur True si la compilation de la DLL détecte un travail avec des threads. En fonction de la valeur de cette variable, le gestionnaire de mémoire de la DLL passe en mode multithread. En principe, il est possible de forcer le gestionnaire de mémoire à passer en mode multithread, même si les threads ne sont pas explicitement utilisés dans la DLL. IsMultiThread:=True ; Naturellement, le mode multithread est plus lent que le mode single-thread en raison du fait que les threads sont synchronisés les uns avec les autres.

MainThreadID, - descripteur du thread principal de l'application.

Et ainsi de suite. En général, le module System exécute approximativement les mêmes fonctions que CRT en C. Y compris les fonctions de gestion de la mémoire. Une liste de toutes les fonctions et variables présentes dans la DLL compilée, non seulement celles qui sont exportées, mais toutes, peut être obtenue si vous activez l'option Linker, Map file - Detailed, dans les paramètres du projet.
 

Gestion de la mémoire

La gestion de la mémoire dans les DLL est un autre problème assez sérieux qui pose souvent des difficultés. Plus précisément, la gestion de la mémoire elle-même ne pose aucun problème, mais dès que la DLL essaie de travailler activement avec la mémoire allouée par le gestionnaire de mémoire de l'application elle-même, c'est là que les problèmes commencent généralement.

En effet, les applications sont généralement compilées avec un MemoryManager intégré. La DLL compilée contient également son propre MemoryManager. Cela est particulièrement vrai pour les applications et les DLL créées dans des environnements de programmation différents. Dans notre cas, le terminal est en MSVC, la DLL est en Delphi. Il est clair qu'il s'agit de gestionnaires différents de par leur structure, mais en même temps, il s'agit de gestionnaires physiquement différents, chacun gérant sa propre mémoire dans l'espace d'adressage commun du processus. En principe, ils n'interfèrent pas l'un avec l'autre, ne prennent pas la mémoire de l'autre, existent en parallèle l'un avec l'autre et ne "savent" généralement rien de l'existence de concurrents. Cela est possible parce que les deux gestionnaires accèdent à la mémoire à partir de la même source, le gestionnaire de mémoire de Windows.

Les problèmes commencent lorsqu'une fonction DLL et une application tentent de gérer des sections de mémoire distribuées par un gestionnaire de mémoire différent. C'est pourquoi il existe une règle empirique parmi les programmeurs qui dit que "la mémoire ne doit pas franchir les limites d'un module de code".

C'est une bonne règle, mais elle n'est pas tout à fait correcte. Il serait plus correct d'utiliser le même gestionnaire de mémoire dans la DLL utilisée par l'application. En fait, j'aime bien l'idée de connecter le gestionnaire de mémoire de MT4au gestionnaire de mémoire de Delphi FastMM, mais ce n'est pas une idée très réalisable. Quoi qu'il en soit, la gestion de la mémoire devrait être une chose.

Dans Delphi , il est possible de remplacer le gestionnaire de mémoire par défaut par n'importe quel autre gestionnaire de mémoire répondant à certaines exigences. Il est donc possible de faire en sorte que la DLL et l'application n'aient qu'un seul gestionnaire de mémoire, et ce sera le gestionnaire de mémoire MT4.