用 Delphi 为 MQL5 编写 DLL 指南·进阶篇
📘

用 Delphi 为 MQL5 编写 DLL 指南·进阶篇

第 2/3 篇

◍ 跨语言调 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,平台稳定性风险高,建议先在模拟盘验证。

MQL5 / C++
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,就能确认你的调用约定没错位。

MQL5 / C++
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 交互只建议在策略测试器先验证再上真实账户。

MQL5 / C++
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 做高频计算时更要先在模拟盘验证稳定性,实盘存在滑点与断连的高风险。

常见问题

Delphi 默认按值或副本传递结构体,EA 侧拿到的不是原内存。需用指针或 var 参数传引用,才能在调用方看到修改。
跨语言数组内存布局不同,别直接返回动态数组。改为由调用方分配缓冲区、DLL 填充,并显式传长度参数。
可以。小布能对照你的调用代码标出 ANSI/Unicode 不匹配和默认参数缺失点,并给出改 DLL 声明的具体写法。
多是 Delphi DLL 用 ANSI、调用端用 Unicode。统一用 WideString 或 UTF-8 显式转换,并在声明里固定类型。
跨语言不支持默认参数语法糖。把可选参数改为显式传入,或用独立重载函数,避免栈帧错位。