Руководство по написанию DLL для MQL5 на Delphi

Andriy Voitenko | 26 мая, 2010

Введение

Механизм написания DLL будет рассмотрен на примере среды разработки Delphi 2009. Выбор именно этой версии обусловлен тем, что в MQL5 строки хранятся в формате Юникод. А в более старых версиях Delphi в модуле SysUtils отсутствуют функции для работы со строками Юникод формата.

Если вы, по каким-то причинам, используете более раннюю версию (Delphi 2007 и ниже), то вам придется работать со строками в ANSI формате, а для обмена данными с MetaTrader 5 производить прямое и обратное преобразование в Юникод. Дабы избежать таких сложностей рекомендую разрабатывать DLL модуль для MQL5 в среде не ниже чем Delphi 2009. Ознакомительную, 30-и дневную версию Delphi можно скачать с официального сайта http://embarcadero.com/

1. Создание проекта

Для созданиия проекта необходимо запустить DLL Wizard выбрав пункт меню: 'File -> New -> Other... -> DLL Wizard', как показано на рисунке 1.

Рисунок 1. Создание проекта с помощью DLL Wizard

В результате, будет создан пустой проект DLL, как показано на рисунке 2.


Рисунок 2. Пустой проект DLL

Суть длинного комментария в заголовке проекта сводится к напоминанию о правильном  подключении и использовании менеджера памяти при работе с динамически выделяемой памятью. Об этом более подробно будет рассказано в разделе, посвященному работе со строками.

Перед тем как приступить к наполнению DLL новыми функциями, важно настроить проект.

Вызовите окно свойств проекта через меню: 'Project -> Options...' или через сочетание клавиш 'Shift+Ctrl+F11'.

В целях упрощения отладки, необходимо, чтобы файл DLL создавался непосредственно в папке '..\MQL5\Libraries' торгового терминала MetaTrtader5. Для этого на закладке DelphiCompiler задайте соответствующее значение свойства Output directory, как показано на рисунке 3. Это избавит вас от необходимости постоянного копирования файла создаваемой DLL из папки проекта в папку терминала.

 Рисунок 3. Задание папки для размещения результирующего файла DLL

Чтобы в процессе сборки не подключались модули семейства BPL, без наличия которых, в системной папке Windows, создаваемая DLL в будущем не сможет работать,  важно проконтролировать, чтобы на вкладке Packages, флаг Build with runtime packages был снят, как показано на рисунке 4.

  Рисунок 4. Исключение из сборки, модулей семейства BPL

После настройки проекта, сохраните его в вашу рабочую папку, учитывая, что задаваемое имя проекта является будущим именем скомпилированного DLL файла.

2. Добавление процедур и функций 

Рассмотрим общие положения при написании экспортируемых процедур и функций в модуле DLL, на  примере процедуры без параметров. Объявление и передача параметров будет рассмотрена в следующем разделе.

Небольшое отступление. При написании процедур и функций на языке Object Pascal в распоряжение программиста появляется возможность использовать встроенные библиотечные функции Delphi, не говоря о бессчетном множестве компонентов разработанных для этой среды. Например, для выполнения одного и того же действия, вывода на экран модального окна с текстовым сообщением, можно использовать как API функцию - MessageBox так и процедуру из VCL библиотеки - ShowMessage.

Второй вариант предполагает подключения модуля Dialogs, давая преимущество простой работы со стандартными диалоговыми окнами Windows. Однако, размер результирующего DLL файла увеличится примерно на 500 Кб. Поэтому, если вы сторонник создавать маленькие, по занимаемому дисковому пространству, DLL файлы, вам придется отказаться от использования VCL.

Ниже приведен пример тестового проекта с пояснениями:

library dll_mql5;

uses
  Windows, // необходим для работы функции MessageBox
  Dialogs; // необходим для работы процедуры ShowMessage из модуля Dialogs

var   Buffer: PWideChar;
//------------------------------------------------------+
procedure MsgBox(); stdcall; // во избежание ошибок, следует использовать соглашение stdcall (или cdecl) для экспортируемых функций
//------------------------------------------------------+
begin
    {1} MessageBox(0,'Hello World!','terminal', MB_OK);
    {2} ShowMessage('Hello World!');// альтернатива функции MessageBox
