English Русский 中文 Español Deutsch 日本語 Português 한국어 Français Türkçe
Esposizione del codice C# in MQL5 utilizzando esportazioni non gestite

Esposizione del codice C# in MQL5 utilizzando esportazioni non gestite

MetaTrader 5Integrazione | 17 dicembre 2021, 15:15
154 0
investeo
investeo

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

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.dll
MQL5\Scripts\unmanagedexportsdllexample1.mq5
MQL5\Scripts\unmanagedexportsdllexample2.mq5
MQL5\Scripts\unmanagedexportsdllexample3.mq5
MQL5\Scripts\unmanagedexportsdllexample4.mq5
MQL5\Scripts\unmanagedexportsdllexample5.mq5
MQL5\Experts\unmanagedexportsdllexample6.mq5


Fonti

  1. Exporting .NET DLLs with Visual Studio 2005 to be Consumed by Native Applications
  2. Interoperating with Unmadged Coded
  3. Introduction to COM Interop
  4. Component Object Model (COM)
  5. Exporting from a DLL Using __declspec(dllexport)
  6. How to: Declare Handles in Native Types
  7. How to call C++ code from Managed, and vice versa (Interop)
  8. Reverse P/Invoke and exception 
  9. How to call a managed DLL from native Visual C++ code in Visual Studio.NET or in Visual Studio 2005
  10. Platform Invoke Tutorial
  11. PInvoke-Reverse PInvoke and __stdcall - __cdecl
  12. Gotchas with Reverse Pinvoke (unmanaged to managed code callbacks)
  13. Mixing .NET and native code
  14. Export Managed Code as Unmanaged
  15. Understanding Classic COM Interoperability With .NET Applications
  16. Managed Extensions for C++ Programming
  17. Robert Giesecke's site
  18. MSBuild Tasks
  19. Common Language Runtime

Tradotto dall’inglese da MetaQuotes Ltd.
Articolo originale: https://www.mql5.com/en/articles/249

Modello di regressione universale per la previsione dei prezzi di mercato Modello di regressione universale per la previsione dei prezzi di mercato
Il prezzo di mercato è formato da un equilibrio stabile tra domanda e offerta che, a sua volta, dipende da una varietà di fattori economici, politici e psicologici. Le differenze di natura e le cause di influenza di questi fattori rendono difficile considerare direttamente tutti i componenti. Questo articolo espone un tentativo di prevedere il prezzo di mercato sulla base di un modello di regressione elaborato.
Random Walk e l'indicatore di tendenza Random Walk e l'indicatore di tendenza
Random Walk sembra molto simile ai dati di mercato reali, ma ha alcune caratteristiche significative. In questo articolo considereremo le proprietà di Random Walk, simulato utilizzando il gioco del lancio della moneta. Per studiare le proprietà dei dati, viene sviluppato l'indicatore di tendenza.
Utilizzo degli indicatori MetaTrader 5 con il framework di apprendimento automatico ENCOG per la previsione delle serie temporali Utilizzo degli indicatori MetaTrader 5 con il framework di apprendimento automatico ENCOG per la previsione delle serie temporali
Questo articolo presenta la connessione di MetaTrader 5 a ENCOG - Advanced Neural Network e Machine Learning Framework. Contiene la descrizione e l'implementazione di un semplice indicatore di rete neurale basato su indicatori tecnici standard e un Expert Advisor basato su un indicatore neurale. Il codice sorgente, i binari compilati, le DLL e una rete addestrata esemplare sono allegati all'articolo.
Implementazione degli indicatori come classi mediante esempi di Zigzag e ATR Implementazione degli indicatori come classi mediante esempi di Zigzag e ATR
Il dibattito su un modo ottimale per calcolare gli indicatori è infinito. Dove dovremmo calcolare i valori dell'indicatore? Nell'indicatore stesso o occorre incorporare l'intera logica in un Expert Advisor che lo utilizza? L'articolo descrive una delle varianti dello spostamento del codice sorgente di un indicatore personalizzato iCustom direttamente nel codice di un Expert Advisor o di uno script con ottimizzazione dei calcoli e modellazione del valore prev_calculated.