Cómo intercambiar datos: una DLL para MQL5 en 10 minutos.

MetaQuotes | 22 enero, 2014

No hay muchos desarrolladores que recuerden cómo escribir una simple DLL y cuáles son las características de la vinculación de los diferentes sistemas. Usando varios ejemplos intentaré mostrar todo el proceso de creación de la DLL en 10 minutos, así como discutir algunos aspectos técnicos de nuestra implementación de la vinculación. Mostraré el proceso de creación de la librería DLL paso a paso en Visual Studio con ejemplos de intercambio de diferentes tipos de variables  (números, matrices, strings, etc.). Además, explicaré cómo proteger su terminal de cliente frente a los fallos en DLL personalizadas.

De hecho, no hay muchos programadores que recuerden exactamente cómo escribir una simple librería DLL y cuáles son las características de la vinculación de los diferentes sistemas. Usando varios ejemplos intentaré mostrar todo el proceso de creación de la DLL en 10 minutos, así como discutir algunos aspectos técnicos de nuestra implementación de la vinculación. Usaré Visual Studio 2005/2008. Sus expresiones Express son gratuitas y pueden descargarse de la web de Microsoft.

1. Crear un proyecto de DLL en C++ en Visual Studio 2005/2008

Ejecutamos el Win32 Application Wizard usando el menú "Archivo-> Nuevo", y seleccionamos como tipo de proyecto "Visual C++", elegimos la plantilla Win32 Console Application' e introducimos el nombre del proyecto (por ejemplo, 'MQL5DLLSamples'). Seleccionamos un directorio raíz para almacenar la "Ubicación" del proyecto en lugar de elegir la que nos ofrece por defecto, deshabilitamos la casilla de verificación de "Crear un directorio para la solución" y hacemos clic en OK:

Fig. 1. Win32 Application Wizard, creación del proyecto DLL

En el siguiente paso pulsamos "Siguiente" para ir a la página de configuración:

Fig. 2. Win32 Application Wizard, configuración del proyecto

En la página final seleccionamos el tipo de aplicación DLL y dejamos los demás campos vacíos tal y como están y hacemos clic en Finalizar. No debemos elegir la opción "Exportar símbolos" si no queremos eliminar el código de ejemplo que se añade automáticamente:

Fig. 3. Win32 Application Wizard, configuración de la aplicación

Como resultado tendremos un proyecto vacío:

Fig. 4. El proyecto DLL vacío preparado por el Wizard

Para simplificar la prueba es mejor especificar en las opciones "Directorio de salida" la salida de los archivos DLL en "...\MQL5\Libraries" del terminal de cliente. Posteriormente nos ahorrará mucho tiempo.

Fig. 5. Directorio de salida de DLL


2. Preparándonos para añadir las funciones

Añadimos la macro '_DLLAPI' al final del archivo stdafx.h para que podamos describir de forma fácil y apropiada las funciones exportadas:

//+------------------------------------------------------------------+
//|                                                 MQL5 DLL Samples |
//|                   Copyright 2001-2010, MetaQuotes Software Corp. |
//|                                        https://www.metaquotes.net |
//+------------------------------------------------------------------+
#pragma once

#include "targetver.h"

#define WIN32_LEAN_AND_MEAN             // Exclude rarely-used stuff from Windows headers
#include <windows.h>

//---
#define _DLLAPI extern "C" __declspec(dllexport)
//+------------------------------------------------------------------+

Las llamadas a las funciones importadas DLL en MQL5 deben tener la convención de llamada stdcall y cdecl. Aunque stdcall y cdecl se diferencian en la forma de extraer los parámetros de una pila, el entorno de ejecución de MQL5 puede usar ambas versiones de forma segura debido a la envolvente especial de las llamadas de las DLL.

El compilador C++ usa la llamada __cdecl por defecto pero recomiendo explícitamente especificar el modo __stdcall para las funciones exportadas.

Una función de exportación escrita correctamente debe tener la siguiente forma:

_DLLAPI int __stdcall fnCalculateSpeed(int &res1,double &res2)
  {
   return(0);
  }

En un programa MQL5 la función debe ser definida y llamada de la siguiente forma:

#import "MQL5DLLSamples.dll"
int  fnCalculateSpeed(int &res1,double &res2);
#import

//--- call
   speed=fnCalculateSpeed(res_int,res_double);

Después de la compilación del proyecto stdcall se mostrará en la tabla de exportación como _fnCalculateSpeed@8, donde el compilador añade un subrayado y un número de bytes transmitidos a través de la pila. Esta decoración nos permite controlar mejor las llamadas de las funciones DLL debido al hecho de que el que llama sabe exactamente cuántos (¡pero no el tipo!) datos deben situarse en la pila.

Si el tamaño final del bloque del parámetro tiene un error en la descripción de importación de la función DLL, la función no será llamada y aparecerá un nuevo mensaje en el diario: 'No se ha podido encontrar 'fnCrashTestParametersStdCall' en 'MQL5DLLSamples.dll'. En estos casos es necesario verificar cuidadosamente todos los parámetros, tanto en el prototipo de la función como en la fuente de la DLL.

