In this thread i will try to collect all the small bits of information that are not so obvious but needed to create DLLs that play together with Metatader, a collection of minimal and self contained code snippets that illustrate how things are done. The intended audience are people already familiar with programming, this is no Pascal tutorial, i will focus only on all the little things that need to be known for Metatrader specific development projects.
I will use Lazarus, the free IDE containing the the Free Pascal compiler FPC, an excellent and very advanced (Object-)Pascal Compiler. I chose Lazarus/FPC because it is by far the easiest and most efficient tool for this job that is available today, and it is free!
Make sure you are downloading the 32 bit version, even if you are running Windows 64 bit on x64 hardware, Metatrader is a 32 bit application and can only use 32 bit DLLs, the 64 bit version of Lazarus/FPC would by default produce 64 bit binaries which can not be used for our purpose.
To create a new DLL project (if you started Lazarus for the first time) click on Project -> new project -> Library. Give it a meaningful name and save it into an empty folder. The project will initially consist of only two files. An .lpi file containing all compiler settings and and .lpr file containing the source code, these two files are all you need to backup, pass on to your colleagues or manage in a source code repository. Additional units (if you decide to split up bigger projects) are saved as individual .pas files.
If you come from a C background you will find it notable that there is no need for make, .def or whatever-files, everything that compiler and linker need to know is written in the pascal source itself, you simply throw the main unit at the compiler and it will figure out everything by itself: http://www.at.freepascal.org/advantage.html
I will expand this first Posting over time with examples of increasing complexity, whenever I have new examples.
1. Minimal example with strings
I will start this thread with a minimal example of a working dll that will show you how to pass strings to your DLL and how to return strings to Metatrader. Since we are using modern Object-Pascal instead of ancient and clumsy C or C++ it is really easy and intuitive:
This is all that is needed on the Pascal side:
and this is the corresponding mql4:
The very first and most important thing you need to know about DLLs used for MT4 is marked with red color: Metatrader assumes the 'stdcall' calling convention which means MT4 will push the arguments onto the stack from right to left and our function is responsible for cleaning the stack before returning. FPC's default calling convention would be 'register', also known as 'Borland fastcall', which would use processor registers for the first three 32 Bit arguments. Using the wrong calling convention would immediately crash Metatrader with a segfault due to a messed up stack. Thus we must decorate all exported functions with the keyword stdcall. Another option for achieving the same goal would be the compiler directive {$CALLING STDCALL} at the top of each unit containing exported functions.
2. Passing doubles and integers by reference
By reference means in Metatrader simply putting an ampersand & behind the type identifier in the function declaration and the function can then write values to these arguments. This is a convenient way to return more than one value from a function. Unfortunately with imported functions this seems only possible with arrays. For some weird reason we cannot just pass scalar values by reference to a DLL, MT4 would just push a bunch of zeros onto the stack which is quite useless.
We need to do this with the help of arrays. If we pass an array by reference MT4 will pass a Pointer to the first array element which is easily handled in Pascal either by a typed pointer to an array that can be dereferenced or even more elegantly by declaring the arguments with the keyword var which is technically the same, only nicer:
and in mql4:
3. Giving the DLL access to all bars in the chart
mql4 has two interesting functions: ArrayCopyRates() and ArrayCopySeries(). These functions, when applied to an empty array, do something interesting with it. Despite the name "copy" they don't actually copy anything, instead they replace the array entirely with something different and very interesting: Once you have applied one of these functions to an array variable, this variable will from now on behave exactly like one of the built-in arrays Time[], Open[], High[], Low[], Close[], Volume[]. It will automatically update when new ticks come in and if you now pass such an array by-reference to your DLL your DLL will receive a pointer to a memory location containing always up to date quotes.
We will use ArrayCopyRates() since this will give us the pointer to a 2-dimensional array, containing time and OHLCV for every bar in the chart.
There is only one noteworthy thing: in mql these series arrays always start with 0 being the current bar, in reality when accessing the memory directly in our DLL, it starts with 0 being the oldest bar and we need to pass an additional parameter to tell the DLL how many bars there are in total so it can figure out which one is the current one. (The array has no upper bound but values beyond the current index (Bars-1) are undefined and meaningless.)
The following example will calculate a simple moving average over the last 14 close prices to illustrate this concept, the mql4 EA will call this function and plot an arrow into the chart at the calculated price:
and in mql4:
This is only an example to demonstrate how to access the bars in the chart. See Post #48 in this thread for a more complete version that properly uses an indicator buffer the way indicators are supposed to be written and indicator buffers are supposed to be used.
I will use Lazarus, the free IDE containing the the Free Pascal compiler FPC, an excellent and very advanced (Object-)Pascal Compiler. I chose Lazarus/FPC because it is by far the easiest and most efficient tool for this job that is available today, and it is free!
Make sure you are downloading the 32 bit version, even if you are running Windows 64 bit on x64 hardware, Metatrader is a 32 bit application and can only use 32 bit DLLs, the 64 bit version of Lazarus/FPC would by default produce 64 bit binaries which can not be used for our purpose.
To create a new DLL project (if you started Lazarus for the first time) click on Project -> new project -> Library. Give it a meaningful name and save it into an empty folder. The project will initially consist of only two files. An .lpi file containing all compiler settings and and .lpr file containing the source code, these two files are all you need to backup, pass on to your colleagues or manage in a source code repository. Additional units (if you decide to split up bigger projects) are saved as individual .pas files.
If you come from a C background you will find it notable that there is no need for make, .def or whatever-files, everything that compiler and linker need to know is written in the pascal source itself, you simply throw the main unit at the compiler and it will figure out everything by itself: http://www.at.freepascal.org/advantage.html
I will expand this first Posting over time with examples of increasing complexity, whenever I have new examples.
1. Minimal example with strings
I will start this thread with a minimal example of a working dll that will show you how to pass strings to your DLL and how to return strings to Metatrader. Since we are using modern Object-Pascal instead of ancient and clumsy C or C++ it is really easy and intuitive:
This is all that is needed on the Pascal side:
Inserted Code
[b]library[/b] minimal; [color=DeepSkyBlue]{$mode objfpc}{$H+}[/color] [b]uses[/b] sysutils; [color=SeaGreen]// strings from and to Metatrader will always be passed // as PChar which is a pointer to a nullterminated C-string.[/color] [b]function[/b] foo(x: double; y: [color=Blue]PChar[/color]): [color=Blue]PChar[/color]; [b][color=Red]stdcall[/color][/b]; [b]var[/b] s :[color=Black]ansistring[/color]; [color=SeaGreen]// reference counted and memory managed strings.[/color] [b]begin[/b] [color=SeaGreen] // our PChar will be copied into an ansistring automatically, // no need to worry about the ugly details of memory allocation.[/color] s := 'Hello ' + FloatToStr(x) + ' ' + y + '!'; [color=SeaGreen] // cast it back into a pointer. Metatrader will copy the // string from the pointer into it's own memory.[/color] result := [color=Blue]PChar[/color](s); [b]end;[/b] [b]exports[/b] foo; [b] begin end.[/b]
Inserted Code
#import "minimal.dll" [color=SeaGreen]// declare the imported function exactly as it is exported by the dll[/color] [color=Blue]string[/color] foo(double x, [color=Blue]string[/color] y); #import int init(){ [color=Blue]string[/color] s = foo(42.3, "Worlds"); Print(s); [color=SeaGreen]// will print "Hello 42.3 Worlds!"[/color] } int start(){ }
2. Passing doubles and integers by reference
By reference means in Metatrader simply putting an ampersand & behind the type identifier in the function declaration and the function can then write values to these arguments. This is a convenient way to return more than one value from a function. Unfortunately with imported functions this seems only possible with arrays. For some weird reason we cannot just pass scalar values by reference to a DLL, MT4 would just push a bunch of zeros onto the stack which is quite useless.
We need to do this with the help of arrays. If we pass an array by reference MT4 will pass a Pointer to the first array element which is easily handled in Pascal either by a typed pointer to an array that can be dereferenced or even more elegantly by declaring the arguments with the keyword var which is technically the same, only nicer:
Inserted Code
[b]library[/b] testlib; [color=DeepSkyBlue]{$mode objfpc}{$H+}[/color] [b]type[/b] TDPair = array[0..1] of double; TIPair = array[0..1] of LongInt;[color=SeaGreen] // 32 bit int[/color] [color=SeaGreen]// function parameters declared as var will accept pointers.[/color] [b]procedure[/b] VarsByReference([color=Red][b]var[/b][/color] a: TDPair; [color=Red][b]var[/b][/color] b: TIPair); stdcall; [b]begin[/b] [color=SeaGreen]// now let's make some changes to the variables[/color] a[0] += a[1]; a[1] -= a[0]; b[0] += b[1]; b[1] -= b[0]; [b]end;[/b] [b]exports[/b] VarsByReference; [b]begin[/b] [b]end.[/b]
Inserted Code
#import "testlib.dll" [color=SeaGreen] /** * the function takes 2 arrays which will be * passed by reference. On the DLL side this * means we will receive Pointers to the * the memory locations of the arrays */[/color] void VarsByReference(double[b][color=Red]&[/color][/b] a[], int[b][color=Red]&[/color][/b] b[]); #import int init(){ double foo[2];[color=SeaGreen] // define pair of doubles[/color] int bar[2]; [color=SeaGreen]// define pair of integers[/color] foo[0] = 1.23; foo[1] = 4.56; bar[0] = 42; bar[1] = 23; VarsByReference(foo, bar); [color=SeaGreen]// print the changed values[/color] Print(foo[0]); Print(foo[1]); Print(bar[0]); Print(bar[1]); } int start(){ }
3. Giving the DLL access to all bars in the chart
mql4 has two interesting functions: ArrayCopyRates() and ArrayCopySeries(). These functions, when applied to an empty array, do something interesting with it. Despite the name "copy" they don't actually copy anything, instead they replace the array entirely with something different and very interesting: Once you have applied one of these functions to an array variable, this variable will from now on behave exactly like one of the built-in arrays Time[], Open[], High[], Low[], Close[], Volume[]. It will automatically update when new ticks come in and if you now pass such an array by-reference to your DLL your DLL will receive a pointer to a memory location containing always up to date quotes.
We will use ArrayCopyRates() since this will give us the pointer to a 2-dimensional array, containing time and OHLCV for every bar in the chart.
There is only one noteworthy thing: in mql these series arrays always start with 0 being the current bar, in reality when accessing the memory directly in our DLL, it starts with 0 being the oldest bar and we need to pass an additional parameter to tell the DLL how many bars there are in total so it can figure out which one is the current one. (The array has no upper bound but values beyond the current index (Bars-1) are undefined and meaningless.)
The following example will calculate a simple moving average over the last 14 close prices to illustrate this concept, the mql4 EA will call this function and plot an arrow into the chart at the calculated price:
Inserted Code
[b]library[/b] testlib; [color=MediumTurquoise]{$mode objfpc} {$H+}[/color] [b]type[/b] [color=SeaGreen]{ MT4 stores the data of each candle in such a structure. MT4 will then give us a pointer to an array of such candles. NOTE the unconventional order: open, [b]low, high[/b], close, volume instead of OHLCV, this might be wrong in some of the other examples, I noticed it too late, please check your code. This here is the correct definition }[/color] TCandle = [b]packed record[/b] time: LongInt; [color=SeaGreen]// POSIX timestamp[/color] open, low, high, close, volume: double; [b]end[/b]; [color=SeaGreen]{ the following would be a pretty large array but we actually never create an instance of it, we only need a typed pointer to such an array.}[/color] THistory = [b]array[/b][0..MaxLongInt [b]div[/b] SizeOf(TCandle) - 1] [b]of[/b] TCandle; PHistory = ^THistory; [color=SeaGreen]{ this function demonstrates how use such a PHistory pointer. Note that unlike in MT4 the array is indexed with 0 being the OLDEST and history_size - 1 being the CURRENT bar.}[/color] [b]function[/b] movingAverage(history: PHistory; history_size: LongInt; period: LongInt): Double; stdcall; [b]var[/b] sum: Double; i: LongInt; [b]begin[/b] [b]if[/b] period < 1 [b]then[/b] exit(0); [color=SeaGreen] // calculate the average of period last bars' close prices[/color] sum := 0; [b]for[/b] i := 1 [b]to[/b] period [b]do[/b] [color=SeaGreen]// i=1 is the current bar (history_size - 1)![/color] [b]begin[/b] sum += history^[history_size - i].close; [b]end[/b]; result := sum / period; [b]end[/b]; [b]exports[/b] movingAverage; [b]begin[/b] [b]end[/b].
Inserted Code
#import "testlib.dll" double movingAverage(double& history[][6], int history_size, int period); #import double history[][6]; int init(){ [color=SeaGreen] // This won't actually copy anything, instead it // will make the array point to a metatrader // data structure where all candles are stored. // You only need to do this once, new candles will // be added automatically as time goes by.[/color] ArrayCopyRates(history, NULL, 0); } int start(){ [color=SeaGreen] // Bars will always contain the number of usable // candles in our array. The DLL needs it to // determine the index of the current bar, because // in reality (outside of MQL4) this array starts // with 0 being the oldest bar and Bars-1 the current.[/color] double ma = movingAverage(history, Bars, 14); [color=SeaGreen] // draw an arrow at our calculated average price[/color] string name = "arrow_" + Time[0]; ObjectCreate(name, OBJ_ARROW, 0, Time[0], ma); ObjectSet(name, OBJPROP_ARROWCODE, 4); ObjectSet(name, OBJPROP_PRICE1, ma); }