end;

//----------------------------------------------------------+
exports
//----------------------------------------------------------+
  {A} MsgBox,
  {B} MsgBox name 'MessageBox';// переименование экспортируемой функции


//----------------------------------------------------------+
procedure DLLEntryPoint(dwReason: DWord); // обработчик событий
//----------------------------------------------------------+
begin
    case dwReason of

      DLL_PROCESS_ATTACH: // DLL присоединена к процессу;
          // выделение памяти
          Buffer:=AllocMem(BUFFER_SIZE);

      DLL_PROCESS_DETACH: // DLL отсоединена от процесса;
          // освобождение памяти
          FreeMem(Buffer);

    end;
end;

//----------------------------------------------------------+
begin
    DllProc := @DLLEntryPoint; //Назначение процедуры обработки событий
    DLLEntryPoint(DLL_PROCESS_ATTACH);
end.
//----------------------------------------------------------+

Все экспортируемые функции должны быть объявлены с модификатором stdcall или cdecl. Если ни один из этих модификаторов не указан, по умолчанию Delphi использует соглашение fastcall в первую очередь использующее для передачи параметров не стек, а регистры процессора, что без сомнения приведет к ошибке в работе с передаваемыми параметрами на этапе вызова внешних функции DLL.

Секция begin end содержит стандартный код инициализации обработчика событий DLL. Callback-процедура DLLEntryPoint будет вызвана при присоединении и отсоединении от вызвавшего её процесса. Эти события можно использовать для корректного управления динамической памятью, выделяемой для собственных нужд, как показано в примере.

Вызов для MQL5:

#import "dll_mql5.dll"
    void MsgBox(void);
    void MessageBox(void);
#import

// Вызов процедуры
   MsgBox();
// При совпадении имен функций DLL с функциями стандартной библиотеки MQL5, при вызове DLL функций важно указать имя DLL.
   dll_mql5::MessageBox();

3. Передача параметров в функцию и возвращаемые значения

Прежде чем рассматривать передачу параметров, проанализируем таблицу соответствия типов данных для MQL5 и Object Pascal.

Тип данных MQL5
Тип данных Object Pascal (Delphi)
Примечание
charShortInt
uchar
Byte
short
SmallInt
ushort
Word

int
Integer
 
uintCardinal
longInt64
ulong
UInt64

floatSingle
doubleDouble

ushort (символ)WideChar
stringPWideChar 
boolBoolean 
datetimeTDateTimeтребуется преобразование (см. ниже в этом разделе)
colorTColor 

Таблица 1. Соответствие типов данных для MQL5 и Object Pascal

Как видно из таблицы, для всех типов данных кроме datetime, в Delphi существует полный аналог.

Теперь рассмотрим два способа передачи параметров: по значению и по ссылке. Формат объявления параметров для обоих вариантов приведен в таблице 2.

Способ передачи параметров
Объявление для MQL5
Объявление для Delphi
Примечание 
по значению
int func (int a);func (a:Integer): Integer; правильно

int func (int a);
func (var a: Integer): Integer;
 ошибка: access violation write to <memory address>
по ссылке
int func (int &a);
func (var a: Integer): Integer;
 правильно, однако строки передаются без модификатора var!
 int func (int &a); func (a: Integer): Integer; ошибка: вместо значения переменная содержит адрес ячейки памяти

 Таблица 2. Способы передачи параметров

Теперь перейдем к рассмотрению примеров работы с передаваемыми параметрами и возвращаемыми значениями.

3.1 Преобразование даты и времени

Вначале, разберемся с типом даты и времени, который необходимо конвертировать, т.к. тип datetime соответствует TDateTime лишь по размеру, но не по формату. Для удобства преобразования, в качестве принимаемого типа данных вместо TDateTime используйте Int64. Ниже приведены функции для прямого и обратного преобразования:

uses 
    SysUtils,  // используется для константы UnixDateDelta 
    DateUtils; // используется для функции IncSecon, DateTimeToUnix