La búsqueda de la descripción simplificada sin decoración se utiliza para cuestiones de compatibilidad en caso de que la tabla de exportación no contenga el nombre completo de la función. Nombres como fnCalculateSpeed son creados si las funciones son definidas en el formato __cdecl.
_DLLAPI int fnCalculateSpeed(int &res1,double &res2)
  {
   return(0);
  }


3. Métodos para pasar parámetros e intercambiar datos

Vamos a considerar varias variantes de parámetros pasados:

  1. Recibir y pasar variables simples
    El caso de las variables simples es sencillo, ya que estas pueden pasarse por valor o por referencia usando &.
    _DLLAPI int __stdcall fnCalculateSpeed(int &res1,double &res2)
      {
       int    res_int=0;
       double res_double=0.0;
       int    start=GetTickCount();
    //--- simple math calculations
       for(int i=0;i<=10000000;i++)
         {
          res_int+=i*i;
          res_int++;
          res_double+=i*i;
          res_double++;
         }
    //--- set calculation results
       res1=res_int;
       res2=res_double;
    //--- return calculation time 
       return(GetTickCount()-start);
      }
        
    Llamada de MQL5:
    #import "MQL5DLLSamples.dll"
    int  fnCalculateSpeed(int &res1,double &res2);
    #import
    
    //--- calling the function for calculations
       int    speed=0;
       int    res_int=0;
       double res_double=0.0;
    
       speed=fnCalculateSpeed(res_int,res_double);
       Print("Time ",speed," msec, int: ",res_int," double: ",res_double);
    
    La salida es:
    MQL5DLL Test (GBPUSD,M1) 19:56:42 Time  16  msec, int:  -752584127  double:  17247836076609
  2. Recibir y pasar una matriz con llenado de elementos

    A diferencia de otros programas de MQL5, las matrices se pasan mediante una referencia directa al buffer de datos sin acceder a la información propietaria sobre dimensiones y tamaños. Por eso deben pasarse por separado las dimensiones y el tamaño de la matriz.

    _DLLAPI void __stdcall fnFillArray(int *arr,const int arr_size)
      {
    //--- check for the input parameters
       if(arr==NULL || arr_size<1) return;
    //--- fill array with values
       for(int i=0;i<arr_size;i++) arr[i]=i;
      }
        
    Llamada de MQL5:
    #import "MQL5DLLSamples.dll"
    void fnFillArray(int &arr[],int arr_size);
    #import
    
    //--- call for the array filling
       int    arr[];
       string result="Array: "; 
       ArrayResize(arr,10);
       
       fnFillArray(arr,ArraySize(arr));
       for(int i=0;i<ArraySize(arr);i++) result=result+IntegerToString(arr[i])+" ";
       Print(result);
    
    La salida es:
    MQL5DLL Test (GBPUSD,M1) 20:31:12 Array: 0 1 2 3 4 5 6 7 8 9 
  3. Pasar y modificar strings
    Los strings Unicode se pasan usando referencias directas a las direcciones de sus buffers sin pasar ninguna información adicional.
    _DLLAPI void fnReplaceString(wchar_t *text,wchar_t *from,wchar_t *to)
      {
       wchar_t *cp;
    //--- parameters check
       if(text==NULL || from==NULL || to==NULL) return;
       if(wcslen(from)!=wcslen(to))             return;
    //--- search for substring
       if((cp=wcsstr(text,from))==NULL)         return;
    //--- replace it
       memcpy(cp,to,wcslen(to)*sizeof(wchar_t));
      }
    
    Llamada de MQL5:
    #import "MQL5DLLSamples.dll"
    void fnReplaceString(string text,string from,string to);
    #import
    
    //--- modify the string
       string text="A quick brown fox jumps over the lazy dog"; 
       
       fnReplaceString(text,"fox","cat");
       Print("Replace: ",text);
    El resultado es:
    MQL5DLL Test (GBPUSD,M1) 19:56:42 Replace:  Un rápido zorro marrón salta sobre el perezoso perro
    ¡Resulta que la línea no ha cambiado! Este es un error común de principiante al transmitir copias de objetos (un string como objeto), en lugar de referirse a ellos. La copia del string "text" se ha creado automáticamente y ha sido modificada en la DLL y a continuación se ha eliminado automáticamente sin afectar al original.

    Para resolver esta situación es necesario pasar un string por referencia. Para hacer esto modificamos simplemente el bloque de importar añadiendo & al parámetro "text".
    #import "MQL5DLLSamples.dll"
    void fnReplaceString(string &text,string from,string to);
    #import
    Tras compilar e iniciar obtendremos el resultado correcto:
    MQL5DLL Test (GBPUSD,M1) 19:58:31 Replace:  Un rápido gato marrón salta sobre el perezoso perro


4. Detectar excepciones en funciones DLL

Para evitar el fallo total del terminal, cada llamada DLL se protege automáticamente con una envolvente de excepción no controlada. Este mecanismo permite la protección frente a los errores más habituales (errores de acceso a memoria, división por cero, etc.)

