English Русский 中文 Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Exponer código C# a MQL5 usando exportaciones no gestionadas

Exponer código C# a MQL5 usando exportaciones no gestionadas

MetaTrader 5Integración | 21 marzo 2014, 13:43
2 342 0
investeo
investeo

Introducción

Durante mucho tiempo estuve buscando una solución sencilla que me permitiera activar el uso de DLLs de código gestionado C# en MQL5. Tras leer varios artículos, me sentí listo para implementar una envoltura C++ para DLL gestionados, y fue entonces cuando me encontré con una solución que me ahorró muchas horas de trabajo.

La solución me facilitó un ejemplo sencillo de exportación de código gestionado C# para ser usado en una aplicación no gestionada. En este artículo, explicaré a fondo el modo gestionado de DLLs (managed mode DLLs), explicaré por qué no se puede acceder a ellos directamente desde MetaTrader y presentaré las soluciones que encontré que me permitieron usar código gestionado desde MetaTrader.

Facilitaré un ejemplo de uso sencillo de una plantilla de exportación no gestionada y continuaré con todos mis descubrimientos. Aquí encontrará información de fondo sólida para cualquiera que intente usar código DLL C# en MetaTrader 5.


1. Código Gestionado vs no Gestionado

Puesto que la mayoría de los lectores no son conscientes de la diferencia entre el código gestionado y el no gestionado, los describiré en unas pocas frases. Básicamente, MetaTrader usa el lenguaje MQL para implementar reglas, indicadores, Asesores Expertos (EA, por sus siglas en inglés) y scripts. No obstante, puede usar bibliotecas ya implementadas en otros lenguajes y enlazarlas dinámicamente durante la ejecución. Estas bibliotecas se llaman también DLLs, o Dynamic Link Libraries (Bibliotecas de Enlace Dinámicas).

Estas bibliotecas son, de hecho, archivos binarios que contienen código fuente que se puede invocar por un número de programas externos para realizar operaciones específicas. Por ejemplo, una biblioteca de redes neuronales puede exportar funciones para formación y simulación de redes neuronales; una biblioteca derivativa puede exportar cálculos de diferentes derivados; una biblioteca de matriz puede exportar operaciones en matrices. Los DLLs para MetaTrader adquirieron gran popularidad al hacer posible esconder partes de la implementación de indicadores en Asesores Expertos. Sin embargo, la principal razón para usar bibliotecas es reutilizar código existente sin necesidad de implementarlo una y otra vez.

Antes de que .NET existiera, todas las DLLs que se compilaban con Visual Basic, Delphi, VC++, ya fueran COM, Win32, o simplemente C++, se podían ejecutar directamente por el sistema operativo. A este código le llamamos código no gestionado o nativo. Después apareció .NET, y facilitó un entorno muy diferente.

El código se controla (gestiona) por .NET Ejecución de Lenguaje Común, o .NET Common Language Runtime - CLR. Los compiladores CLR producen desde el código fuente, que puede estar escrito en varios lenguajes diferentes, Metadata y el Lenguaje Intermedio Común, o Common Intermediate Language - CIL.

CIL es un lenguaje de alto nivel independiente de los aparatos, y Metadata describe completamente tipos de objetos descritos por CIL de acuerdo a la Especificación de Tipografía Común, o Common Type Specification - CTS. Puesto que CLR conoce todo sobre las tipografías, puede facilitarnos un entorno de ejecución gestionado. El control se puede considerar como un servicio de recogida de basura: gestión de memoria automática, eliminación de objetos y sistemas de seguridad, o protección contra errores frecuentes en lenguajes nativos que podría provocar la ejecución de códigos externos con privilegios de administrador, o simplemente invalidación de memoria.

Se debe señalar que el código CIL nunca se ejecuta directamente; siempre se traduce al código nativo del ordenador por la compilación JIT (Just-In-Time, "justo a tiempo"), o pre-compilando CIL en una asamblea. Para una persona que lee esto por primera vez, el concepto de código gestionado puede resultar confuso, de modo que pegaré aquí el flujo general dentro de CLR abajo:

 

 

Figura 1. Common Language Runtime 


2. Posibles implementaciones de acceso a código gestionado desde MQL5

En el siguiente párrafo describiré métodos que nos permitirán acceso a código gestionado desde código no gestionado.

