简介
考虑使用 Delphi 2009 开发环境来作为编写 DLL 的机制的例子。之所以选择这个版本是因为在 MQL5 中,所有代码行都是以 Unicode 格式存储的。在旧版本的 Delphi 中,SysUtils 模块缺少处理采用 Unicode 格式的代码行的函数。
如果您出于任何原因正在使用早期版本(Delphi 2007 及更早),则您不得不处理采用 ANSI 格式的代码行,并且为了与 MetaTrader 5 交换数据,您需要进行 Unicode 的正反转换。为了避免此类复杂情况的出现,我建议在不早于 Delphi 2009 的环境中为 MQL5 开发 DLL 模块。可以从官方网站 http://embarcadero.com 下载 30 天试用版 Delphi。
1. 创建项目
要创建项目,我们需要通过选择菜单项来运行 DLL Wizard(DLL 向导):“File(文件) -> New(新建) -> Other(其他) ...-> DLL Wizard(DLL 向导)”。如图 1 所示。
图 1. 使用 DLL Wizard 创建项目
因此,我们将创建一个空的 DLL 项目,如图 2 所示。
图 2. 一个空的 DLL 项目
项目标题中很长的注释的本质是提醒您在处理动态分配的内存时正确连接和使用内存管理器。这将在处理字符串的部分中详细讨论。
在开始用函数填充新的 DLL 之前,配置项目非常重要。
从菜单打开项目属性窗口:“Project(项目) -> Options(选项) ...”,或按键盘上的组合键 'Shift + Ctrl + F11'。
为了简化调试过程,必须直接在交易客户端 MetaTrtader5 的文件夹 '..\\MQL5\\Libraries' 中创建 DLL 文件。为此,在 DelphiCompiler 选项卡中将 Output directory(输出目录)设置为相应的属性值,如图 3 所示。这样就不需要不断地从项目文件夹将 DLL 生成的文件复制到客户端文件夹。
图 3. 指定用于存储生成 DLL 文件的文件夹
为了避免 BPL 模块在汇编期间挂起,Windows 系统文件夹中不存在该模块,在 Packages(程序包)选项卡上检查未选中标记 Build with runtime packages(带运行时间程序包编译),如图 4 所示。
图 4. 汇编时不包括 BPL 模块
在完成项目配置之后,将其保存到您的工作文件夹,指定的项目名称将是编译后的 DLL 文件的名称。
2. 添加过程和函数
让我们以一个不含参数过程为例,说明在 DLL 模块中编写导出的过程和函数时的一般情形。参数的声明和传递将在下一节中讨论。
有一点点离题。在用 Object Pascal 语言编写过程和函数时,程序员有机会使用内置的 Delphi 库函数,而不用提及专为此环境开发的无数组件。例如,要执行相同的操作,例如,要显示含有文本消息的对话框,可以使用 API 函数 - MessageBox,还可以使用来自 VCL 库的过程 - ShowMessage。
第二个选项导致包含 Dialogs(对话框)模块在内,并且具备轻松使用标准 Windows 对话框的优点。但是,生成的 DLL 文件的大小将增加大约 500 KB。因此,如果您宁愿创建小的 DLL 文件,不占用太多磁盘空间,则我不会建议您使用 VCL 组件。
以下是一个含有解释的测试项目示例:
library dll_mql5; uses Windows, // 必要工作,用于 MessageBox 函数 Dialogs; // 必要工作,用于 ShowMessage 进程 ,来自 Dialogs 模块 var Buffer: PWideChar; //------------------------------------------------------+ procedure MsgBox(); stdcall; // //为避免错误, 使用 stdcall (或 cdecl) 来输出函数 //------------------------------------------------------+ begin {1} MessageBox(0,'你好世界!','终端', MB_OK); {2} ShowMessage('你好世界!');// 替代 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 在默认情况下使用快速调用协议,该协议主要使用 CPU 寄存器来传递参数,而不是堆栈。毫无疑问,这将导致在调用外部函数 DLL 时出现参数传递处理错误。
"begin end" 部分包含 DLL 事件处理程序的标准初始化代码。在连接调用该过程的进程以及中断与该进程的连接时,将调用 DLLEntryPoint 回调过程。这些事件可用于正确的动态管理为满足我们的需要而分配的内存,如示例所示。
MQL5 调用:
#import "dll_mql5.dll" void MsgBox(void); void MessageBox(void); #import // 调用进程 MsgBox(); // 如果函数的名称与 MQL5 标准库函数的名称一致 // 调用函数时使用 DLL 名称 dll_mql5::MessageBox();
3. 将参数传递到函数和返回值
在考虑参数传递之前,让我们分析一下 MQL5 和 Object Pascal 的数据对应表。
MQL5 的数据类型 |
Object Pascal (Delphi) 的数据类型 |
备注 |
---|---|---|
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 | 需要转换(见本节中的下文) |
color | TColor |
表 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; |
错误:写入到 <内存地址> 时出现访问冲突 | |
按链接 |
int func (int &a); |
func (var a:Integer):Integer; |
正确;事实上,在没有修饰符 var 的情况下传输代码行! |
int func (int &a); | func (a:Integer):Integer; | 错误:代替变量的值,包含内存单元格的地址 |
表 2. 参数传递方法
现在,让我们考虑使用传递的参数和返回值的例子。
3.1 转换日期和时间
首先,让我们处理您要转换的日期和时间的类型,因为 datetime 类型仅在其大小方面对应于 TDateTime,但是在格式方面并不对应。为了便于转换,使用 Int64 作为接收数据类型,而不是 TDateTime。下面是用于正向和反向转换的函数:
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 和 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 和 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 中分配的内存,反之亦然。
让我们考虑一个采用 Windows API 函数调用风格的内存管理例子。在我们的例子中,mql5 程序为缓存分配内存,一个指向缓存的指针作为 PWideChar 被传递给 DLL,而 DLL 仅将需要的值填入此缓存,如下面的例子所示:
Object Pascal 调用:
//----------------------------------------------------------+ procedure SetString(const str:PWideChar) stdcall; //----------------------------------------------------------+ begin StrCat(str,'Current time:'); 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
重要的是所有四个选项都起作用。在前两个选项中,通过全局分配的内存处理代码行。
在选项 A 中,内存是独立分配的,在选项 B 中,内存管理工作由内存管理器承担。
在选项 C 中,代码行常量并不是存储在内存中,而是在代码段中,因此内存管理器并不为其存储分配动态内存。选项 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。
解决方法:通过菜单“ Tools(工具)-Options(选项)”进入 MetaTrader 5 设置,并且允许导入 DLL 函数,如图 5 所示。
图 5. 允许导入 DLL 函数
错误:在 DLL 名称中找不到函数名称。
解决方法:检查是否在 DLL 项目的 Exports(导出)部分指定了回调函数。如果指定,则应检查 DLL 中的名称与 mql5 程序中的名称是否完全相同 - 注意名称是区分大小写的!
错误:写入到 [内存地址] 时出现访问冲突
解决方案:您需要检查所传递参数的说明是否正确(见表 2)。因为此错误通常与代码行的处理有关,遵循本文的第 3.4 段所指出的代码行处理建议非常重要。
5. DLL 代码示例
作为使用 DLL 的可视化例子,考虑由三条线构成的回归通道参数的计算。为了验证通道构建的正确性,我们将使用内置的对象 "Canal regression"(回归通道)。针对 LS(最小平方法)的近似线的计算来自网站 http://alglib.sources.ru/,该网站收集了大量的数据处理算法。算法代码以几种编程语言表示,包括 Delphi。
为了通过近似线 y = a + b * x 计算 a 和 b 的系数,使用文件 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); // 复制数据至 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); // 复制历史数据至 2 维数组 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);
文件 LR_Channel.mq5 给出了使用函数 CalcLRChannel 进行计算的指标代码,如下所示:
//+------------------------------------------------------------------ //| 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("图表未创建"); return(false);} chart.Attach(); if(chart.ChartId()==0) {printf("图表未打开");return(false);} if((line_up=new CChartObjectChannel)==NULL) {printf("通道未创建"); return(false);} if((line_dn=new CChartObjectChannel)==NULL) {printf("通道未创建"); return(false);} if((line_md=new CChartObjectChannel)==NULL) {printf("通道未创建"); 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); // 复制历史数据至 2 维数组 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 技术分析工具兵工厂的 "Regression Canal"(回归通道),以红色标记。
如图所示,通道的中心线合在一起。同时,通道的宽度稍有差异(几点),这是因为计算方法的不同造成的。
图 6. 回归通道的比较
总结
本文说明了使用 Delphi 应用程序开发平台编写 DLL 的特点。