Download MetaTrader 5

Guide to writing a DLL for MQL5 in Delphi

25 June 2010, 13:49
Andrey Voytenko
2
6 168

Introduction

The mechanism of writing a DLL will be considered using an example of the development environment of Delphi 2009. This version was selected due to the fact that in MQL5, all lines are stored in Unicode format. In older versions of Delphi, the SysUtils module is missing the function for working with lines in Unicode format.

If you, for whatever reason, are using an earlier version (Delphi 2007 and older), then you have to work with lines in ANSI format, and in order to exchange data with MetaTrader 5, you need to produce direct and reverse conversions to Unicode. To avoid such complications I recommend developing the DLL module for MQL5 in an environment no older than Delphi 2009. A familiarizing 30-day trial version for Delphi can be downloaded from the official website http://embarcadero.com.

1. Creating the Project

To create the project we need to run DLL Wizard by choosing the menu item: 'File -> New -> Other ... -> DLL Wizard ' As shown at Figure 1.

Figure 1. Creating a project using DLL Wizard

As a result, we will create an empty DLL project, as shown at Figure 2.


Figure 2. An empty DLL project

The essence of a long commentary in the title of the project is to remind you of a correct connection and the use of a memory manager when working with dynamically allocated memory. This will be discussed in more detail in the section dealing with strings.

Before you begin filling the new DLL with functions, it is important to configure the project.

Open the project properties window from the menu: 'Project -> Options ...' or via the keyboard 'Shift + Ctrl + F11' .

In order to simplify the process of debugging, it is necessary that the DLL file is created directly in the folder '.. \\MQL5\\Libraries' Trade Terminal MetaTrtader5. To do this, on the DelphiCompiler tab set the corresponding property value Output directory , as shown at Figure 3. It will eliminates the need to constantly copy the file, generated by DLL, from the project folder into the folder of the terminal.

 Figure 3. Specify the folder for storing the resulting DLL file

In order to avoid the hooking up of BPL modules during the assembly, without the presence of which in the Windows system folder, the created DLL will not be work in the future, it is important to check that on the tab Packages , the flag Build with runtime packages is unchecked, as shown at Figure 4.

  Figure 4. Exclusion of the BPL modules from the assembly

After completing project configuration, save it to your working folder, the specified name of the project is the future name of the compiled DLL file.

2. Adding the procedures and functions 

Lets consider the general situation when writing the exported procedures and functions in the DLL module, on an example of a procedure without parameters. Announcement and transfer of parameters will be discussed in the next section.

A small digression. When writing the procedures and functions in the Object Pascal language, the programmer has the opportunity to use the built-in library Delphi functions, not to mention the countless components, developed for this environment. For example, for the performance of the same action, like bringing up a display of a modal window with a text message, you can use as an API function - MessageBox as well as a procedure from the VCL library - ShowMessage.

The second option leads to including the Dialogs module and gives the advantage of easy work with standard Windows dialogs. However, the size of the resulting DLL file will increase by approximately 500 KB. Therefore, if you prefer to create small DLL files, which do not take up a lot of disk space, I would not advise you to use the VCL components.

A sample test project with explanations is below:

library dll_mql5;

uses
  Windows, // necessary for the work of the MessageBox function
  Dialogs; // necessary for the work of the ShowMessage procedure from the Dialogs module

var   Buffer: PWideChar;
//------------------------------------------------------+
procedure MsgBox(); stdcall; // 
//to avoid errors, use the stdcall (or cdecl) for the exported functions
//------------------------------------------------------+
begin
    {1} MessageBox(0,'Hello World!','terminal', MB_OK);
    {2} ShowMessage('Hello World!');// alternative to the MessageBox function 
end;

//----------------------------------------------------------+
exports
//----------------------------------------------------------+
  {A} MsgBox,
  {B} MsgBox name 'MessageBox';// renaming of the exported function


//----------------------------------------------------------+
procedure DLLEntryPoint(dwReason: DWord); // event handler
//----------------------------------------------------------+
begin
    case dwReason of

      DLL_PROCESS_ATTACH: // DLL attached to the process;
          // allocate memory
          Buffer:=AllocMem(BUFFER_SIZE);

      DLL_PROCESS_DETACH: // DLL detached from the process;
          // release memory
          FreeMem(Buffer);

    end;