Creo que merece la pena mencionarlos todos, puesto que puede que haya alguien que prefiera usar otro método en lugar del que yo uso. Los métodos que se usan con COM Interop, Reverse P/Invoke, C++ IJW, C++/Cli wrapper class (clase de envoltura C++/Cli) y Unmanaged Exports (Exportaciones No Gestionadas).


2.1. COM Interop 

Component Object Model (COM) es una interfaz binaria estándar creada por Microsoft a principios de los 90. La objetivo principal de esta tecnología era permitir el uso de objetos creados en diferentes lenguajes de programación en cualquier otro objeto COM sin conocimiento de su implementación interna. Este requisito refuerza la implementación de un interfaz estrictamente definida del COM completamente separada de su implementación.

De hecho, COM se sustituyó por tecnología .NET, y Microsoft anima a usar .NET en lugar de COM. Para facilitar compatibilidad inversa con códigos antiguos, .NET puede cooperar con COM en ambas direcciones, es decir, .NET puede llamar a métodos COM, y los objetos COM pueden hacer uso de código gestionado .NET.

Esta funcionalidad se llama COM Interopability, o COM Interop. COM interop API ES el espacio de nombres gestionado del System.Runtime.InteropServices.

 

Figura 2. Modelo de COM Interoperability

Figura 2. Modelo de COM Interoperability 


El siguiente código COM llama a una función simple raw_factorial.

Por favor, fíjese en las funciones CoInitialize(), CoCreateInstance() y Uninitialize(), y en la función de llamada de interfaz:

#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;
}

Para más información sobre COM Interop, por favor, lea la documentación detallada en la Introduction to COM Interop (Introducción a COM Interop), y el ejemplo de uso que encontré en el blog de msdn: How to call C++ code from Managed, and vice versa (Interop) (Cómo Llamar a un Código C++ desde un Código Gestionado, y viceversa (Interop)).


2.2. Reverse P/Invoke

Platform Invoke, comúnmente conocido como P/Invoke, permite a .NET llamar a cualquier función en cualquier lenguaje no gestionado siempre que su firma se redeclare. Esto se consigue ejecutando un señalizador de función nativa desde .NET. Su uso se describe a fondo en Platform Invoke Tutorial (Manual de Invocación de Plataforma).

La forma básica de utilizarlo es usando el atributo DllImport para marcar la función importada:

// 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();
    }
}

La operación inversa se puede describir como la facilitación de una rellamada delegada gestionada a un código no gestionado.

Esto se llama Reverse P/Invoke (P/Invoke Inverso), y se consigue implementando una función delegada pública en un entorno gestionado e importando la función de llamada implementada en DLL nativo: 

#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);
      }
}

El ejemplo de código gestionado es así:

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);
}

Es importante señalar que esta solución requiere el lado gestionado para comenzar la interacción.

Para más información, por favor lea Gotchas with Reverse Pinvoke (unmanaged to managed code callbacks) (Trucos con Reverse Pinvoke (rellamadas de código gestionado y no gestionado)) y PInvoke-Reverse PInvoke and  stdcall - cdecl (PInvoke- PInvoke Inverso y  stdcall - cdecl).


2.3. C++ IJW

C++ interop, conocido como It Just Works ("Simplemente Funciona" - IJW) es un elemento específico de C++ facilitado por Managed Extensions for C++ (Extensiones Gestionadas para 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);
} 

Esta solución podría ser útil para gente que desea usar su C++ gestionado en una aplicación no gestionada. Para más referencias, por favor lea Interoperability in Managed Extensions for C++ (Interoperabilidad en Extensiones Gestionadas para C++) y Using IJW in Managed C++ (Usar IJW en C++ Gestionados).


2.4. C++/Cli wrapper class

La implementación de C++/Cli wrapper class (C++/clase de envoltura Cli) toma su nombre de la acción de incrustar o envolver una clase con otra clase escrita en modo C++/Cli. El primer paso para escribir la envoltura DLL es escribir la clase C++ que envuelve los métodos de la clase gestionada original. 

La clase de envoltura debe contener un identificador de objeto .NET usando una plantilla gcroot<> y debe delegar todas las llamadas de la clase original. La clase de envoltura se compila a un formato de IL (lenguaje intermediario), que es un lenguaje gestionado.

El siguiente paso es escribir una clase nativa C++ con la directiva no gestionada #pragma que envuelva la clase IL y delegue todas las llamadas con la directiva __declspec(dllexport). Estos pasos haran un DLL C++ nativo que se podrá usar por cualquier aplicación no gestionada.

