Era da tempo che aspettavo un articolo come questo. Grazie all'autore.
Grazie a te. Non sei il primo a ringraziarmi. Sarò lieto di ascoltare tutti i desideri e i commenti critici sul materiale dell'articolo.
In futuro vorrei sviluppare l'argomento della programmazione in Delphi per MT5, aggiungendo nuove informazioni al sito.
Penso che sia un articolo utile per molte persone. Un paio di commenti:
1. Le unità SysUtils e Classes avrebbero dovuto essere lasciate nel progetto. Nonostante la loro presenza "gonfi" un po' il progetto, hanno molte funzioni piccole ma importanti. Per esempio, la presenza di SysUtils aggiunge automaticamente al progetto l'elaborazione delle eccezioni. Come sapete, se un'eccezione non viene elaborata nella dll, viene passata a mt5, dove provoca l'interruzione dell'esecuzione del programma mql5.
2. Non si devono utilizzare tutti i tipi di procedure all'interno di DllEntryPoint (alias DllMain). Come afferma Microsoft nei suoi documenti, questa operazione è soggetta a vari effetti spiacevoli. Ecco un piccolo elenco di articoli sull'argomento:
Best Practices for Creating DLLs by Microsoft - http://www.microsoft.com/whdc/driver/kernel/DLL_bestprac.mspx.
DllMain e la vita prima del parto - http://transl-gunsmoker.blogspot.com/2009/01/dllmain.html.
DllMain - una storia della buonanotte - http://transl-gunsmoker.blogspot.com/2009/01/dllmain_04.html
Alcune ragioni per non fare nulla di spaventoso nel vostro DllMain - http://transl-gunsmoker.blogspot.com/2009/01/dllmain_05.html
Altri motivi per non fare nulla di spaventoso nel vostro DllMain: il blocco accidentale -
http://transl-gunsmoker.blogspot.com/2009/01/dllmain_7983.htmlHo già dato un estratto di un articolo non finito da qualche parte, credo sul forum di quad. Lo ripeterò qui.
inizio...fine
Quando si crea un progetto Delphi destinato alla compilazione di DLL, nel file di progetto .DPR compare la sezione begin...end. Questa sezione viene sempre eseguita quando la DLL viene proiettata per la prima volta nello spazio degli indirizzi del processo. In altre parole, può essere considerata una sorta di sezione di inizializzazione che tutte le unità hanno. In questo punto è possibile eseguire alcune azioni che devono essere eseguite all'inizio e solo una volta, per il processo corrente. Quando si carica la DLL nello spazio degli indirizzi di un altro processo, questa sezione verrà eseguita nuovamente. Tuttavia, poiché gli spazi degli indirizzi dei processi sono separati l'uno dall'altro, l'inizializzazione in un processo non influenzerà in alcun modo l'altro processo.
Questa sezione presenta alcune limitazioni di cui è bene tenere conto. Queste limitazioni sono legate alle sottigliezze del meccanismo di caricamento delle DLL di Windows. Ne parleremo più dettagliatamente in seguito.
inizializzazione/finalizzazione
Ogni unità Delphi ha sezioni speciali, le cosiddette sezioni di inizializzazione e finalizzazione. Non appena una qualsiasi unità viene collegata al progetto, queste sezioni vengono collegate allo speciale meccanismo di carico e scarico del modulo principale. Queste sezioni vengono eseguite prima che la sezione principale begin...end inizi il suo lavoro e dopo che il lavoro è terminato. Questo è molto comodo, perché elimina la necessità di scrivere l'inizializzazione e la finalizzazione nel programma stesso. Allo stesso tempo, la connessione e la disconnessione avvengono automaticamente: è sufficiente collegare o scollegare l'unità al progetto. Questo avviene non solo nei tradizionali file EXE, ma anche nelle DLL. L'ordine di inizializzazione della DLL, quando viene "caricata" in memoria, è il seguente: prima vengono eseguite tutte le sezioni di inizializzazione dell'unità, nell'ordine in cui sono segnate negli usi del progetto, poi viene eseguita la sezione begin...end. La finalizzazione avviene nell'ordine inverso, tranne per il fatto che non esiste una funzione di terminazione appositamente progettata nel file di progetto della DLL. Questo è, in generale, un altro motivo per cui si raccomanda di separare i progetti DLL in un file di progetto e in un'unità di utilizzo.
DllMain
È il cosiddetto punto di ingresso della DLL. Il punto è che Windows deve occasionalmente segnalare qualsiasi evento che si verifica all'interno del processo alla DLL stessa. Per fare ciò, esiste un punto di ingresso. Cioè una funzione appositamente predefinita che ogni DLL possiede e che può gestire i messaggi. Anche se non abbiamo ancora visto questa funzione in una DLL scritta in Delphi, tuttavia essa ha un punto di ingresso. Il meccanismo del suo funzionamento è velato, ma è sempre possibile raggiungerlo. La risposta alla domanda - è proprio necessaria? - non è così ovvia come sembra.
Innanzitutto cerchiamo di capire che cosa Windows sta cercando di dire alla DLL. Ci sono in totale 4 messaggi con cui il sistema operativo si rivolge alla DLL. Il primo, la notifica DLL_PROCESS_ATTACH, viene inviato ogni volta che il sistema aggancia una DLL allo spazio degli indirizzi del processo chiamante. Nel caso di MQL4si tratta di un caricamento implicito. Non importa se questa DLL è già stata caricata nello spazio degli indirizzi di un altro processo, il messaggio verrà comunque inviato. E non importa che Windows carichi una particolare DLL in memoria solo una volta, tutti i processi che desiderano caricare questa DLL nel loro spazio di indirizzamento ricevono solo un riflesso di questa DLL. Si tratta dello stesso codice, ma i dati che la DLL può avere sono unici per ogni processo (anche se è possibile che esistano dati comuni). La seconda, la notifica DLL_PROCESS_DETACH, indica alla DLL di staccarsi dallo spazio degli indirizzi del processo chiamante. In realtà, questo messaggio viene ricevuto prima che Windows inizi a scaricare la DLL. Infatti, se la DLL è utilizzata da altri processi, non avviene alcuno scarico, Windows semplicemente "dimentica" che la DLL esisteva nello spazio degli indirizzi del processo. Altre due notifiche, DLL_THREAD_ATTACH eDLL_THREAD_DETACH, vengono ricevute quando il processo che ha caricato la DLL crea o distrugge thread all'interno del processo. L'ordine di ricezione delle notifiche di thread presenta alcuni problemi sottili, che non verranno presi in considerazione.
Ora parliamo di come sono organizzate le DLL scritte in Delphi e di ciò che di solito è nascosto ai programmatori. Dopo che Windows ha "proiettato la DLL nello spazio degli indirizzi del processo chiamante", o in parole povere, ha caricato la DLL in memoria, a questo punto si chiama la funzione che si trova nel punto di ingresso e si passa la notifica DLL_PROCESS_ATTACH a questa funzione. In una DLL scritta in Delphi, questo punto di ingresso contiene codice speciale che fa molte cose diverse, tra cui l'avvio dell'inizializzazione delle unità. Ricorda che l'inizializzazione e la prima esecuzione della DLL sono state effettuate ed esegue begin...end del file di progetto principale. In questo modo, questo codice di caricamento iniziale viene eseguito una sola volta, mentre tutte le altre chiamate di Windows al punto di ingresso vengono effettuate a un'altra funzione, che gestisce le notifiche successive - di fatto, le ignora, tranne il messaggio DLL_PROCESS_DETACH, che finalizza l'unità. Questo è il meccanismo di caricamento di una DLL scritta in Delphi in termini generali. Nella maggior parte dei casi, è sufficiente scrivere e utilizzare le DLL in MQL4.
Se si ha ancora bisogno di una DllMain esattamente come in C, non è difficile organizzarla. Si procede come segue. Quando si carica una DLL per la prima volta, tra le altre cose, il modulo System (è sempre presente in un programma o in una DLL) crea automaticamente una variabile procedurale globale DllProc, che viene inizializzata con nil. Ciò significa che non è necessaria alcuna elaborazione aggiuntiva delle notifiche di DllMain, oltre a quella esistente. Non appena l'indirizzo della funzione viene assegnato a questa variabile, tutte le notifiche per le DLL di Windows andranno a questa funzione. Questo è ciò che viene richiesto al punto di ingresso. Tuttavia, la notifica DLL_PROCESS_DETACH sarà ancora monitorata dalla funzione di terminazione della DLL, come in precedenza, per consentire la finalizzazione.
proceduraDllEntryPoint(Reason: DWORD);
iniziare
caso Motivo di
DLL_PROCESS_ATTACH : ;//'Processo di connessione'. al processo".
DLL_THREAD_ATTACH : ;//'Connessione del thread'. thread'
DLL_THREAD_DETACH : ;//'Disconnessione di un thread'. flusso'
DLL_PROCESS_DETACH : ;//'Disconnessione del processo'. processo'
fine;
fine;
iniziare
if not Assigned(DllProc) then begin
DllProc :=@DllEntryPoint;
DllEntryPoint (DLL_PROCESS_ATTACH);
fine;
fine.
Se non siamo interessati alle notifiche dei thread, tutto questo non è necessario. È necessario solo organizzare le sezioni di inizializzazione/finalizzazione in unità, poiché gli eventi di connessione e disconnessione del processo saranno tracciati automaticamente.
La perfidia e il tradimento di DllMain
È forse giunto il momento di affrontare un argomento sorprendentemente poco trattato nella letteratura sulla programmazione. Questo argomento non riguarda solo Delphi o C, ma qualsiasi linguaggio di programmazione in grado di creare DLL. Si tratta di una proprietà del caricatore di DLL di Windows. Dalla letteratura seria e diffusa tradotta sulla programmazione in ambiente Windows, solo un autore è riuscito a trovare un accenno a questo argomento e nei termini più vaghi. Questo autore è J. Richter, ed è perdonato perché il suo splendido libro è stato pubblicato nel 2001, quando in generale Windows a 32 bit non era così diffuso.
È interessante notare che MS stessa non ha mai nascosto l'esistenza del problema con DllMain e ha persino pubblicato un documento speciale, qualcosa come "Il modo migliore per usare DllMain". In cui spiegava cosa si può fare in DllMain e cosa è sconsigliato. È stato sottolineato che le cose non raccomandate portano a errori difficili da vedere e incoerenti. Chi desidera leggere questo documento può consultare questo sito. Un riassunto più popolare di diverse traduzioni di vari rapporti allarmistici sull'argomento è descritto qui.
L'essenza del problema è molto semplice. Il punto è che DllMain, soprattutto quando si carica una DLL, è un luogo speciale. Un luogo in cui non si dovrebbe fare nulla di complicato ed eccezionale. Ad esempio, non è consigliabile CreateProcess o LoadLibrary di altre DLL. Non è nemmeno consigliabile creare un thread o co-inizializzare un COM. E così via.
È possibile fare le cose più semplici. Altrimenti, nulla è garantito. Pertanto, non inserite nulla di inutile in DllMain, altrimenti vi sorprenderete a vedere le applicazioni che utilizzano la vostra DLL bloccarsi. È meglio essere sicuri e creare funzioni speciali esportate di inizializzazione e finalizzazione, che verranno chiamate dall'applicazione principale al momento giusto. Questo aiuterà almeno a evitare problemi con DllMain.
ExitProc, ExitCode,MainInstance,HInstance....
Il modulo System, che viene sempre agganciato alla DLL in fase di compilazione, ha alcune utili variabili globali che possono essere utilizzate.
ExitCode, - una variabile in cui è possibile inserire un numero diverso da 0 al momento del caricamento; di conseguenza, il caricamento della DLL si interromperà.
ExitProc, - una variabile procedurale che può memorizzare l'indirizzo della funzione che verrà eseguita all'uscita. Questa variabile è una reliquia del lontano passato, non funziona nelle DLL e, inoltre, gli sviluppatori di Delphi ne sconsigliano l'uso nelle DLL a causa di possibili problemi.
HInstance, - una variabile in cui dopo il caricamento viene memorizzato il descrittore della DLL stessa. Può essere molto utile.
MainInstance, - descrittore dell'applicazione che ha caricato la DLL nel suo spazio degli indirizzi.
IsMultiThread, - variabile impostata automaticamente su True, se la compilazione della DLL rileva la presenza di thread. In base al valore di questa variabile, il gestore della memoria della DLL passa alla modalità multithread. In linea di principio, è possibile forzare il gestore della memoria a passare alla modalità multithread anche se i thread non sono esplicitamente utilizzati nella DLL. IsMultiThread:=True; Naturalmente, la modalità multithread è più lenta della modalità single-thread, perché i thread sono sincronizzati tra loro.
MainThreadID, - descrittore del thread principale dell'applicazione.
E così via. In generale, il modulo System svolge approssimativamente le stesse funzioni di CRT in C. Comprese le funzioni di gestione della memoria. Un elenco di tutte le funzioni e le variabili presenti nella DLL compilata, non solo di quelle esportate, ma di tutte, si può ottenere se si attiva l'opzione Linker, Map file - Detailed, nelle impostazioni del progetto.
Gestione della memoria
Il prossimo problema, piuttosto serio, che spesso causa difficoltà è la gestione della memoria nelle DLL. Più precisamente, la gestione della memoria in sé non causa alcun problema, ma non appena la DLL cerca di lavorare attivamente con la memoria allocata dal gestore della memoria dell'applicazione stessa, è qui che di solito iniziano i problemi.
Il fatto è che di solito le applicazioni vengono compilate con MemoryManager incorporato. Anche la DLL compilata contiene il proprio MemoryManager. Questo è particolarmente vero per le applicazioni e le DLL create in ambienti di programmazione diversi. Come nel nostro caso, il terminale è in MSVC, la DLL è in Delphi. È chiaro che si tratta di gestori diversi per la loro struttura, ma allo stesso tempo sono anche gestori fisicamente diversi, ognuno dei quali gestisce la propria memoria all'interno dello spazio di indirizzi comune del processo. In linea di principio, non interferiscono l'uno con l'altro, non si sottraggono memoria a vicenda, esistono in parallelo l'uno con l'altro e di solito non "sanno" nulla dell'esistenza di concorrenti. Questo è possibile perché entrambi i gestori accedono alla memoria dalla stessa fonte, il gestore della memoria di Windows.
I problemi iniziano quando una funzione DLL e un'applicazione cercano di gestire sezioni di memoria distribuite da un gestore di memoria diverso. Per questo motivo, esiste una regola empirica tra i programmatori che dice che "la memoria non dovrebbe attraversare i confini di un modulo di codice".
È una buona regola, ma non è del tutto corretta. Sarebbe più corretto utilizzare lo stesso MemoryManager nella DLL utilizzata dall'applicazione. In realtà, mi piace l'idea di collegare il gestore della memoria di MT4al gestore della memoria di Delphi FastMM, ma non è un'idea molto fattibile. Comunque, la gestione della memoria dovrebbe essere una cosa.
In Delphi è possibile sostituire il gestore di memoria predefinito con qualsiasi altro gestore di memoria che soddisfi alcuni requisiti. Quindi è possibile fare in modo che la DLL e l'applicazione abbiano un unico gestore di memoria, che sarà il gestore di memoria di MT4.
- App di trading gratuite
- Oltre 8.000 segnali per il copy trading
- Notizie economiche per esplorare i mercati finanziari
Accetti la politica del sito e le condizioni d’uso
Il nuovo articolo Guida alla scrittura di una DLL per MQL5 in Delphi è stato pubblicato:
L'articolo descrive il meccanismo per creare un modulo DLL, utilizzando il popolare linguaggio di programmazione ObjectPascal, all'interno di un ambiente di programmazione Delphi. I materiali, forniti in questo articolo, sono progettati principalmente per i programmatori principianti che stanno affrontando dei problemi, che violano i confini del linguaggio di programmazione integrato di MQL5, collegando i moduli DLL esterni.
Il risultato del lavoro dell'indicatore è la creazione di un canale di regressione blu, come mostrato nella Figura 6. Per verificare la correttezza della costruzione del canale, il grafico mostra un "Canale di regressione", dall'arsenale di strumenti di analisi tecnica MetaTrader 5, contrassegnato in rosso.
Come si può vedere nella figura, le linee centrali del canale si fondono insieme. Nel frattempo, vi è una leggera differenza della larghezza del canale (pochi punti), dovuta ai diversi approcci nel suo calcolo.
Figura 6. Confronto dei canali di regressione
Autore: Andrey Voytenko