Para ver cómo funciona el mecanismo vamos a crear el siguiente código:

_DLLAPI void __stdcall fnCrashTest(int *arr)
  {
//--- wait for receipt of a zero reference to call the exception
   *arr=0;
  }

y lo llamamos desde el terminal de cliente:

#import "MQL5DLLSamples.dll"
void fnCrashTest(int arr);
#import

//--- call for the crash (the execution environment will catch the exception and prevent the client terminal crush)
   fnCrashTest(NULL);
   Print("You won't see this text!");
//---

Como resultado, intentará escribir en la dirección cero y generar una excepción. El terminal de cliente lo detectará, lo registrará en el diario y continuará su trabajo:

MQL5DLL Test (GBPUSD,M1) 20:31:12 Access violation write to 0x00000000


5. Envolvente de llamadas DLL y pérdida de velocidad en las llamadas

Como se ha descrito anteriormente, cada llamada de las funciones DLL se envuelve en una envolvente especial para garantizar la seguridad. El vínculo enmascara el código básico, reemplaza la pila, ayuda en las transacciones stdcall/cdecl y supervisa las excepciones dentro de las funciones llamadas.

Este volumen de trabajo no supone un retraso significativo de la llamada de la función.


6. La construcción final

Vamos a recopilar todos los ejemplos de funciones DLL anteriores en el archivo 'MQL5DLLSamples.cpp' y los ejemplos de MQL5 en el script 'MQL5DLL Test.mq5'. El proyecto final para Visual Studio 2008 y el script en MQL5 van adjuntos al archivo.

//+------------------------------------------------------------------+
//|                                                 MQL5 DLL Samples |
//|                   Copyright 2001-2010, MetaQuotes Software Corp. |
//|                                        https://www.metaquotes.net |
//+------------------------------------------------------------------+
#include "stdafx.h"

//+------------------------------------------------------------------+
//| Passing and receving of simple variables                         |
//+------------------------------------------------------------------+
_DLLAPI int __stdcall fnCalculateSpeed(int &res1,double &res2)
  {
   int    res_int=0;
   double res_double=0.0;
   int    start=GetTickCount();
//--- simple math calculations
   for(int i=0;i<=10000000;i++)
     {
      res_int+=i*i;
      res_int++;
      res_double+=i*i;
      res_double++;
     }
//--- set calculation results
   res1=res_int;
   res2=res_double;
//--- return calculation time
   return(GetTickCount()-start);
  }
//+------------------------------------------------------------------+
//| Filling the array with values                                    |
//+------------------------------------------------------------------+
_DLLAPI void __stdcall fnFillArray(int *arr,const int arr_size)
  {
//--- check input variables
   if(arr==NULL || arr_size<1) return;
//--- fill array with values
   for(int i=0;i<arr_size;i++) arr[i]=i;
  }
//+------------------------------------------------------------------+
//| The substring replacement of the text string                     |
//| the string is passed as direct reference to the string content   |
//+------------------------------------------------------------------+
_DLLAPI void fnReplaceString(wchar_t *text,wchar_t *from,wchar_t *to)
  {
   wchar_t *cp;
//--- parameters checking
   if(text==NULL || from==NULL || to==NULL) return;
   if(wcslen(from)!=wcslen(to))             return;
//--- search for substring
   if((cp=wcsstr(text,from))==NULL)         return;
//--- replace it 
   memcpy(cp,to,wcslen(to)*sizeof(wchar_t));
  }
//+------------------------------------------------------------------+
//| Call for the crush                                               |
//+------------------------------------------------------------------+
_DLLAPI void __stdcall fnCrashTest(int *arr)
  {
//--- wait for receipt of a zero reference to call the exception
   *arr=0;
  }
//+------------------------------------------------------------------+

//+------------------------------------------------------------------+
//|                                                 MQL5DLL Test.mq5 |
//|                        Copyright 2010, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2010, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
//---
#import "MQL5DLLSamples.dll"
int  fnCalculateSpeed(int &res1,double &res2);
void fnFillArray(int &arr[],int arr_size);
void fnReplaceString(string text,string from,string to);
void fnCrashTest(int arr);
#import

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
//--- calling the function for calculations
   int    speed=0;
   int    res_int=0;
   double res_double=0.0;

   speed=fnCalculateSpeed(res_int,res_double);
   Print("Time ",speed," msec, int: ",res_int," double: ",res_double);
//--- call for the array filling
   int    arr[];
   string result="Array: "; 
   ArrayResize(arr,10);
   
   fnFillArray(arr,ArraySize(arr));
   for(int i=0;i<ArraySize(arr);i++) result=result+IntegerToString(arr[i])+" ";
   Print(result);
//--- modifying the string
   string text="A quick brown fox jumps over the lazy dog"; 
   
   fnReplaceString(text,"fox","cat");
   Print("Replace: ",text);
//--- and finally call a crash
//--- (the execution environment will catch the exception and prevent the client terminal crush)
   fnCrashTest(NULL);
   Print("You won't see this text!");
//---
  }
//+------------------------------------------------------------------+

¡Gracias por su interés! Estoy listo para responder a sus preguntas.