end;

//----------------------------------------------------------+
begin
    DllProc := @DLLEntryPoint; //Assign event handler
    DLLEntryPoint(DLL_PROCESS_ATTACH);
end.
//----------------------------------------------------------+

All exported functions must be announced with the modifier stdcall or cdecl. If none of these modifiers is specified, Delphi uses the fastcall agreement by default, which primarily uses CPU registers for passing  parameters, rather than stack. It will undoubtedly lead to an error in working with the passed parameters, at the stage of calling up the external functions DLL.

The section "begin end" contains a standard initialization code of a DLL event handler. The DLLEntryPoint callback- procedure will be called when connecting and disconnecting from the process that called it. These events can be used for the correct dynamic memory management, allocated for our own needs, as shown in the example.

Call for MQL5:

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

// Call of procedure
   MsgBox();
// If the names of the function coincide with the names of MQL5 standard library function
// use the DLL name when calling the function
   dll_mql5::MessageBox();

3. Passing Parameters to the Function and Returned Values

Before considering the passing of parameters, let's analyze the data correspondency table for MQL5 and Object Pascal.

Data type for MQL5
Data type for Object Pascal (Delphi)
Note
char ShortInt
uchar
Byte
short
SmallInt
ushort
Word

int
Integer
 
uint Cardinal
long Int64
ulong
UInt64

float Single
double Double

ushort (символ) WideChar
string PWideChar  
bool Boolean  
datetime TDateTime conversion is required (see below in this section)
color TColor  

Table 1. The data corresondency table for MQL5 and Object Pascal

As you can see from the table, for all data types other than datetime, Delphi has a complete analogue.

Now consider two ways of parameters passing: by value and by reference. The format of parameters declaration for both versions is given in Table 2.

Method of transferring parameters
Announcement for MQL5
Announcement for Delphi
Note 
by value
int func (int a); func (a:Integer): Integer;  correct

int func (int a);
func (var a: Integer): Integer;
 Error: access violation write to <memory address>
by link
int func (int &a);
func (var a: Integer): Integer;
 correct, however lines are transmitted without a modifier var!
  int func (int &a);  func (a: Integer): Integer;  error: instead of the value of the variable, contains the address of the memory cell

 Table 2. Methods of parameters passing

Now let us consider the examples of working with passed parameters and returned values.

3.1 Converting date and time

First, let's deal with the type of date and time that you want to convert, because the datetime type corresponds to TDateTime only in its size but not in format. For ease of transformation, use Int64 as the received data type, instead of TDateTime. Below are the functions for direct and inverse transformation:

uses 
    SysUtils,  // used for the constant UnixDateDelta 
    DateUtils; // used for the function 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 Working with simple data types

Let's examine how to transfer simple data types, on the example of the most commonly used ones, int, double, bool, and datetime.

Call for Object Pascal:

//----------------------------------------------------------+
function SetParam(var i: Integer; d: Double; const b: Boolean; var dt: Int64): PWideChar; stdcall;
//----------------------------------------------------------+
begin
  if (b) then d:=0;                   // the value of the variable d is not changed in the calling program
  i:= 10;                             // assign a new value for i
  dt:= TDateTime_To_MQL5_Time(Now()); // assign the current time for dt
  Result:= 'value of variables i and dt are changed';
end;

Call for MQL5:

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

// initialization of variables
   int i = 5;
   double d = 2.8;
   bool b = true;
   datetime dt= D'05.05.2010 08:31:27';
// calling the function
   s=SetParam(i,d,b,dt);
// output of results
   printf("%s i=%s d=%s b=%s dt=%s",s,IntegerToString(i),DoubleToString(d),b?"true":"false",TimeToString(dt));
Result: 
The values of variables i and dt are changed i =  10  d =  2.80000000  b = true dt =  2009.05 .  05  08 :  42 

The value of d has not changed since it was transferred by value. To prevent the occurrence of changes in the value of a variable, inside a DLL function, a modifier const. was used on the variable b.

3.3 Working with structures and arrays

On several occasions, it is useful to group the parameters of different types into structures, and parameters of one type into arrays. Consider working with all of the transferred parameter of the function SetParam, from the previous example, integrating them into a structure.

