10 分钟掌握 MQL5 的 DLL(第二部分):使用 Visual Studio 2017 创建·进阶篇
◍ 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 侧主动调用,才不会掉进静默坑。
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;但导出的函数签名以后可能改、新人可能忘,统一开启能少踩坑。符号数组按通用方式传收即可,前文已覆盖,这里不重复。
PROJECT2_API <span class="keyword">class="type">void</span> SamplesW(wchar_t* pChar) { class="type">size_t len = wcslen(pChar); 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>& 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>& 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 设定比纠结类方法省事得多。
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 跑一遍比看文档更实在。
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 封装数据前,先拆掉这类包裹,改用基础类型数组或指针外传,否则运行时直接卡在调用端。
class="kw">struct BAD_STRUCT { class="type">class="kw">string simple_str; };