Por favor, vea el siguiente ejemplo de implementación. El primer paso es para implementar el código C#.

La clase calculadora del ejemplo contiene dos métodos públicos: 

public class Calculator
{
    public int Add(int first, int second)
    {
        return first + second;
    }
    public string FormatAsString(float i)
    {
        return i.ToString();
    }
}

El siguiente paso es escribir una envoltura gestionada que delegará todos los métodos de la clase calculadora:

#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;
    }
};

Por favor, fíjese en que la referencia a la clase Calculadora original se almacena usando la instrucción gcnew y se guarda como plantilla gcroot<>. Todos los métodos envueltos pueden tener el mismo nombre que los originales, y los parámetros y valores de devolución están precedidos por __Param y __ReturnVal, respectivamente.

Ahora se debe implementar la clase C++ no gestionada que envuelve a C++/Cli y exporta métodos DLL C++ nativos.

El archivo del encabezamiento debe contener una definición de clase con la directiva__declspec(dllexport) y almacenar el señalizador a la clase de envoltura.

#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);
};

Y su implementación:

#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;
}

Puede encontrar una guía paso a paso para hacer esta clase de envoltura en .NET to C++ Bridge (Puente de .NET a C++).

Puede encontrar más información sobre la creación de envolturas en Mixing .NET and native code (Mezclar .NET y código nativo), y para información general sobre la declaración de identificadores en Tipografía Nativa, por favor lea How to: Declare Handles in Native Types (Cómo Declarar Identificadores en Tipografías Nativas).


2.5. Exportaciones no gestionadas 

Esta tecnología se describe en detalle en el libro Expert .NET 2.0 IL Assembler (Ensamblador Experto de .NET 2.0 IL), que recomiendo a cualquiera que quiera leer más sobre el compilador .NET. La principal idea es exponer métodos gestionados como exportaciones no gestionadas de un DLL gestionado descompilando módulos ya compilados en código IL usando ILDasm, cambiando las tablas VTable y VTableFixup del módulo y recompilando el DLL usando ILAsm.

Esta tarea puede dar algo de miedo, pero el resultado de esta operación será la producción de un DLL que se puede usar desde cualquier aplicación no gestionada. Hay que recordar que se sigue tratando de una asamblea gestionada, de modo se debe instalar un marco de trabajo .NET. Puede encontrar un manual paso a paso para ello en Export Managed Code as Unmanaged (Código Gestionado de Exportaciones como no Gestionadas) .

Tras descompilar DLL usando ILDasm, obtendremos el código fuente en lenguaje IL. Por favor, observe un ejemplo sencillo de código IL con exportación no gestionada a continuación:

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
}

Las líneas de código fuente IL responsables de la implementación de exportaciones no gestionadas son:

..vtfixup [1] int32 fromunmanaged at VT_01
..data VT_01 = int32(0)

y

..vtentry 1:1
..export [1] as foo

La primera parte es responsable de añadir la entrada de función en la tabla VTableFixup y de configurar la dirección virtual de VT_01 en la función. La segunda parte especifica qué VTEntry se usará para esta función y el alias de la exportación para que la función de exporte. 

Las ventajas de esta solución son que, durante la fase de la implementación del DLL, no necesitamos implementar ningún otro código adicional aparte del DLL gestionado habitual C# y, tal y como se dice en el libro, que este método abre el mundo gestionado completamente, con toda su seguridad y todas sus bibliotecas de clase para clientes no gestionados.

La desventaja es que familiarizarse con el lenguaje de asamblea .NET no es plato del gusto de todos. Yo estaba convencido de escribir una clase de envoltura c++ en lugar de ello hasta que encontré la plantilla de exportaciones no gestionadas de Rober Giesecke: http://sites.google.com/site/robertgiesecke/, que permite usar exportaciones no gestionadas sin necesidad alguna de estudiar el código IL.


3. Plantilla de Exportaciones no Gestionadas C#

La plantilla de proyectos de exportaciones no gestionadas C# de R.Giesecke usa la tarea MSBuild, que automáticamente añade los elementos de reparación VT tras su instalación. Con ello, no hay necesidad de cambiar el lenguaje IL. El paquete de la plantilla solo se debe descargar como archivo .zip y copiar en la carpeta ProjectTemplates del Visual Studio.