Call for 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;                                 // assign a new value for i
  data.dt:= TDateTime_To_MQL5_Time(Now()); // assign the current time for dt
  Result:= 'The values of variables i, d and dt are changed';
end

Call for 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));
Result:
The values of variables i,  d  and dt are changed i =  10  d =  0.00000000  b = true dt =  2009.05 .  05  12 :  19 

It's necessary to note one significant difference from the result of the previous example. Since the structure is transferred through a reference, it makes it impossible to protect the selected fields from being edited in the called function. The task of monitoring the integrity of the data, in this case, lies fully upon the programmer.

Consider working with arrays, on an example of filling the array with the sequence of Fibonacci numbers:

Call for Object Pascal:

//----------------------------------------------------------+
function SetArray(var arr: IntegerArray; const len: Cardinal): PWideChar; stdcall;
//----------------------------------------------------------+
var i:Integer;
begin
  Result:='Fibonacci numbers:';
  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;

Call for MQL5: 

#import "dll_mql5.dll"
    string SetArray(int &arr[],int len);
#import
   
   int arr[12];
   int len = ArraySize(arr);
// passing the array by reference to be filled by data in DLL
   s = SetArray(arr,len);
//output of result
   for(int i=0; i<len; i++) s = s + " " + IntegerToString(arr[i]);
   printf(s);
Result:  
Fibonacci numbers 0 1 1 2 3 5 8 13 21 34 55 89

3.4 Working with strings

Let's return to the memory management. In DLL it is possible to operate your own memory manager. However, because DLL and the program which calls it, are often written in different programming languages, and their own memory managers, rather than general system memory, are used in work, the entire burden of responsibility for the correctness of the operation of memory at the junction DLL and the application, rests on the programmer.

To work with the memory, it is important to comply with the golden rule, which sounds something like: "Those who allocated memory, must be the ones to free it." I.e. you should not try to release the memory in the code mql5- program, allocated in DLL, and vice versa.

Lets consider an example of memory management in a style of Windows API function calls. In our case the mql5-program allocates memory for the buffer, a pointer to the buffer passed to DLL as PWideChar and DLL only fills in this buffer with the desired value, as shown in the following example:

Call for Object Pascal:

//----------------------------------------------------------+
procedure SetString(const str:PWideChar) stdcall;
//----------------------------------------------------------+
begin
  StrCat(str,'Current time:');
  strCat(str, PWideChar(TimeToStr(Now)));
end;

Call for MQL5: 

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

// the string must be initialized before the use
// the size of the buffer must be initially larger or equal to the string length
   StringInit(s,255,0); 
//passing the buffer reference to DLL 
   SetString(s);
// output of result 
   printf(s);

Result:

Current Time: 11: 48:51 

Memory for the line buffer can be selected in DLL in several ways, as seen from the following example:

Call for Object Pascal: 

//----------------------------------------------------------+
function GetStringBuffer():PWideChar; stdcall;
//----------------------------------------------------------+
var StrLocal: WideString;
begin
     // working through the dynamically allocated memory buffer
     StrPCopy(Buffer, WideFormat('Current date and time: %s', [DateTimeToStr(Now)]));
     // working through the global varialble of WideString type
     StrGlobal:=WideFormat('Current time: %s', [TimeToStr(Time)]);
     // working through the local varialble of WideString type
     StrLocal:= WideFormat('Current data: %s', [DateToStr(Date)]);

{A}  Result := Buffer;

{B}  Result := PWideChar(StrGlobal);
     // it's equal to the following
     Result := @StrGlobal[1];

{С}  Result := 'Return of the line stored in the code section';

     // pointer to the memory, that can be released when exit from the function
{D}  Result := @StrLocal[1];
end;
Call for MQL5: 
#import "dll_mql5.dll"
    string GetStringBuffer(void);
#import

   printf(GetStringBuffer());

 Result: 

Current Date: 19.05.2010

What is significant is that all four options work. In the first two options, the work with the line is done through a globally allocated memory.

In option A, the memory is allocated independently, and in option B, the work with memory management is assumed by the memory manager.

In option C, the line constant is stored not in the memory, but in the code segment, so the memory manager does not allocate dynamic memory for its storage. Option D is a bold error in programming, because the memory, allocated for the local variable, can be released immediately after exiting the function.