//----------------------------------------------------------+
Function MQL5_Time_To_TDateTime(dt: Int64): TDateTime;
//----------------------------------------------------------+
begin
      Result:= IncSecond(UnixDateDelta, dt);
end;

//----------------------------------------------------------+
Function TDateTime_To_MQL5_Time(dt: TDateTime):Int64;
//----------------------------------------------------------+
begin
      Result:= DateTimeToUnix(dt);
end;

3.2 Работа с простыми типами данных

Посмотрим, как передавать простые типы данных на примере наиболее востребованных int, double, bool и datetime.

Вызов для Object Pascal:

//----------------------------------------------------------+
function SetParam(var i: Integer; d: Double; const b: Boolean; var dt: Int64): PWideChar; stdcall;
//----------------------------------------------------------+
begin
  if (b) then d:=0;                   // значение переменной d не меняется в вызывающей программе
  i:= 10;                             // задаём новое значение для i
  dt:= TDateTime_To_MQL5_Time(Now()); // задаём текущее время для dt
  Result:= 'Значения переменных i и dt изменены';
end;

Вызов для MQL5:

#import "dll_mql5.dll"
    string SetParam(int &i, double d, bool b, datetime &dt);
#import

// инициализация переменных
   int i = 5;
   double d = 2.8;
   bool b = true;
   datetime dt= D'05.05.2010 08:31:27';
// вызов функции
   s=SetParam(i,d,b,dt);
// вывод результата
   printf("%s i=%s d=%s b=%s dt=%s",s,IntegerToString(i),DoubleToString(d),b?"true":"false",TimeToString(dt));
Результат: 
Значения переменных i и dt изменены i=10 d=2.80000000 b=true dt=2009.05.05 08:42 

Значение переменной d не претерпело изменения т.к. она передавалась по значению. Для защиты от изменения значения переменной внутри функции DLL к переменной b был применен модификатор const.

3.3 Работа со структурами и массивами

В ряде случаев, целесообразно группировать передаваемые параметры разного типа в структуры, а параметры одного типа в массивы. Рассмотрим работу со всеми передаваемыми параметрами функции SetParam, из предыдущего примера, объединив их в структуру.

Вызов для Object Pascal: 

type
StructData = packed record
    i: Integer;
    d: Double;
    b: Boolean;
    dt: Int64;
  end;

//----------------------------------------------------------+
function SetStruct(var data: StructData): PWideChar; stdcall;
//----------------------------------------------------------+
begin
  if (data.b) then data.d:=0;
  data.i:= 10;                                 // задаём новое значение для i
  data.dt:= TDateTime_To_MQL5_Time(Now()); // задаём текущее время для dt
  Result:= 'Значения переменных i, d и dt изменены';
end

Вызов для MQL5:  

struct STRUCT_DATA
  {
   int i;
   double d;
   bool b;
   datetime dt;
  };

#import "dll_mql5.dll"
    string SetStruct(STRUCT_DATA &data);
#import

   STRUCT_DATA data;
   
   data.i = 5;
   data.d = 2.8;
   data.b = true;
   data.dt = D'05.05.2010 08:31:27';
   s = SetStruct(data);
   printf("%s i=%s d=%s b=%s dt=%s", s, IntegerToString(data.i),DoubleToString(data.d), 
             data.b?"true":"false",TimeToString(data.dt));
Результат:
Значения переменных i, d и dt изменены i=10 d=0.00000000 b=true dt=2009.05.05 12:19

Нельзя не отметить одного существенного отличия от результата предыдущего примера. Т.к. структура передается по ссылке, это делает невозможным защитить выборочные поля структуры от редактирования в вызываемой функции. Задача контроля целостности данных, в этом случае, лежит на плечах программиста.

Рассмотрим работу с массивами, на примере заполнения массива последовательностью чисел Фибоначчи:

Вызов для Object Pascal:

//----------------------------------------------------------+
function SetArray(var arr: IntegerArray; const len: Cardinal): PWideChar; stdcall;
//----------------------------------------------------------+
var i:Integer;
begin
  Result:='Числа Фибоначчи:';
  if (len < 3) then exit;
  arr[0]:= 0;
  arr[1]:= 1;
  for i := 2 to len-1 do
    arr[i]:= arr[i-1] + arr[i-2];