Tras compilar el proyecto, el archivo DLL resultante se pude importar sin problemas por MetaTrader, y daré ejemplos de ello en la siguiente sección.


4. Ejemplos

No ha sido tarea fácil deducir cómo pasar variables, arrays y estructuras entre MetaTrader y C# usando un método de ordenamiento correcto, y creo que esta información que les daré ahora les ahorrará mucho tiempo. Todos los ejemplos se compilaron en Windows Vista con .NET 4.0 y Visual C# Express 2010. También añadiré un archivo adjunto con una muestra de DLL con código MQL5 que invoca funcíones de DLL C# al artículo.


4.1. Ejemplo 1. Añadir dos números íntegros, variables dobles o flotantes en una función DLL y devolver los resultados 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;
        }

    }
}

Como seguramente han notado, cada función exportada está precedida por la directiva DllExport. El primer parámetro describe el alias de la función exportada, y el segundo parámetro llama a la convención - para MetaTrader debemos usar CallingConvention.StdCall.

El código MQL5 que importa y usa las funciones exportadas del DLL es directo y no se diferencia de ningún otro DLL escrito en C++ nativo. Al principio debemos declarar las funciones importadas dentro del bloque #import, e indicar qué funciones del DLL se pueden usar más adelante del código 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));
     }
  }
//+------------------------------------------------------------------+

Resultado 

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.3. Ejemplo 2. Acceso a array monodimensional

        [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];
        }

Para ordenar un array monodimensional, la directiva MarshalAs debe pasar UnmanagedType.LPArray como el primer parámetro, y SizeParamIndex como el segundo parámetro. SizeParamIndex indica qué parámetro (contando desde 0) es el parámetro que contiene el tamaño del array.

En los ejemplos de arriba, i es el tamaño del array, e idx es el índice del elemento a devolver.

Ejemplo de código MQL5 con uso de acceso al 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));

     }
  }
//+------------------------------------------------------------------+

Resultado

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. Ejemplo 3. Poblar un array monodimensional y devolverlo 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;
        }

Este ejemplo usa dos arrays de entrada para comparar la convención de parámetro de entrada. Si los elementos que se cambian se devuelven a Metatrader (pasando por referencia), es suficiente poner atributos [In, Out,] antes del atributo 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]));
      
  }
//+------------------------------------------------------------------+

 Resultado

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. Ejemplo 4. Acceso a un array bidimensional

        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;
        }

Un array bidimensional no es tan sencillo para ordenar, pero yo usé un truco: pasar un array 2D como un array monodimensional, y acceder a los elementos del array con funciones idx auxiliares.

//+------------------------------------------------------------------+
//|                                  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]));

  }
//+------------------------------------------------------------------+

Resultado

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. Ejemplo 5. Sustituir contenidos de la cadena de caracteres

  	 [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;
        }

Este ejemplo es corto, pero me llevó bastante tiempo el implementarlo porque intenté usar parámetros de cadena de caracteres usando atributos [In,Out] o con palabras clave ref o out sin éxito.

La solución es usar StringBuilder en lugar de una variable de cadena de caracteres.

//+------------------------------------------------------------------+
//|                                  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);

  }
//+------------------------------------------------------------------+

Resultado

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. Ejemplo 6. Enviar y cambiar la estructura MqlTick struct

	 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;
        }

MqlTick struct se pasa como referencia, marcada por la palabra clave ref. La estructura MqlTick struct en sí misma debe ser precedida por el atributo [StructLayout (LayoutKind.Sequential, Pack =1)].

El parámetro "pack" describe el alineamiento de datos en la estructura, por favor lea el campo StructLayoutAttribute.Pack para más detalles.  

//+------------------------------------------------------------------+
//|                                  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);
  }
//+------------------------------------------------------------------+

Resultado

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

 

Conclusión

En este artículo presento diferentes métodos de interacción entre código MQL5 y código gestionado C#.

También facilito varios ejemplos sobre cómo ordenar estructuras MQL5 en contraposición a C#, y cómo invocar funciones DLL exportadas en scripts MQL5. Creo que los ejemplos que proporciono podrán servir como base para estudios futuros sobre escritura de DLLs en código gestionado.

Este artículo también abre puertas para MetaTrader para usar varias bibliotecas que ya están implementadas en C#. Para más información, por favor consulte los enlaces de artículos de la sección de Referencias.