And although the memory manager does not release this memory instantly, and there is no time for it to fill up with trash, I recommend excluding the latter option from use.

3.5 Using the default parameters

Let's talk about the use of optional parameters. They are interesting because their values do not need to be specified when calling up procedures and functions. Meanwhile, they must be described, strictly after all of the obligatory parameters in the declaration of procedures and functions, as shown in the following example:

Call for Object Pascal: 

//----------------------------------------------------------+
function SetOptional(var a:Integer; b:Integer=0):PWideChar; stdcall;
//----------------------------------------------------------+
begin
    if (b=0) then Result:='Call with default parameters'
    else          Result:='Call without default parameters';
end;
Call for MQL5:
#import "dll_mql5.dll"
    string SetOptional(int &a, int b=0);
#import

  i = 1;
  s = SetOptional(i); // second parameter is optional
  printf(s);

Result: 

Call with default parameters

For ease of debugging, the code from the above examples is organized as script, it located in the file Testing_DLL.mq5.

4. Possible errors at the design stage

Error: DLL Loading is not allowed.

Solution: Go to the MetaTrader 5 settings through the menu ' Tools-Options' and allow the import of DLL function, as shown in Figure 5.

  Figure 5. Permission to import DLL functions

 Figure 5. Permission to import DLL functions

Error: Cannot find 'function name' in 'DLL name'.
Solution: Check whether the callback function is specified in the Exports section of the DLL project. If it is, you should check the full match of the function's name in DLL and in the mql5 program -considering that it is character sensitive!

Error: Access violation write to [memory address]
Solution: You need to check the correctness of the description of the transmitted parameters (see table 2). Since usually this error is associated with the processing of lines, it is important to follow the recommendations for working with lines, laid out in paragraph 3.4 of this article.

5. Example of DLL code

As a visual example of the use of DLL, consider the calculations of parameters of the regression channel, consisting of three lines. To verify the correctness of the construction of the canal, we will use the built-in object "Canal regression". The calculation of the approximating line for LS (least squares method) is taken from the site http://alglib.sources.ru/, where there is a collection of algorithms for data processing. The algorithms code is presented in several programming languages, including Delphi.

To calculate the coefficients of a and b, by the approximating line y = a + b * x, use the procedure, described in the file LRLine linreg.pas.

 procedure  LRLine (  const  XY: TReal2DArray;  / / Two-dimensional array of real numbers for X and Y coordinates 
                           N : AlglibInteger;  // number of points
                     var Info : AlglibInteger; // conversion status
                       var  A: Double;  / / Coefficients of the approximating line 
                        var  B: Double);
  

To calculate the parameters of the channel, use the function CalcLRChannel.

Call for 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);
    // copy the data to a two-dimensional array
    for info:= 0 to len - 1 do
    begin
      arr[info,0]:= rates[info,0];
      arr[info,1]:= rates[info,1];
    end;

    // calculation of linear regression coefficients
    LRLine(arr, len, info, A,  B);

    // find the maximal deviation from the approximation line found
    // and determine the width of the channel 
    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;

Call for MQL5:

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

   double arr[][2], //data array for processing in the ALGLIB format
              a, b,  // Coefficients of the approximating line  
              max; // maximum deviation from the approximating line is equal to half the width of the channel
   
   int len = period; //number of points for calculation
   ArrayResize(arr,len);

// copying the history to a two-dimensional array
   int j=0;
   for(int i=rates_total-1; i>=rates_total-len; i--)
     {
      arr[j][0] = j;
      arr[j][1] = close[i];
      j++;
     }

// calculation of channel parameters
   CalcLRChannel(arr,len,a,b,max);

The indicator code, which uses the function CalcLRChannel for calculations, is located in the file LR_Channel.mq5 and below: 

//+------------------------------------------------------------------+
//|                                                   LR_Channel.mq5 |
//|                        Copyright 2009, MetaQuotes Software Corp. |
//|                                              http://www.mql5.com |
//+------------------------------------------------------------------+
#property copyright "2009, MetaQuotes Software Corp."
#property link      "http://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);

