用 Delphi 为 MQL5 编写 DLL 指南·进阶篇
◍ 跨语言调 DLL 时的参数与返回值坑点
把 MQL5 的变量丢给 Object Pascal 写的 DLL,先得对齐类型。除 datetime 外,char→ShortInt、int→Integer、double→Double、string→PWideChar 基本是一一对应的;datetime 虽然长度跟 TDateTime 都是 8 字节,但内部计数起点不同,不能直接当 TDateTime 用,得先转成 Int64 再处理。
传参分按值和按引用两种。MQL5 里写 int func(int a) 是按值,Delphi 端必须写成 func(a:Integer):Integer;如果 Delphi 误写成 func(var a:Integer),运行时会抛出“写入内存地址访问冲突”。反过来 MQL5 写 int func(int &a) 是按引用,Delphi 端必须用 var a:Integer 接收,否则传进去的是内存地址而不是变量值。
简单类型里有个细节:下面代码里 d 是按值传的,所以在 DLL 中改 d 不会影响调用方;而 b 加了 const 修饰符,从语法上锁死不会被改。i 和 dt 是 var 引用,出函数后原值被改写——i 从 5 变成 10,dt 被填成调用时刻。
结构体和数组走引用传递时,字段保护完全靠程序员自己。斐波那契数组那种写法,DLL 直接往 MT5 给的数组内存里填数,调用完数据已经在原数组上生效。
字符串最容易爆内存问题。黄金法则只有一条:谁分配谁释放。MT5 里用 string 分配好 PWideChar 缓存传给 DLL,DLL 只填不释放;若 DLL 内部用局部变量做字符串返回(类似文中所说选项 D),函数退出后内存立即释放,属于明显错误,别碰。
默认参数放声明末尾即可,调用时可省略;文中示例脚本放在 Testing_DLL.mq5 里方便你直接挂 MT5 调试。外汇与贵金属跨语言调用涉及 DLL,平台稳定性风险高,建议先在模拟盘验证。
uses SysUtils, class=class="str">"cmt">// 用于恒定 UnixDateDelta DateUtils; class=class="str">"cmt">// 用于函数 IncSecon, DateTimeToUnix class=class="str">"cmt">//----------------------------------------------------------+ Function MQL5_Time_To_TDateTime(dt: Int64): TDateTime; class=class="str">"cmt">//----------------------------------------------------------+ begin Result:= IncSecond(UnixDateDelta, dt); end; class=class="str">"cmt">//----------------------------------------------------------+ Function TDateTime_To_MQL5_Time(dt: TDateTime):Int64; class=class="str">"cmt">//----------------------------------------------------------+ begin Result:= DateTimeToUnix(dt); end; class=class="str">"cmt">//----------------------------------------------------------+ function SetParam(var i: Integer; d: Double; const b: Boolean; var dt: Int64): PWideChar; stdcall; class=class="str">"cmt">//----------------------------------------------------------+ begin if (b) then d:=class="num">0; class=class="str">"cmt">// 变量 d 的值调用程序时未改变 i:= class="num">10; class=class="str">"cmt">// 分配新值给 i dt:= TDateTime_To_MQL5_Time(Now()); class=class="str">"cmt">// 分配当前时间给 dt Result:= &class="macro">#x27;变量 i 和 dt 的数值改变&class="macro">#x27;; end; class="macro">#class="kw">import "dll_mql5.dll" class="type">class="kw">string SetParam(class="type">int &i, class="type">class="kw">double d, class="type">bool b, class="type">class="kw">datetime &dt); class="macro">#class="kw">import class=class="str">"cmt">// 初始化变量 class="type">int i = class="num">5; class="type">class="kw">double d = class="num">2.8; class="type">bool b = true; class="type">class="kw">datetime dt= D&class="macro">#x27;class="num">05.05.class="num">2010 class="num">08:class="num">31:class="num">27&class="macro">#x27;; class=class="str">"cmt">// 调用函数 s=SetParam(i,d,b,dt);
DLL 里改结构体和数组的实情
把 MQL5 的 struct 通过引用丢给外部 DLL,DLL 内部对字段的改写会直接回写到调用端。上面那段测试中,初始 data.i=5、data.d=2.8、data.b=true、data.dt 为 2010.05.05 08:31:27;DLL 里强制把 i 置 10、b 为真时把 d 清 0、dt 刷成当前时间,返回后本地打印出 i=10、d=0.00000000、b=true、dt=变成 2009.05.05 12:19。 数组也是同一套逻辑:SetArray 接收 int 引用数组和长度,len 小于 3 直接退出,否则把前两个元素设 0 和 1,从下标 2 开始填斐波那契。本地开 12 长度的 int 数组传进去,回来打印就是 0 1 1 2 3 5 8 13 21 34 55 89。 [CODE] 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)); [/CODE] 逐行看这段代码:struct 定义四个字段,类型与 DLL 端 packed record 一一对应;#import 声明从 dll_mql5.dll 引入 SetStruct,参数是结构体引用;本地先给 i、d、b、dt 赋初值,再调用;DLL 改完引用内容后,printf 打出的就是被改过的字段值,证明引用传递生效。 做跨 DLL 计算时,外汇和贵金属行情波动剧烈、杠杆风险高,引用传参虽方便但 DLL 若写错下标可能破坏相邻内存。开 MT5 建个 dll_mql5 测试工程,把上面数组长度改成 20,看斐波那契序列是否延伸到 6765,就能确认你的调用约定没错位。
class="kw">struct STRUCT_DATA { class="type">int i; class="type">class="kw">double d; class="type">bool b; class="type">class="kw">datetime dt; }; class="macro">#class="kw">import "dll_mql5.dll" class="type">class="kw">string SetStruct(STRUCT_DATA &data); class="macro">#class="kw">import STRUCT_DATA data; data.i = class="num">5; data.d = class="num">2.8; data.b = true; data.dt = D&class="macro">#x27;class="num">05.05.class="num">2010 class="num">08:class="num">31:class="num">27&class="macro">#x27;; 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));
「MT5 调用 DLL 时的字符串与可选参数坑点」
在 MT5 里通过 #import 调外部 dll_mql5.dll 传字符串,最容易踩的是缓冲区未初始化。原文实测用 StringInit(s,255,0) 先开 255 宽字符的零缓冲,再传引用进 SetString(s),printf 才能稳定打出 'Current time: 11:48:51' 这类结果;不初始化或缓冲小于字符串长度,大概率直接崩或出乱码。 DLL 端返回 PWideChar 有四种写法差别很大:用全局 WideString 取 @StrGlobal[1] 安全,局部变量 StrLocal 的 @StrLocal[1] 在函数退出后内存释放,调用方读到的是野指针;代码段常量字符串 '返回的线保存于代码段' 则生命周期最长。 可选参数在声明端写 b:Integer=0,MT5 侧对应 int b=0。实测 i=1 时只传一个参数调 SetOptional(i),返回 '以省缺参数调用';补第二个非 0 参就走 else 分支。外汇和贵金属行情波动剧烈、杠杆高风险,这类 dll 交互只建议在策略测试器先验证再上真实账户。
class=class="str">"cmt">//----------------------------------------------------------+ procedure SetString(const str:PWideChar) stdcall; class=class="str">"cmt">//----------------------------------------------------------+ begin StrCat(str,&class="macro">#x27;Current time:&class="macro">#x27;); strCat(str, PWideChar(TimeToStr(Now))); end; class="macro">#class="kw">import "dll_mql5.dll" class="type">void SetString(class="type">class="kw">string &a); class="macro">#class="kw">import class=class="str">"cmt">// 字符串在使用前必须被初始化 class=class="str">"cmt">// 初始化缓存区大小必须大于或等于字符串长度 StringInit(s,class="num">255,class="num">0); class=class="str">"cmt">//传递缓存区引用至 DLL SetString(s); class=class="str">"cmt">// 输出结果 printf(s); class=class="str">"cmt">//----------------------------------------------------------+ function GetStringBuffer():PWideChar; stdcall; class=class="str">"cmt">//----------------------------------------------------------+ var StrLocal: WideString; begin class=class="str">"cmt">// 通过动态分配内存缓存区工作 StrPCopy(Buffer, WideFormat(&class="macro">#x27;当前日期和时间: %s&class="macro">#x27;, [DateTimeToStr(Now)])); class=class="str">"cmt">// 通过全局变量 WideString 类型工作 StrGlobal:=WideFormat(&class="macro">#x27;当前时间: %s&class="macro">#x27;, [TimeToStr(Time)]); class=class="str">"cmt">// 通过局部变量 WideString 类型工作 StrLocal:= WideFormat(&class="macro">#x27;当前日期: %s&class="macro">#x27;, [DateToStr(Date)]); {A} Result := Buffer; {B} Result := PWideChar(StrGlobal); class=class="str">"cmt">// 它等同于以下 Result := @StrGlobal[class="num">1]; {С} Result := &class="macro">#x27;返回的线保存于代码段&class="macro">#x27;; class=class="str">"cmt">// 内存指针, 当函数退出时可被释放 {D} Result := @StrLocal[class="num">1]; end; class="macro">#class="kw">import "dll_mql5.dll" class="type">class="kw">string GetStringBuffer(class="type">void); class="macro">#class="kw">import printf(GetStringBuffer()); class=class="str">"cmt">//----------------------------------------------------------+ function SetOptional(var a:Integer; b:Integer=class="num">0):PWideChar; stdcall; class=class="str">"cmt">//----------------------------------------------------------+ begin if (b=class="num">0) then Result:=&class="macro">#x27;以省缺参数调用&class="macro">#x27; else Result:=&class="macro">#x27;不以省缺参数调用&class="macro">#x27;; end; class="macro">#class="kw">import "dll_mql5.dll" class="type">class="kw">string SetOptional(class="type">int &a, class="type">int b=class="num">0); class="macro">#class="kw">import i = class="num">1; s = SetOptional(i); class=class="str">"cmt">// 第二个参数是选项 printf(s);
◍ DLL 接入阶段的三个典型坑
在 MT5 里调用自建 DLL 时,最先撞墙的往往不是算法,而是环境开关。若 EA 或脚本死活加载不了外部库,先走菜单“工具-选项”,在相应页签勾选允许导入 DLL 函数,否则 MQL5 侧连入口都打不开。 另一个高频报错是“在 DLL 名称中找不到函数名称”。根源通常是 DLL 工程 Exports 段没声明回调,或者 MQL5 调用处的函数名与导出名不一致——注意 Windows 导出符号区分大小写,少一个字母、错一个大小写都会直接找不到。 最棘手的是“写入到 [内存地址] 时出现访问冲突”。这基本指向参数传递说明错了(参见参数映射表),栈帧被破坏后才会踩到非法地址。按前文第 3.4 段给出的行处理约定对齐结构体与调用约定,能消掉大部分此类崩溃。外汇与贵金属品种波动剧烈,接 DLL 做高频计算时更要先在模拟盘验证稳定性,实盘存在滑点与断连的高风险。