end;

Вызов для MQL5: 

#import "dll_mql5.dll"
    string SetArray(int &arr[],int len);
#import
   
   int arr[12];
   int len = ArraySize(arr);
// передача массива по ссылке для наполнения данными в DLL
   s = SetArray(arr,len);
//вывод результата
   for(int i=0; i<len; i++) s = s + " " + IntegerToString(arr[i]);
   printf(s);
Результат:  
Числа Фибоначчи 0 1 1 2 3 5 8 13 21 34 55 89

3.4 Работа со строками

Вернемся к рассмотрению вопроса об управлении памятью. В DLL предусмотрена работа собственного менеджера памяти. Однако т.к. DLL и программа её вызывающая зачастую написаны на разных языках программирования, и используются в работе собственные, а не общий системный менеджер памяти, то весь груз ответственности за корректную работу с памятью на стыке DLL и приложения лежит на программисте.

Для работы с памятью, важно соблюдать золотое правило, которое звучит примерно так: “Тот, кто выделил память, тот и должен её освободить”.  Т.е. вы не должны пытаться освобождать память в коде mql5-программы выделенной в DLL, и наоборот.

Чтобы уйти от работы с менеджером памяти в сторону самостоятельного контроля над её выделением и освобождением, важно, как говориться в комментарии к проекту DLL, научится использовать тип PWideChar таким образом, как он используется в API-функциях. В нашем случае mql5-программа выделяет память для буфера, указатель на этот буфер передаёт в DLL как PWideChar, а DLL только заносит в этот буфер требуемое значение, как показано в следующем примере:

Вызов для Object Pascal:

//----------------------------------------------------------+
procedure SetString(const str:PWideChar) stdcall;
//----------------------------------------------------------+
begin
  StrCat(str,'Текущее время:');
  strCat(str, PWideChar(TimeToStr(Now)));
end;

Вызов для MQL5: 

#import "dll_mql5.dll"
    void SetString(string &a);
#import

// перед использованием строка должна быть проинициализирована
// размер буфера должен быть заведомо больше или равен длине принимаемой строки
   StringInit(s,255,0); 
//передача строкового буфера в DLL 
   SetString(s);
// вывод результата 
   printf(s);

Результат:

Текущее время: 11:48:51

Память для строкового буфера можно выделить и в DLL несколькими способами, как видно из следующего примера:

Вызов для Object Pascal: 

//----------------------------------------------------------+
function GetStringBuffer():PWideChar; stdcall;
//----------------------------------------------------------+
var StrLocal: WideString;
begin
     // работа через динамически выделенный буффер памяти
     StrPCopy(Buffer, WideFormat('Текущая дата и время: %s', [DateTimeToStr(Now)]));
     // работа через глобальную переменную типа WideString
     StrGlobal:=WideFormat('Текущее время: %s', [TimeToStr(Time)]);
     // работа через локальную переменную типа WideString
     StrLocal:= WideFormat('Текущая дата: %s', [DateToStr(Date)]);

{A}  Result := Buffer;

{B}  Result := PWideChar(StrGlobal);
     // это равносильно следующей записи
     Result := @StrGlobal[1];

{С}  Result := 'Возврат строки хранящейся в секции кода';

     // ссылка на память которая может быть освобождена при выходе из функции
{D}  Result := @StrLocal[1];
end;
Вызов для MQL5: 
#import "dll_mql5.dll"
    string GetStringBuffer(void);
#import

   printf(GetStringBuffer());

 Результат: 

Current Date: 19.05.2010

Что характерно, работают все четыре варианта. В первых двух вариантах работа со строкой ведется через глобально выделенную память.

В варианте А память распределяется самостоятельно а в варианте B работа по управлению памятью берет на себя менеджер памяти.

В варианте С строковая константа хранится не в памяти а в сегменте кода, поэтому менеджер памяти не выделяет для её хранения объем динамической памяти.  Вариант D является грубейшей ошибкой программирования, т.к. память выделенная под локальную переменную может быть освобождена сразу после выхода из функции.

