Guide to writing a DLL for MQL5 in Delphi
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
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. | //| 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); // 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
Conclusion
This article describes the features of writing a DLL, using a Delphi application development platform.
Translated from Russian by MetaQuotes Ltd.
Original article: https://www.mql5.com/ru/articles/96
- Free trading apps
- Over 8,000 signals for copying
- Economic news for exploring financial markets
You agree to website policy and terms of use
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.
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.