
Esposizione del codice C# in MQL5 utilizzando esportazioni non gestite
Introduzione
Stavo cercando da molto tempo una soluzione semplice che mi consentisse di utilizzare le DLL C# in modalità gestita in MQL5. Dopo aver letto molti articoli, ero pronto per implementare il wrapper C++ per le DLL gestite quando mi sono imbattuto in una soluzione brillante che mi ha risparmiato molte ore di lavoro.
La soluzione mi ha fornito un semplice esempio di esportazione di codice C# gestito da utilizzare per l'applicazione non gestita. In questo articolo fornirò informazioni sulle DLL in modalità gestita, descriverò perché non è possibile accedervi direttamente da MetaTrader e introdurrò le soluzioni che ho trovato e che consentono di utilizzare il codice gestito da MetaTrader.
Fornirò un esempio di utilizzo semplice del modello di esportazione non gestito e continuerò con tutto ciò che ho scoperto. Ciò fornirà un contesto solido per chiunque tenti di utilizzare il codice DLL C# su MetaTrader 5.
1. Codice gestito e non gestito
Dato che la maggior parte dei lettori potrebbe non essere consapevole della differenza tra codice gestito e non gestito, lo descriverò in breve. Fondamentalmente, MetaTrader utilizza il linguaggio MQL per implementare regole di trading, indicatori, Expert Advisor e script. Può comunque utilizzare librerie già implementate in altri linguaggi e collegarle dinamicamente in fase di esecuzione. Queste librerie sono chiamate anche DLL o Dynamic Link Libraries.
Le librerie sono infatti file binari che contengono codice sorgente compilato e che può essere richiamato da una serie di programmi esterni per eseguire operazioni specifiche. Ad esempio, la libreria di reti neurali può esportare funzioni per l'addestramento e il test della rete neurale, la libreria derivata può esportare calcoli di diverse derivate, la libreria di matrici può esportare operazioni su matrici. Le DLL per MetaTrader sono diventate sempre più popolari poiché hanno permesso di nascondere parti dell'implementazione di indicatori o Expert Advisor. Uno dei motivi principali per utilizzare le librerie è riutilizzare il codice esistente senza la necessità di implementarlo più e più volte.
Prima che esistesse .NET, tutte le DLL compilate da Visual Basic, Delphi, VC++, che si tratti di COM, Win32 o semplicemente C++, potevano essere eseguite direttamente dal sistema operativo. Ci riferiamo a questo codice come codice non gestito o nativo. Poi è arrivato .NET e ha fornito un ambiente molto diverso.
Il codice è controllato (o gestito) da .NET Common Language Runtime - CLR. I compilatori CLR sono tenuti a produrre dal codice sorgente, che può essere scritto in diversi linguaggi, Metadata e Common Intermediate Language - CIL.
CIL è un linguaggio di livello superiore e indipendente dalla macchina e i metadati descrivono completamente i tipi degli oggetti descritti da CIL secondo il Common Type Specification - CTS. Poiché CLR sa tutto sui tipi, può fornirci un ambiente di esecuzione gestito. La gestione può essere pensata come una raccolta di rifiuti a gestione automatica della memoria e come cancellazione di oggetti e fornitura di sicurezza e protezione contro gli errori comuni nei linguaggi nativi, i quali potrebbero causare l'esecuzione di codice estraneo con privilegi di amministratore o semplicemente l'override della memoria.
Va detto che il codice CIL non viene mai eseguito direttamente - è sempre tradotto in codice macchina nativo dalla compilazione JIT (Just-In-Time) o tramite la precompilazione CIL in assembly. Per una persona che legge questo per la prima volta la nozione di codice in modalità gestita può essere fonte di confusione, per questo motivo sto per incollare di seguito il flusso generale all'interno di CLR:
Figura 1. Common Language Runtime
2. Possibili implementazioni di accesso al codice gestito da MQL5
Nel paragrafo seguente descriverò i metodi che consentono di accedere al codice gestito dal codice non gestito.
Penso che valga la pena menzionarli tutti in quanto potrebbe esserci qualcuno che preferisce usare un altro metodo rispetto a quello che sto usando io. I metodi in uso sono COM Interop, Reverse P/Invoke, C++ IJW, C++/Cli wrapper class e Unmanaged Exports.
2.1. Interoperabilità COM
Component Object Model (COM) è uno standard di interfaccia binaria introdotto da Microsoft all'inizio degli anni novanta. L'idea centrale di questa tecnologia è quella di consentire agli oggetti creati in diversi linguaggi di programmazione di essere utilizzati da qualsiasi altro oggetto COM senza conoscerne l'implementazione interna. Tale requisito impone l'implementazione di un'interfaccia rigorosa e ben definita del COM che è completamente separata dall'implementazione.
In effetti, COM è stato sostituito dalla tecnologia .NET e Microsoft spinge per utilizzare .NET invece di COM. Per fornire la compatibilità con le versioni precedenti del codice precedente, .NET può cooperare con COM in entrambe le direzioni, ovvero .NET può chiamare metodi COM e l'oggetto COM può utilizzare il codice gestito .NET.
Questa funzionalità è denominata interoperabilità COM o COM Interop. L'API di interoperabilità COM si trova nello spazio dei nomi System.Runtime.InteropServices gestito.
Figura 2. Modello di interoperabilità COM
Il codice di interoperabilità COM seguente chiama una singola funzione raw_factorial.
Da notare le funzioni CoInitialize(), CoCreateInstance() e CoUninitialize() e la funzione di chiamata dell'interfaccia:
#include "windows.h" #include <stdio.h> #import "CSDll.tlb" named_guids int main(int argc, char* argv[]) { HRESULT hRes = S_OK; CoInitialize(NULL); CSDll::IMyManagedInterface *pManagedInterface = NULL; hRes = CoCreateInstance(CSDll::CLSID_Class1, NULL, CLSCTX_INPROC_SERVER, CSDll::IID_IMyManagedInterface, reinterpret_cast<void**> (&pManagedInterface)); if (S_OK == hRes) { long retVal =0; hRes = pManagedInterface->raw_factorial(4, &retVal); printf("The value returned by the dll is %ld\n",retVal); pManagedInterface->Release(); } CoUninitialize(); return 0; }
Per ulteriori letture su COM Interop, vedi la documentazione dettagliata su Introduction to COM Interop e l'esempio di utilizzo che ho trovato sul blog msdn: How to call C++ code from Managed, and vice versa (Interop).
2.2. Reverse P/Invoke
Il Platform Invoke, denominato P/Invoke, consente a .NET di chiamare qualsiasi funzione in qualsiasi linguaggio non gestito, purché la sua firma venga dichiarata nuovamente. Ciò si ottiene eseguendo un puntatore di funzione nativo da .NET. L'utilizzo è ben descritto su Platform Invoke Tutorial.
L'utilizzo di base consiste nell'utilizzare l'attributo DllImport per contrassegnare la funzione importata:
// PInvokeTest.cs using System; using System.Runtime.InteropServices; class PlatformInvokeTest { [DllImport("msvcrt.dll")] public static extern int puts(string c); [DllImport("msvcrt.dll")] internal static extern int _flushall(); public static void Main() { puts("Test"); _flushall(); } }
L'operazione inversa può essere descritta come la fornitura di un callback delegato gestito al codice non gestito.
Viene chiamato Reverse P/Invoke e si ottiene implementando una funzione pubblico di delega in un ambiente gestito e importando la funzione chiamante implementata nella DLL nativa:
#include <stdio.h> #include <string.h> typedef void (__stdcall *callback)(wchar_t * str); extern "C" __declspec(dllexport) void __stdcall caller(wchar_t * input, int count, callback call) { for(int i = 0; i < count; i++) { call(input); } }
L'esempio di codice gestito è il seguente:
using System.Runtime.InteropServices; public class foo { public delegate void callback(string str); public static void callee(string str) { System.Console.WriteLine("Managed: " +str); } public static int Main() { caller("Hello World!", 10, new callback(foo.callee)); return 0; } [DllImport("nat.dll",CallingConvention=CallingConvention.StdCall)] public static extern void caller(string str, int count, callback call); }
Il punto principale di questa soluzione è che richiede che il lato gestito inizi l'interazione.
Per ulteriori riferimenti, consulta Gotchas con Reverse Pinvoke (callback non gestiti a codice gestito) e PInvoke-Reverse PInvoke e stdcall - cdecl.
2.3. C++ IJW
L'interoperabilità C++, denominata It Just Works (IJW)), è una funzionalità specifica di C++, fornita da Managed Extensions for C++:
#using <mscorlib.dll> using namespace System; using namespace System::Runtime::InteropServices; #include <stdio.h> int main() { String * pStr = S"Hello World!"; char* pChars = (char*)Marshal::StringToHGlobalAnsi(pStr).ToPointer(); puts(pChars); Marshal::FreeHGlobal(pChars); }
Questa soluzione potrebbe essere utile per coloro che desiderano utilizzare il proprio C++ gestito in un'applicazione non gestita. Per una fonte completo, consulta Interoperability in Managed Extensions for C++ e Using IJW in Managed C++.
2.4. Classe wrapper C++/Cli
L'implementazione della classe wrapper C++/Cli prende il nome dall'incorporamento o dal wrapping della classe gestita da un'altra classe scritta in modalità C++/Cli. Il primo passaggio per scrivere la DLL wrapper consiste nello scrivere la classe C++ che racchiude i metodi della classe gestita originale.
La classe wrapper deve contenere un handle per l'oggetto .NET utilizzando gcroot<> template e deve delegare tutte le chiamate dalla classe originale. La classe wrapper è compilata in formato IL (linguaggio intermedio) e quindi è gestita.
Il passaggio successivo consiste nello scrivere la classe C++ nativa con la direttiva non gestita #pragma, la quale avvolge la classe IL e delega tutte le chiamate con __declspec(dllexport) directive. Questi passaggi creeranno una DLL C++ nativa che può essere utilizzata da qualsiasi applicazione non gestita.
Osserva l'implementazione di esempio. Il primo passaggio consiste nell'implementare il codice C#.
La classe calcolatrice di esempio contiene due metodi pubblici:
public class Calculator { public int Add(int first, int second) { return first + second; } public string FormatAsString(float i) { return i.ToString(); } }
Il prossimo passaggio è scrivere un wrapper gestito che delegherà tutti i metodi dalla classe calcolatrice:
#pragma once #pragma managed #include <vcclr.h> class ILBridge_CppCliWrapper_Calculator { private: //Aggregating the managed class gcroot<CppCliWrapper::Calculator^> __Impl; public: ILBridge_CppCliWrapper_Calculator() { __Impl = gcnew CppCliWrapper::Calculator; } int Add(int first, int second) { System::Int32 __Param_first = first; System::Int32 __Param_second = second; System::Int32 __ReturnVal = __Impl->Add(__Param_first, __Param_second); return __ReturnVal; } wchar_t* FormatAsString(float i) { System::Single __Param_i = i; System::String __ReturnVal = __Impl->FormatAsString(__Param_i); wchar_t* __MarshaledReturnVal = marshal_to<wchar_t*>(__ReturnVal); return __MarshaledReturnVal; } };
Si noti che il riferimento alla classe Calculator originale viene memorizzato utilizzando l'istruzione gcnew e memorizzato come modello gcroot<>. Tutti i metodi incapsulati possono avere lo stesso nome di quelli originali e i parametri ei valori restituiti sono preceduti rispettivamente da __Param e __ReturnVal.
Ora è necessario implementare la classe C++ non gestita che avvolge il C++/Cli ed esporta i metodi DLL C++ nativi.
Il file di intestazione deve contenere la definizione della classe con la direttiva __declspec(dllexport) e memorizzare il puntatore alla classe wrapper.
#pragma once #pragma unmanaged #ifdef THISDLL_EXPORTS #define THISDLL_API __declspec(dllexport) #else #define THISDLL_API __declspec(dllimport) #endif //Forward declaration for the bridge class ILBridge_CppCliWrapper_Calculator; class THISDLL_API NativeExport_CppCliWrapper_Calculator { private: //Aggregating the bridge ILBridge_CppCliWrapper_Calculator* __Impl; public: NativeExport_CppCliWrapper_Calculator(); ~NativeExport_CppCliWrapper_Calculator(); int Add(int first, int second); wchar_t* FormatAsString(float i); };
E la sua implementazione:
#pragma managed #include "ILBridge_CppCliWrapper_Calculator.h" #pragma unmanaged #include "NativeExport_CppCliWrapper_Calculator.h" NativeExport_CppCliWrapper_Calculator::NativeExport_CppCliWrapper_Calculator() { __Impl = new ILBridge_CppCliWrapper_Calculator; } NativeExport_CppCliWrapper_Calculator::~NativeExport_CppCliWrapper_Calculator() { delete __Impl; } int NativeExport_CppCliWrapper_Calculator::Add(int first, int second) { int __ReturnVal = __Impl->Add(first, second); return __ReturnVal; } wchar_t* NativeExport_CppCliWrapper_Calculator::FormatAsString(float i) { wchar_t* __ReturnVal = __Impl->FormatAsString(i); return __ReturnVal; }
Una guida passo passo per creare questa classe wrapper è descritta su .NET to C++ Bridge.
Una fonte completa per la creazione dei wrapper è disponibile all'indirizzo Mixing .NET and native code , mentre per informazioni generali sulla dichiarazione degli handle nei tipi nativi, consulta How to: Declare Handles in Native Types.
2.5. Esportazioni non gestite
Questa tecnica è descritta nel completo nel libro Expert .NET 2.0 IL Assembler, che consiglio a chiunque voglia leggere riguardo ai dettagli del compilatore .NET. L'idea principale è quella di esporre i metodi gestiti come esportazioni non gestite di una DLL gestita decompilando il modulo già compilato nel codice IL utilizzando ILDasm, modificando le tabelle VTable e VTableFixup del modulo e ricompilando la DLL utilizzando ILAsm.
Questa attività può sembrare scoraggiante, ma il risultato di questa operazione sarà la produzione di una DLL che può essere utilizzata da qualsiasi applicazione non gestita. Bisogna ricordare che è ancora un assembly gestito, quindi si deve installare .NET Framework. Un tutorial dettagliato per eseguire questa operazione è disponibile all'indirizzo Export Managed Code as Unmanaged.
Dopo aver decompilato la DLL usando ILDasm, otteniamo il codice sorgente in linguaggio IL. Osservare questo semplice esempio di codice IL con esportazione non gestita incollata di seguito:
assembly extern mscorlib {} ..assembly UnmExports {} ..module UnmExports.dll ..corflags 0x00000002 ..vtfixup [1] int32 fromunmanaged at VT_01 ..data VT_01 = int32(0) ..method public static void foo() { ..vtentry 1:1 ..export [1] as foo ldstr "Hello from managed world" call void [mscorlib]System.Console::WriteLine(string) ret }
Le righe del codice sorgente IL responsabili dell'implementazione delle esportazioni non gestite sono:
..vtfixup [1] int32 fromunmanaged at VT_01 ..data VT_01 = int32(0)
e
..vtentry 1:1 ..export [1] as foo
La prima parte è responsabile dell'aggiunta della voce della funzione nella tabella VTableFixup e dell'impostazione dell'indirizzo virtuale VT_01 alla funzione. La seconda parte specifica quale VEntry deve essere utilizzato per questa funzione ed esporta l'alias per la funzione da esportare.
I vantaggi di questa soluzione sono che durante la fase di implementazione della DLL non è necessario implementare alcun codice aggiuntivo oltre alla consueta DLL C# gestita e, come affermato dal libro, questo metodo apre completamente il mondo gestito con tutte le sue librerie di sicurezza e classi a non gestite clienti.
Lo svantaggio è che entrare nel linguaggio assembly .NET non è adatto a tutti. Ero convinto che avrei scritto della classe wrapper c++ fino a quando non ho trovato il modello di esportazione non gestito di Robert Giesecke: http://sites.google.com/site/robertgiesecke/ che consente di utilizzare le esportazioni non gestite senza la necessità di entrare nel codice IL.
3. Modello C# per esportazioni non gestite
Il modello per progetti C# di esportazione non gestiti di R.Giesecke utilizza l’attività MSBuild che aggiunge automaticamente le correzioni VT appropriate dopo la compilazione, quindi non è affatto necessario modificare il codice IL. Il pacchetto del modello deve essere scaricato solo come file zip e copiato nella cartella ProjectTemplates di Visual Studio.
Dopo aver compilato il progetto, il file DLL risultante può essere importato senza problemi da MetaTrader. Fornirò degli esempi nelle sezioni successive.
4. Esempi
È stato un compito piuttosto impegnativo capire come passare variabili, array e struct tra MetaTrader e C# utilizzando il metodo Marshalling corretto e penso che le informazioni fornite qui ti faranno risparmiare molto tempo. Tutti gli esempi sono stati compilati su Windows Vista con .NET 4.0 e Visual C# Express 2010. Allego anche la DLL di esempio con codice MQL5 che richiama le funzioni da DLL C# all'articolo.
4.1. Esempio 1. Aggiunta di due variabili intere, doppie o float nella funzione DLL e restituzione del risultato a MetaTrader
using System; using System.Text; using RGiesecke.DllExport; using System.Runtime.InteropServices; namespace Testme { class Test { [DllExport("Add", CallingConvention = CallingConvention.StdCall)] public static int Add(int left, int right) { return left + right; } [DllExport("Sub", CallingConvention = CallingConvention.StdCall)] public static int Sub(int left, int right) { return left - right; } [DllExport("AddDouble", CallingConvention = CallingConvention.StdCall)] public static double AddDouble(double left, double right) { return left + right; } [DllExport("AddFloat", CallingConvention = CallingConvention.StdCall)] public static float AddFloat(float left, float right) { return left + right; } } }
Come avrai notato, ogni funzione esportata è preceduta dalla direttiva DllExport. Il primo parametro descrive l'alias della funzione esportata e il secondo parametro la convenzione di chiamata. Per MetaTrader dobbiamo usare CallingConvention.StdCall.
Il codice MQL5 che importa e utilizza le funzioni esportate dalla DLL è semplice e non differisce da nessun'altra DLL scritta in C++ nativo. Inizialmente si devono dichiarare le funzioni importate all'interno del blocco #import e indicare quali funzioni della DLL possono essere successivamente utilizzate dal codice MQL5:
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample1.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int Add(int left,int right); int Sub(int left,int right); float AddFloat(float left,float right); double AddDouble(double left,double right); #import //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- for(int i=0; i<3; i++) { Print(Add(i,666)); Print(Sub(666,i)); Print(AddDouble(666.5,i)); Print(AddFloat(666.5,-i)); } } //+------------------------------------------------------------------+
Risultato
2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 664.50000 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 668.5 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 664 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 668 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 665.50000 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 667.5 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 665 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 667 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 666.50000 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 666.5 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 666 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 666
4.2. Esempio 2. Accesso all’array uni-dimensionale
[DllExport("Get1DInt", CallingConvention = CallingConvention.StdCall)] public static int Get1DInt([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] tab, int i, int idx) { return tab[idx]; } [DllExport("Get1DFloat", CallingConvention = CallingConvention.StdCall)] public static float Get1DFloat([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] float[] tab, int i, int idx) { return tab[idx]; } [DllExport("Get1DDouble", CallingConvention = CallingConvention.StdCall)] public static double Get1DDouble([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] double[] tab, int i, int idx) { return tab[idx]; }
Per eseguire il marshalling di un array uni-dimensionale, la direttiva MarshalAs deve passare UnmanagedType.LPArray come primo parametro e SizeParamIndex come secondo parametro. SizeParamIndex indica quale parametro (a partire da 0) contiene la dimensione dell'array.
Negli esempi sopra i è la dimensione dell'array e idx è l'indice dell'elemento da restituire.
Di seguito è riportato il codice di esempio MQL5 che utilizza l'accesso all'array:
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample2.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int Get1DInt(int &t[],int i,int idx); float Get1DFloat(float &t[],int i,int idx); double Get1DDouble(double &t[],int i,int idx); #import //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- int tab[3]; tab[0] = 11; tab[1] = 22; tab[2] = 33; float tfloat[3]={0.5,1.0,1.5}; double tdouble[3]={0.5,1.0,1.5}; for(int i=0; i<3; i++) { Print(tab[i]); Print(Get1DInt(tab,3,i)); Print(Get1DFloat(tfloat,3,i)); Print(Get1DDouble(tdouble,3,i)); } } //+------------------------------------------------------------------+
Risultato
2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 1.5 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 1.50000 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 33 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 33 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 1 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 1.00000 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 22 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 22 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 0.5 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 0.50000 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 11 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 11
4.3. Esempio 3. Popolare un array dimensionale con restituzione a MetaTrader
[DllExport("SetFiboArray", CallingConvention = CallingConvention.StdCall)] public static int SetFiboArray([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] tab, int len, [In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] res) { res[0] = 0; res[1] = 1; if (len < 3) return -1; for (int i=2; i<len; i++) res[i] = res[i-1] + res[i-2]; return 0; }
Questo esempio usa due array di input per confrontare la convenzione dei parametri di input. Se gli elementi modificati devono essere restituiti a Metatrader (passando per il riferimento) è sufficiente mettere gli attributi [In, Out,] prima dell'attributo MarshalAs.
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample3.mq5 | //| Copyright 2011, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int SetFiboArray(int& t[], int i, int& o[]); #import //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- int fibo[10]; static int o[10]; for (int i=0; i<4; i++) { fibo[i]=i; o[i] = i; } SetFiboArray(fibo, 6, o); for (int i=0; i<6; i++) Print(IntegerToString(fibo[i])+":"+IntegerToString(o[i])); } //+------------------------------------------------------------------+
Risultato
2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 0:5 2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 0:3 2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 3:2 2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 2:1 2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 1:1 2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 0:0
4.4. Esempio 4. Accesso all’array bidimensionale
public static int idx(int a, int b) {int cols = 2; return a * cols + b; } [DllExport("Set2DArray", CallingConvention = CallingConvention.StdCall)] public static int Set2DArray([In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] tab, int len) { tab[idx(0, 0)] = 0; tab[idx(0, 1)] = 1; tab[idx(1, 0)] = 2; tab[idx(1, 1)] = 3; tab[idx(2, 0)] = 4; tab[idx(2, 1)] = 5; return 0; }
Non è così semplice eseguire il marshalling per 'array bidimensionale, ma ho usato un trucco: passare l'array 2D come unidimensionale e accedere agli elementi dell'array tramite la funzione idx ausiliaria.
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample4.mq5 | //| Copyright 2011, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int Set2DArray(int &t[][2],int i); #import //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- int t2[3][2]; Set2DArray(t2,6); for(int row=0; row<3; row++) for(int col=0; col<2; col++) Print("t2["+IntegerToString(row)+"]["+IntegerToString(col)+"]="+IntegerToString(t2[row][col])); } //+------------------------------------------------------------------+
Risultato
2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[2][1]=5 2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[2][0]=4 2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[1][1]=3 2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[1][0]=2 2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[0][1]=1 2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[0][0]=0
4.5. Esempio 5. Sostituzione del contenuto della stringa
[DllExport("ReplaceString", CallingConvention = CallingConvention.StdCall)] public static int ReplaceString([In, Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder str, [MarshalAs(UnmanagedType.LPWStr)]string a, [MarshalAs(UnmanagedType.LPWStr)]string b) { str.Replace(a, b); if (str.ToString().Contains(a)) return 1; else return 0; }
Questo esempio è breve, ma mi ci è voluto molto tempo per implementarlo poiché ho provato a utilizzare il parametro stringa utilizzando gli attributi [In,Out] o con parole chiave ref o out senza successo.
La soluzione è usare StringBuilder invece della variabile stringa.
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample5.mq5 | //| Copyright 2011, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int ReplaceString(string &str,string a,string b); #import //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- string str="A quick brown fox jumps over the lazy dog"; string stra = "fox"; string strb = "cat"; Print(str); Print(ReplaceString(str,stra,strb)); Print(str); } //+------------------------------------------------------------------+
Risultato
2011.01.30 22:18:36 UnmanagedExportsDLLExample5 (EURUSD,M1) A quick brown cat jumps over the lazy dog 2011.01.30 22:18:36 UnmanagedExportsDLLExample5 (EURUSD,M1) 0 2011.01.30 22:18:36 UnmanagedExportsDLLExample5 (EURUSD,M1) A quick brown fox jumps over the lazy dog
4.6. Esempio 6. Invio e modifica della struttura MqlTick
private static List<MqlTick> list; [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct MqlTick { public Int64 Time; public Double Bid; public Double Ask; public Double Last; public UInt64 Volume; } [DllExport("AddTick", CallingConvention = CallingConvention.StdCall)] public static int AddTick(ref MqlTick tick, ref double bidsum) { bidsum = 0.0; if (list == null) list = new List<MqlTick>(); tick.Volume = 666; list.Add(tick); foreach (MqlTick t in list) bidsum += t.Ask; return list.Count; }
La struttura MqlTick viene passata come riferimento, contrassegnata dalla parola chiave ref. La struttura MqlTick stessa deve essere preceduta dall'attributo [StructLayout (LayoutKind.Sequential, Pack = 1)].
Il parametro Pack descrive l'allineamento dei dati nella struttura. Consulta StructLayoutAttribute.Pack Field per i dettagli.
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample6.mq5 | //| Copyright 2011, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int AddTick(MqlTick &tick, double& bidsum); #import //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- indicator buffers mapping //--- return(0); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int& spread[]) { //--- MqlTick newTick; double bidsum; SymbolInfoTick(Symbol(), newTick); Print("before = " + IntegerToString(newTick.volume)); Print(AddTick(newTick, bidsum)); Print("after = " + IntegerToString(newTick.volume) + " : " + DoubleToString(bidsum)); //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+
Risultato
2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 8.167199999999999 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 6 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) before = 0 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 6.806 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 5 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) before = 0 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 5.4448 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 4 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) before = 0 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 4.0836 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 3 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) before = 0 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 2.7224 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 2 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) before = 0 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 1.3612 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 1 2011.01.30 23:59:04 TickDLLSend (EURUSD,M1) before = 0
Conclusione
In questo articolo ho presentato diversi metodi di interazione tra il codice MQL5 e il codice gestito C#.
Ho anche fornito diversi esempi su come eseguire il marshalling di strutture MQL5 contro C# e come richiamare le funzioni DLL esportate negli script MQL5. Credo che gli esempi forniti possano servire come base per ricerche future sulla scrittura di DLL nel codice gestito.
Questo articolo apre anche le porte a MetaTrader per utilizzare le tante librerie che sono già implementate in C#. Per ulteriori riferimenti, consulta gli articoli collegati nella sezione Fonti.
Per testarlo, individuare i file nelle seguenti cartelle:
MQL5\Libraries\testme.dllMQL5\Scripts\unmanagedexportsdllexample1.mq5
MQL5\Scripts\unmanagedexportsdllexample2.mq5
MQL5\Scripts\unmanagedexportsdllexample3.mq5
MQL5\Scripts\unmanagedexportsdllexample4.mq5
MQL5\Scripts\unmanagedexportsdllexample5.mq5
MQL5\Experts\unmanagedexportsdllexample6.mq5
Fonti
- Exporting .NET DLLs with Visual Studio 2005 to be Consumed by Native Applications
- Interoperating with Unmadged Coded
- Introduction to COM Interop
- Component Object Model (COM)
- Exporting from a DLL Using __declspec(dllexport)
- How to: Declare Handles in Native Types
- How to call C++ code from Managed, and vice versa (Interop)
- Reverse P/Invoke and exception
- How to call a managed DLL from native Visual C++ code in Visual Studio.NET or in Visual Studio 2005
- Platform Invoke Tutorial
- PInvoke-Reverse PInvoke and __stdcall - __cdecl
- Gotchas with Reverse Pinvoke (unmanaged to managed code callbacks)
- Mixing .NET and native code
- Export Managed Code as Unmanaged
- Understanding Classic COM Interoperability With .NET Applications
- Managed Extensions for C++ Programming
- Robert Giesecke's site
- MSBuild Tasks
- Common Language Runtime
Tradotto dall’inglese da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/en/articles/249





- 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