И хотя менеджер памяти не освобождает эту память мгновенно и она не успевает быть заполненной мусором, советую последний вариант исключить из употребления.

3.5 Использование параметров по умолчанию

Поговорим об использовании необязательных параметров. Они интересны тем, что их значения можно не указывать при вызове процедур и функций. При этом, они должны быть описаны строго после всех объязательных параметров в обьявлении процедур и функций, как показано в следующем примере:

Вызов для Object Pascal: 

//----------------------------------------------------------+
function SetOptional(var a:Integer; b:Integer=0):PWideChar; stdcall;
//----------------------------------------------------------+
begin
    if (b=0) then Result:='Вызов с параметром по умолчанию'
    else          Result:='Вызов без параметров по умолчанию';
end;
Вызов для MQL5:
#import "dll_mql5.dll"
    string SetOptional(int &a, int b=0);
#import

  i = 1;
  s = SetOptional(i); // второй параметр является необязательным
  printf(s);

Результат: 

Вызов с параметром по умолчанию

Для удобства отладки, код с приведенными выше примерами организован в виде скрипта и находится в файле Testing_DLL. mq5.

4. Возможные ошибки на этапе разработки

Ошибка: DLL Loading is not allowed.

Решение: Зайдя в настройки MetaTrader 5 через меню 'Сервис -> Настройки' необходимо разрешить импорт функций DLL, как показано на рисунке 5.

 

 Рисунок 5. Разрешение импорта функций DLL

Ошибка: Cannot find 'function name' in 'DLL name'.
Решение: Проверьте, указана ли вызываемая функция в разделе Exports проекта DLL. Если да, то стоит проверить полное совпадение имени функции в DLL и mql5-программе с учетом регистра символов!

Ошибка: access violation write to [memory address]
Решение: Необходимо проверить правильность описания передаваемых параметров (смотри таблицу 2). Т.к. чаще всего данная ошибка связана с обработкой строк, важно следовать рекомендациям по работе со строками изложенным в пункте 3.4 данной статьи.

5. Пример кода DLL

В качестве наглядного примера использования DLL, рассмотрим расчет параметров канала регрессии состоящего из трех прямых. Для проверки правильности построения канала будем использовать встроенный объект "Канал регрессии". Расчет аппроксимирующей прямой по МНК (метод наименьших квадратов) взят с сайта http://alglib.sources.ru/,  где находится коллекция алгоритмов по обработке данных. Код алгоритмов представлен на нескольких языках программирования включая Delphi.

Для расчета коэффициентов a и b, аппроксимирующей прямой y=a+b*x, используется процедура LRLine описанная в файле linreg.pas.

procedure LRLine(const XY : TReal2DArray;   // двумерный массив  вещественных  чисел для X и Y координат
                       N : AlglibInteger;  // число точек
                 var Info : AlglibInteger; // статус преобразования
                   var A : Double;         // коэффициенты аппроксимирующей прямой
                    var B : Double);
  

Для расчета параметров канала используется функция CalcLRChannel.

Вызов для Object Pascal:  

//----------------------------------------------------------+
function CalcLRChannel(var rates: DoubleArray; const len: Integer;
                          var A, B, max: Double):Integer; stdcall;
//----------------------------------------------------------+
var arr: TReal2DArray;
    info: Integer;
    value: Double;
begin

    SetLength(arr,len,2);
    // копирование данных в двумерный массив
    for info:= 0 to len - 1 do
    begin
      arr[info,0]:= rates[info,0];
      arr[info,1]:= rates[info,1];
    end;

    // процедура расчета коэффициентов аппроксимирующей прямой
    LRLine(arr, len, info, A,  B);

    // нахождение максимального отклонения от аппроксимирующей прямой
    // определение ширины канала
    max:= rates[0,1] - A;
    for info := 1 to len - 1 do
    begin
      value:= Abs(rates[info,1]- (A + B*info));
      if (value > max) then max := value;
    end;

    Result:=0;
end;

Вызов для MQL5:

#import "dll_mql5.dll"
    int CalcLRChannel(double &rates[][2],int len,double &A,double &B,double &max);
