10 分钟掌握 MQL5 的 DLL(第二部分):使用 Visual Studio 2017 创建·进阶篇
📘

10 分钟掌握 MQL5 的 DLL(第二部分):使用 Visual Studio 2017 创建·进阶篇

第 2/3 篇

◍ DllMain 里的初始化陷阱

在 MT5 里写 DLL 函数库,很多人默认把初始化逻辑塞进 DllMain 的 DLL_PROCESS_ATTACH 分支,以为库一挂上终端就会跑。实际跑一段最小样例就能戳破这个错觉:导出一个 fnExport1 返回内部变量,变量在 DllMain 里赋值为 7,脚本里调用后打印出「fnExport1: 7」。 看输出只拿到 7,说明 DLL_PROCESS_ATTACH 那段确实执行了;但如果你在 DLL_THREAD_ATTACH 里写了 iParam += 1 这类线程挂接时的累加,它并不会被触发——MT5 加载 DLL 的方式不走线程挂接回调。 隐患在这:若你把连接数据库、读配置、算常量这些「以为加载必跑」的代码放在 DllMain 非 PROCESS_ATTACH 分支,不会报任何错,终端静默跳过。等你发现 EA 取到的参数是空的或默认的,排查要花数小时。外汇与贵金属插件开发属高风险底层操作,这类静默失败可能直接影响实盘信号。 别把 DllMain 当万能入口 只在 DLL_PROCESS_ATTACH 里做纯粹的单次赋值或内存申请,线程相关初始化挪到显式导出的 Init 函数里,由 MQL5 侧主动调用,才不会掉进静默坑。

MQL5 / C++
PROJECT2_API class="type">int fnExport1(class="type">void) {
    class="kw">return GetSomeParam();
}
class="kw">const class="type">int GetSomeParam();
class="macro">#include "stdafx.h"
class="macro">#include "Header1.h"
class="type">int iParam;
BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    class="kw">switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
            iParam = class="num">7;
            class="kw">break;
    case DLL_THREAD_ATTACH:
            iParam += class="num">1;
            class="kw">break;
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
            class="kw">break;
    }
    class="kw">return TRUE;
}
class="kw">const class="type">int GetSomeParam() {
        class="kw">return iParam;
}
class="macro">#class="kw">import "Project2.dll"
class="type">int fnExport1(class="type">void);
class="macro">#class="kw">import
class="type">void OnStart() {
Print("fnExport1: ",fnExport1() );
}

把字符串丢进 DLL 前先给它赋值

在 MT5 里把 string 传给外部 DLL 的导出函数,最容易被忽略的一步是初始化。很多人直接声明一个 string 变量就往里塞,结果脚本一跑就崩。 上面这段演示很直观:第一次调用 SamplesW 时传入已赋值的 "Hello from MQL5",终端打印出 SamplesW(): Hello from MQL5 Hello from C++;第二次只写 string t; 不初始化,立刻抛出 Access violation at 0x00007FF96B322B1F read to 0x0000000000000008;补上 string t=""; 再跑,错误消失,输出变成 SamplesW(): Hello from C++。 根因在 C++ 侧:SamplesW 先用 wcslen 算传入指针的长度,再 wcscpy_s 往 pChar+len 后面拼内容。若 MQL5 传进来的 string 底层指针是空的,wcslen 直接读非法地址。外汇与贵金属 EA 调用第三方库时这类闪退很难现场排查,属于高风险隐患。 别把 Unicode 当可选项。只要不把字符串跨给 DLL,确实不用开 Unicode;但导出的函数签名以后可能改、新人可能忘,统一开启能少踩坑。符号数组按通用方式传收即可,前文已覆盖,这里不重复。