// copying of history to a two-dimensional array
   int j=0;
   for(int i=rates_total-1; i>=rates_total-len; i--)
     {
      arr[j][0] = j;
      arr[j][1] = close[i];
      j++;
     }

// procedure of calculating the channel parameters
   CalcLRChannel(arr,len,a,b,max);

// if the width of the channel has changed
   if(max!=save_max)
     {
      save_max=max;

      // Delete the channel
      line_md.Delete();
      line_up.Delete();
      line_dn.Delete();

      // Creating a channel with new coordinates
      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);

      // assigning the color of channel lines     
      line_up.Color(RoyalBlue);
      line_dn.Color(RoyalBlue);
      line_md.Color(RoyalBlue);

      // assigning the line width
      line_up.Width(2);
      line_dn.Width(2);
      line_md.Width(2);
     }

   return(len);
  }
//+------------------------------------------------------------------+
void OnDeinit(const int reason)
//+------------------------------------------------------------------+
  {
// Deleting the created objects
   chart.Detach();

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

The result of the indicator's work is the creation of a blue regression channel, as shown in Figure 6. To verify the correctness of the channel's construction, the chart shows a "Regression Canal", from the MetaTrader 5 staffing arsenal of instruments of technical analysis, marked in red.

As can be seen in the figure, the central lines of the channel merge together. Meanwhile, there is a slight difference in the width of the channel (a few points), which are due to the different approaches in its calculation. 

 Figure 6. Comparison of regression channels

Figure 6. Comparison of regression channels

Conclusion

This article describes the features of writing a DLL, using a Delphi application development platform.

Translated from Russian by MetaQuotes Software Corp.
Original article: https://www.mql5.com/ru/articles/96

Attached files |
mql5_sources.zip (276.43 KB)
dll_mql5_sources.zip (453.47 KB)
Last comments | Go to discussion (2)
bernd
bernd | 10 Jul 2010 at 00:12

It should be mentioned that there is an alternative to using Delphi.

If you are not already a Delphi user you should consider using Lazarus/FPC, it is open source, has almost the same features as Delphi (and even some more), is largely compatible with Delphi source code and I would even bet that all of the above examples compile in Lazarus without any modifications.

If you prefer open source over proprietary software (which is something you should do anyways) then Lazarus is what you are looking for and not a commercial trial version of Delphi.

KRTRADE
KRTRADE | 22 Jan 2011 at 02:20

DLL help
 

Can anyone help guide me .. Can we use MT4 DLL file on MT5 and if yes... where do you instill it in MT5 and any thing else i need to know.

 

Also which folder do we store the DLL file in ??? 

I do not have the code for the DLL anymore so i really cant rewrite it anymore.

 

 

any suggestion and help will be appreciated.  

Step on New Rails: Custom Indicators in MQL5 Step on New Rails: Custom Indicators in MQL5

I will not list all of the new possibilities and features of the new terminal and language. They are numerous, and some novelties are worth the discussion in a separate article. Also there is no code here, written with object-oriented programming, it is a too serous topic to be simply mentioned in a context as additional advantages for developers. In this article we will consider the indicators, their structure, drawing, types and their programming details, as compared to MQL4. I hope that this article will be useful both for beginners and experienced developers, maybe some of them will find something new.

Here Comes the New MetaTrader 5 and MQL5 Here Comes the New MetaTrader 5 and MQL5

This is just a brief review of MetaTrader 5. I can't describe all the system's new features for such a short time period - the testing started on 2009.09.09. This is a symbolical date, and I am sure it will be a lucky number. A few days have passed since I got the beta version of the MetaTrader 5 terminal and MQL5. I haven't managed to try all its features, but I am already impressed.

False trigger protection for Trading Robot False trigger protection for Trading Robot

Profitability of trading systems is defined not only by logic and precision of analyzing the financial instrument dynamics, but also by the quality of the performance algorithm of this logic. False trigger is typical for low quality performance of the main logic of a trading robot. Ways of solving the specified problem are considered in this article.

Using text files for storing input parameters of Expert Advisors, indicators and scripts Using text files for storing input parameters of Expert Advisors, indicators and scripts

The article describes the application of text files for storing dynamic objects, arrays and other variables used as properties of Expert Advisors, indicators and scripts. The files serve as a convenient addition to the functionality of standard tools offered by MQL languages.