#import

   double arr[][2], //массив данных для обработки в формате ALGLIB
              a,b, //коэффициенты аппроксимирующей прямой 
              max; //максимальное отклонение цены от аппроксимирующей линии равное половине ширины канала
   
   int len = period; //количество точек для расчета
   ArrayResize(arr,len);

// копирование истории в двумерный массив
   int j=0;
   for(int i=rates_total-1; i>=rates_total-len; i--)
     {
      arr[j][0] = j;
      arr[j][1] = close[i];
      j++;
     }

// расчет параметров канала
   CalcLRChannel(arr,len,a,b,max);

Код индикатора, использующий в расчетах функцию CalcLRChannel, находится в файле LR_Channel.mq5 и приведен ниже: 

//+------------------------------------------------------------------+
//|                                                   LR_Channel.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              https://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "https://www.mql5.com"
#property version   "1.00"
#property indicator_chart_window

#include <Charts\Chart.mqh>
#include <ChartObjects\ChartObjectsChannels.mqh>

#import "dll_mql5.dll"
int CalcLRChannel(double &rates[][2],int len,double &A,double &B,double &max);
#import

input int period=75;

CChart               *chart;
CChartObjectChannel  *line_up,*line_dn,*line_md;
double                arr[][2];
//+------------------------------------------------------------------+
int OnInit()
//+------------------------------------------------------------------+
  {

   if((chart=new CChart)==NULL)
     {printf("Chart not created"); return(false);}

   chart.Attach();
   if(chart.ChartId()==0)
     {printf("Chart not opened");return(false);}

   if((line_up=new CChartObjectChannel)==NULL)
     {printf("Channel not created"); return(false);}

   if((line_dn=new CChartObjectChannel)==NULL)
     {printf("Channel not created"); return(false);}

   if((line_md=new CChartObjectChannel)==NULL)
     {printf("Channel not created"); return(false);}

   return(0);
  }
//+------------------------------------------------------------------+
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[])
//+------------------------------------------------------------------+
  {

   double a,b,max;
   static double save_max;
   int len=period;

   ArrayResize(arr,len);

// копирование истории в двумерный массив
   int j=0;
   for(int i=rates_total-1; i>=rates_total-len; i--)
     {
      arr[j][0] = j;
      arr[j][1] = close[i];
      j++;
     }

// процедура расчета параметров канала
   CalcLRChannel(arr,len,a,b,max);

// если ширина канала изменилась
   if(max!=save_max)
     {
      save_max=max;

      // Удаление канала
      line_md.Delete();
      line_up.Delete();
      line_dn.Delete();

      // Создание канала с новыми координатами
      line_md.Create(chart.ChartId(),"LR_Md_Line",0, time[rates_total-1],     a, time[rates_total-len], a+b*(len-1)    );
      line_up.Create(chart.ChartId(),"LR_Up_Line",0, time[rates_total-1], a+max, time[rates_total-len], a+b*(len-1)+max);
      line_dn.Create(chart.ChartId(),"LR_Dn_Line",0, time[rates_total-1], a-max, time[rates_total-len], a+b*(len-1)-max);

      // задание цвета линий канала     
      line_up.Color(RoyalBlue);
      line_dn.Color(RoyalBlue);
      line_md.Color(RoyalBlue);

      // задание толщины линий
      line_up.Width(2);
      line_dn.Width(2);
      line_md.Width(2);
     }

   return(len);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
//+------------------------------------------------------------------+
  {
// Удаление созданных объектов
   chart.Detach();

   delete line_dn;
   delete line_up;
   delete line_md;
   delete chart;
  }

Результатом работы индикатора является построение канала регрессии голубого цвета, как показано на рисунке 6. Для проверки правильности построения канала, на график нанесен "Канал регрессии", из штатного арсенала инструментов технического анализа Metatrader 5, показанный красным цветом.

Как видно из рисунка, центральные линии канала сливаются. При этом существует небольшое различие в ширине канала (несколько пунктов), что объясняется разным подходом к её расчету. 

 Рисунок 6. Сравнение каналов регрессии

Рисунок 6. Сравнение каналов регрессии

Заключение

В статье рассмотрены особенности написания DLL с использованием платформы разработки приложений Delphi.