MQL5 / C++
PROJECT2_API <span class="keyword">class="type">void</span> SamplesW(wchar_t* pChar) {
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;class="type">size_t len = wcslen(pChar);
&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;&nbsp;wcscpy_s(pChar + len, <span class="number">class="num">255</span>, L<span class="class="type">class="kw">string">" Hello from C++"</span>);
}
<span class="preprocessor">class="macro">#class="kw">import </span><span class="class="type">class="kw">string">"Project2.dll"</span>
<span class="keyword">class="type">void</span> SamplesW(<span class="keyword">class="type">class="kw">string</span>&amp; pChar);
<span class="preprocessor">class="macro">#class="kw">import
</span><span class="keyword">class="type">void</span> <span class="functions">OnStart</span>() {
<span class="keyword">class="type">class="kw">string</span> t = <span class="class="type">class="kw">string">"Hello from MQL5"</span>;
SamplesW(t);
<span class="functions">Print</span>(<span class="class="type">class="kw">string">"SamplesW(): "</span>, t);
<span class="preprocessor">class="macro">#class="kw">import </span><span class="class="type">class="kw">string">"Project2.dll"</span>
<span class="keyword">class="type">void</span> SamplesW(<span class="keyword">class="type">class="kw">string</span>&amp; pChar);
<span class="preprocessor">class="macro">#class="kw">import
</span><span class="keyword">class="type">void</span> <span class="functions">OnStart</span>() {
<span class="keyword">class="type">class="kw">string</span> t<span class="class="type">class="kw">string"></span>;
SamplesW(t);
<span class="functions">Print</span>(<span class="class="type">class="kw">string">"SamplesW(): "</span>, t);
<span class="keyword">class="type">class="kw">string</span> t=<span class="class="type">class="kw">string">""</span>;

「DLL 与脚本结构对齐错位引发的脏数据」

在 MT5 里让脚本调用外部 DLL 处理结构,最隐蔽的坑不是逻辑写错,而是两边对齐方式不一致。先用只含两个 int 字段的结构做交换,脚本传引用进 DLL,日志拿到 SamplesStruct: val1: 2 val2: 1,符合预期。 一旦结构里插进一个 char 字段,变成 int+char+int,同样的函数在脚本侧输出 SamplesStruct1: val1: -2144992512 cval: A val2: 33554435。int 字段直接变成无意义随机值,这不是异常,是未定义内存被读出来。 根因是 DLL 默认按最大字段(int,4 字节)对齐,而 MQL 结构默认 pack(1) 紧凑排列,字段偏移算错后读写越界。修法有二:要么脚本侧用 pack(sizeof(int)) 把结构按 4 字节对齐,重跑得到 val1: 3 cval: A val2: 2;要么 DLL 侧用 #pragma pack(1) 强制紧凑,结果一致。 别把正态当圣经 结构里塞构造函数或普通方法也不影响跨模块传递。实测带构造函数的 ESTRUCT2 在 DLL 与脚本双向保留方法,日志落 SamplesStruct2: val1: 5 cval: B val2: 4 The execution is correct!;即便脚本侧删掉方法只留数据字段,只要对齐一致,结果仍正确。写跨 DLL 结构时,先核对 pack 设定比纠结类方法省事得多。

MQL5 / C++
class=class="str">"cmt">//在 dll 里:
typedef class="kw">struct E_STRUCT {
    class="type">int val1;
    class="type">int val2;
}ESTRUCT, *PESTRUCT;
class=class="str">"cmt">//在 MQL 脚本里:
class="kw">struct ESTRUCT {
    class="type">int val1;
    class="type">int val2;
};
PROJECT2_API class="type">void SamplesStruct(PESTRUCT s) {
    class="type">int t;
    t = s->val2;
    s->val2 = s->val1;
    s->val1 = t;
}
class="macro">#class="kw">import "Project2.dll"
class="type">void SamplesStruct(ESTRUCT& s);
class="macro">#class="kw">import
....
ESTRUCT e;
e.val1 = class="num">1;
e.val2 = class="num">2;
SamplesStruct(e);
Print("SamplesStruct: val1: ",e.val1," val2: ",e.val2);
typedef class="kw">struct E_STRUCT1 {
    class="type">int val1;
    class="type">char cval;
    class="type">int val2;
}ESTRUCT1, *PESTRUCT1;
PROJECT2_API class="type">void SamplesStruct1(PESTRUCT1 s) {
    class="type">int t;
    t = s->val2;
    s->val2 = s->val1;
    s->val1 = t;
    s->cval = &class="macro">#x27;A&class="macro">#x27;;
}
class="kw">struct ESTRUCT1 pack(class="kw">sizeof(class="type">int)){
    class="type">int val1;
    class="type">char cval;
    class="type">int val2;
};
class="macro">#pragma pack(class="num">1)
typedef class="kw">struct E_STRUCT1 {
    class="type">int val1;
    class="type">char cval;
    class="type">int val2;
}ESTRUCT1, *PESTRUCT1;
class="macro">#pragma pack()
class="macro">#pragma pack(class="num">1)
typedef class="kw">struct E_STRUCT2 {
    E_STRUCT2() {
        val2 = class="num">15;
    }
    class="type">int val1;
    class="type">char cval;
    class="type">int val2;
}ESTRUCT2, *PESTRUCT2;
class="macro">#pragma pack()
PROJECT2_API class="type">void SamplesStruct2(PESTRUCT2 s) {
    class="type">int t;
    t = s->val2;
    s->val2 = s->val1;
    s->val1 = t;
    s->cval = &class="macro">#x27;B&class="macro">#x27;;
}
class="kw">struct ESTRUCT2 pack(class="num">1){
    ESTRUCT2() {
       val1 = -class="num">1;
       val2 = class="num">10;
    }
    class="type">int val1;
    class="type">char cval;

◍ DLL 里改结构体的字段回传

把结构体从 MQL5 侧传给编译好的 Project2.dll,函数内部改了成员再回传,是跨语言调用里最容易踩坑的一环。下面这段调用把 val1 设成 4、val2 设成 5,然后交给 SamplesStruct2 处理,最后把 cval 转成字符串打印。 真正要验证的是:e2 在调用后 val1、val2、cval 到底变成了什么。如果 DLL 没用引用语义或没正确写回内存,Print 出来的就会是旧的 4 和 5,而不是 DLL 里算出来的结果。 外汇与贵金属 EA 若用 DLL 做指标计算,这种结构体回传不一致会直接让信号错位,属于高风险集成点,上 MT5 跑一遍比看文档更实在。

MQL5 / C++
class="type">int f() { class="type">int val3 = val1 + val2; class="kw">return (val3);}
class="type">int val2;
};
class="macro">#class="kw">import "Project2.dll"
class="type">void SamplesStruct2(ESTRUCT2& s);
class="macro">#class="kw">import
...
ESTRUCT2 e2;
e2.val1 = class="num">4;
e2.val2 = class="num">5;
SamplesStruct2(e2);
t = CharToString(e2.cval);
Print("SamplesStruct2: val1: ",e2.val1," cval: ",t," val2: ",e2.val2);

哪些结构过不了 DLL 边界

MQL5 调用 DLL 时,参数传递有硬限制,官方文档列得很全,这里只挑最容易被忽略的一点说。 包裹了 string 的结构体不能直接丢给 DLL。下面这段代码在编译层看似无害,但实际调用 DLL 时不会被接受,更复杂的嵌套对象同样不行,没有例外通道。 [CODE] struct BAD_STRUCT { string simple_str; }; [/CODE] 逐行看:struct BAD_STRUCT 声明一个自定义结构;内部 string simple_str 放了一个 MQL5 字符串成员。就因为这一层 string 在结构里,整个结构便失去作为 DLL 参数的资格。写 EA 封装数据前,先拆掉这类包裹,改用基础类型数组或指针外传,否则运行时直接卡在调用端。

MQL5 / C++
class="kw">struct BAD_STRUCT {
  class="type">class="kw">string simple_str;
};

常见问题

大概率是 DllMain 里做了耗时的初始化或调用了不支持的接口,只在首次加载触发。把初始化挪到导出函数首次调用时做,别堆在 DllMain。
未初始化的字符串指针在跨边界时指向随机内存。调用前务必在脚本侧先给字符串赋空值或具体文本,再传地址进 DLL。
可以。把小布接进你的工作流,它能比对脚本侧与 DLL 侧结构体定义,标出错位字段和可能产生的脏数据,省去手动核对。
按值传递的结构体在 DLL 内改的是副本。需要回传就传指针或把结构体改为通过引用进出,确保写回原内存。
含动态数组、对象、字符串成员的结构通常过不了 DLL 边界。拆成基础类型数组或预分配缓冲再传,避开边界限制。