Para ponerlo en práctica, localice los archivos en las siguientes carpetas:

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


Referencias

  1. Exporting .NET DLLs with Visual Studio 2005 to be Consumed by Native Applications (Exportar DLLs de .NET con Visual Studio 2005 para ser Consumidos por Aplicaciones Nativas)
  2. Interoperating with Unmanaged Coded (Interoperar con Código no Gestionado)
  3. Introduction to COM Interop (Introducción a COM Interop)
  4. Component Object Model (COM)
  5. Exporting from a DLL Using __declspec(dllexport) (Exportar de un DLL usando __declspec(dllexport))
  6. How to: Declare Handles in Native Types (Cómo Declarar Identificadores en Tipografías Nativas)
  7. How to call C++ code from managed, and vice versa (Interop) (Cómo llamar a un código C++ de un código gestionado, y viceversa (Interop))
  8. Reverse P/Invoke and exception (P/Invoke inverso y excepción) 
  9. How to call a managed DLL from native Visual C++ code in Visual Studio.NET or in Visual Studio 2005 (Cómo llamar a un DLL gestinado desde un código Visual C++ nativo en Visual Studio.NET o en Visual Studio 2005)
  10. Platform Invoke Tutorial (Manual de Invocación de Plataforma)
  11. PInvoke-Reverse PInvoke and __stdcall - __cdecl
  12. Gotchas with Reverse Pinvoke (unmanaged to managed code callbacks) (Trucos con Reverse Pinvoke (rellamadas de código gestionado y no gestionado))
  13. Mixing .NET and native code (Mezclar .NET y código nativo)
  14. Export Managed Code as Unmanaged (Código Gestionado de Exportación como Código no Gestionado)
  15. Understanding Classic COM Interoperability With .NET Applications (Entender Interoperabilidad Clásica COM con Aplicaciones .NET)
  16. Managed Extensions for C++ Programming (Extensiones Gestionadas para Programación C++)
  17. La página web de Robert Giesecke
  18. Tareas de MSBuild
  19. Common Language Runtime

Traducción del inglés realizada por MetaQuotes Ltd.
Artículo original: https://www.mql5.com/en/articles/249

Modelo de Regresión Universal para la Predicción de Precio de Mercado Modelo de Regresión Universal para la Predicción de Precio de Mercado
El precio de mercado se forma mediante un equilibrio estable entre oferta y demanda, que a su vez depende de una variedad de factores económicos, políticos y psicológicos. Las diferencias en su naturaleza, así como las causas de influencia de estos factores, hacen que sea difícil considerar directamente todos los componentes. Este artículo llevará a cabo un intento de predecir el precio de mercado basándose en un elaborado modelo de regresión.
Random Walk y el Indicador de Tendencias Random Walk y el Indicador de Tendencias
Random Walk (RW) es muy parecido a los datos del mercado real, pero tiene algunos detalles significativos. En este artículo veremos las propiedades de Random Walk, que simularemos usando el juego de cara o cruz. Para estudiar las propiedades de los datos se desarrolló el indicador de tendencias.
Usar Indicadores de MetaTrader 5 con la Estructura de Aprendizaje Automático ENCOG para Predicción de Series Cronológicas Usar Indicadores de MetaTrader 5 con la Estructura de Aprendizaje Automático ENCOG para Predicción de Series Cronológicas
Este artículo presenta modos de conectar MetaTrader 5 a ENCOG - Red Neuronal Avanzada y Estructura de Aprendizaje Automático. Contiene la descripción e implementación de un indicador de red neuronal sencillo basado en indicadores técnicos estándar y un Asesor Experto basado en un indicador neuronal. Todos los códigos fuente, binarios combinados, DLLs y un ejemplo de red formada se pueden encontrar como archivos adjuntos a este artículo.
Implementación de Indicadores como Classes por Ejemplos de Zigzag y ATR Implementación de Indicadores como Classes por Ejemplos de Zigzag y ATR
El debate sobre la mejor forma de calcular indicadores es infinito. Dónde deberíamos calcular los valores de indicador: en el indicador mismo, o incrustar la lógica entera en un Expert Advisor que la use? Este artículo describe una de las variantes para mover el código fuente de un indicador personalizado iCustom al código de un Expert Advisor o script con optimización de cálculos y modelizacion del valor prev_calculated.