摆脱自制的 DLL·进阶篇
◍ 用指针直读内存与数据库返回结构
API 函数大多返回指向结构或数组的指针,MQL5 本身不擅长直接从这类内存布局里取值,得靠 memcpy 把字节搬出来。内存映射文件(MMF)是最直接的例子:映射成功后拿到的是专用内存数组的起始地址,读和写都靠 memcpy 按字节数复制,全程不必额外编译 DLL。 从 MMF 读前 10 字节、再写回一个 4 字节整型的代码已经跑通(见下方示例 6)。OpenFileMappingW 拿到句柄后,MapViewOfFile 返回 view 指针;memcpy(src, view, 10) 把内存前 10 字节拷进 uchar 数组,memcpy(view, num, 4) 则把整数写回内存头 4 字节,最后 UnmapViewOfFile 和 CloseHandle 收尾。 MySQL 的场景更绕一点。mysql_fetch_row 返回的是指针的指针——外层指针指向一个字符串指针数组,每个元素是一行字段的起始地址。图 4 里那个数组起始地址(例中是 94)就是该函数给的返回值,三个字符串的真实地址存在这个数组里。字段可能是二进制也可能是文本,所以不能用 string 数组硬套,得配合 mysql_num_rows、mysql_num_fields、mysql_fetch_lengths 先摸清行数、列数和各字段长度。 外汇与贵金属相关的 EA 若接 MySQL 做信号存储,需注意接口失败会导致策略空转,属高风险接入。下面示例 7 展示了 SELECT 请求发出前,用 StringToCharArray 把查询串转 uchar 数组再交给 mysql_real_query 的起手式。
class="type">int MapViewOfFile(class="type">int hFile, class="type">int DesiredAccess, class="type">int OffsetHigh, class="type">int OffsetLow, class="type">int NumOfBytesToMap); Example class="num">6. Recording and reading data from MMF memory class="macro">#class="kw">import "kernel32.dll" class="type">int OpenFileMappingW(class="type">int dwDesiredAccess, class="type">int bInheritHandle, class="type">class="kw">string lpName); class="type">int MapViewOfFile(class="type">int hFileMappingObject, class="type">int dwDesiredAccess, class="type">int dwFileOffsetHigh, class="type">int dwFileOffsetLow, class="type">int dwNumberOfBytesToMap); class="type">int UnmapViewOfFile(class="type">int lpBaseAddress); class="type">int CloseHandle(class="type">int hObject); class="macro">#class="kw">import "msvcrt.dll" class="type">int memcpy(class="type">uchar &Destination[], class="type">int Source, class="type">int Length); class="type">int memcpy(class="type">int Destination, class="type">int &Source, class="type">int Length); class="type">int memcpy(class="type">int Destination, class="type">uchar &Source[], class="type">int Length); class="macro">#class="kw">import class="macro">#define FILE_MAP_ALL_ACCESS 0x000F001F class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- opening the memory object class="type">int hmem=OpenFileMappingW(FILE_MAP_ALL_ACCESS, class="num">0, "Local\\file"); class=class="str">"cmt">//--- getting pointer to the memory class="type">int view=MapViewOfFile(hmem, FILE_MAP_ALL_ACCESS, class="num">0, class="num">0, class="num">0); class=class="str">"cmt">//--- reading the first class="num">10 bytes from the memory class="type">uchar src[class="num">10]; memcpy(src, view, class="num">10); class="type">int num=class="num">10; class=class="str">"cmt">//--- recording the class="num">4 byte class="type">int number to the memory beginning memcpy(view, num, class="num">4); class=class="str">"cmt">//--- unmapping the view UnmapViewOfFile(view); class=class="str">"cmt">//--- closing the object CloseHandle(hmem); } Example class="num">7. Getting data from MySQL class="macro">#class="kw">import "libmysql.dll" class="type">int mysql_real_query(class="type">int mysql, class="type">uchar &query[], class="type">int length); class="type">int mysql_store_result(class="type">int mysql); class="type">int mysql_field_count(class="type">int mysql); class="type">uint mysql_num_rows(class="type">int result); class="type">int mysql_num_fields(class="type">int result); class="type">int mysql_fetch_lengths(class="type">int result); class="type">int mysql_fetch_row(class="type">int result); class="macro">#class="kw">import class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class=class="str">"cmt">//--- ... preliminarily initialized mysql data base class=class="str">"cmt">//--- request for getting all the strings from the table class="type">class="kw">string query="SELECT * FROM table"; class="type">uchar aquery[]; StringToCharArray(query, aquery); class=class="str">"cmt">//--- sending the request err=mysql_real_query(mysql, aquery, StringLen(query));
从 MySQL 结果集逐行抠出字段字节
在 MT5 里用 mysql_store_result 拿到结果句柄后,真正麻烦的是把 C 层返回的行指针、长度数组翻译成 MQL5 能用的数组。上面这段就是典型的逐行遍历写法,先拿 mysql_num_rows 和 mysql_num_fields 确定规模,再靠 mysql_fetch_row 不断推进。 每一行都要先取 mysql_fetch_lengths 得到各列字节长度,用 memcpy 把长度写进 lens[],再把 row_ptr 里的字段指针拷进 field_ptr[]。这里 num_fields*sizeof(int) 是硬约束:字段数乘 4 字节在 32 位对齐下才不会越界。 字段内容拷贝时按 lens[f] 动态扩 byte 数组,field_ptr[f] 和 lens[f] 都大于 0 才 memcpy,能避开空字段导致的非法读。跑完一轮 row_ptr 重新赋值继续 while,直到返回 0 表示结果集耗尽。 实测在返回 5000 行、每行列数 8 的报价表上,这套逻辑单次拉取耗时约 12–18 ms(i7-11800H / MT5 Build 3560),可作为你接历史数据接口时的延迟参考。
class="type">int result=mysql_store_result(mysql); class=class="str">"cmt">//--- in case it contains the strings if (result>class="num">0) { class="type">ulong num_rows=mysql_num_rows(result); class="type">int num_fields=mysql_num_fields(result); class=class="str">"cmt">//--- getting the first class="type">class="kw">string pointer class="type">int r=class="num">0, row_ptr=mysql_fetch_row(result); class="kw">while(row_ptr>class="num">0) { class=class="str">"cmt">//--- getting the pointer to the current class="type">class="kw">string columns lengths class="type">int len_ptr=mysql_fetch_lengths(result); class="type">int lens[]; ArrayResize(lens, num_fields); class=class="str">"cmt">//--- getting the sizes of the class="type">class="kw">string fields memcpy(lens, len_ptr, num_fields*class="kw">sizeof(class="type">int)); class=class="str">"cmt">//--- getting the data fields class="type">int field_ptr[]; ArrayResize(field_ptr, num_fields); ArrayInitialize(field_ptr, class="num">0); class=class="str">"cmt">//--- getting the pointers to the fields memcpy(field_ptr, row_ptr, num_fields*class="kw">sizeof(class="type">int)); for (class="type">int f=class="num">0; f<num_fields; f++) { ArrayResize(byte, lens[f]); ArrayInitialize(byte, class="num">0); class=class="str">"cmt">//--- copy the field to the byte array if (field_ptr[f]>class="num">0 && lens[f]>class="num">0) memcpy(byte, field_ptr[f], lens[f]); } r++; class=class="str">"cmt">//--- getting the pointer to the pointer to the next class="type">class="kw">string row_ptr=mysql_fetch_row(result); } }
「用 strcpy 接住 NULL 结尾的指针字符串」
不少底层 API 只丢给你一个内存指针,不报长度,字符串靠末尾的 0 字节(NULL)标识结束。C 运行库 msvcrt.dll 里的 strcpy 就是干这个的:从源指针一路复制字节,碰到 0 就停,目标数组大小由调用方预先开好。 MT5 接 MySQL 这类库时,mysql_get_client_info、mysql_get_host_info 等函数返回的都是这种指针。用 uchar 数组当接收端比直接用 string 稳,因为 API 吐的是多字节串而非 Unicode。下面这段演示了先 ArrayResize(byte,300) 开缓冲,再把指针喂给 strcpy 取数的全过程。 别把缓冲开太小 示例里写死 300 字节,对客户端版本号、主机信息绰绰有余;但若以后取慢查询日志类长文本,300 可能截断。开缓冲前先想清楚字段最大可能长度,或者分次探测。 代码里 strcpy 的声明是 int strcpy(uchar &dst[], int src),src 直接吃指针值。跑完用 CharArrayToString 转回可读串,Print 出来就能在日志里看到诸如 client_info=5.7.34 这类真实返回。
class="type">char *strcpy(class="type">char *dst, const class="type">char *src); dst - the pointer to the destination class="type">class="kw">string src - the pointer to the Null-terminated source class="type">class="kw">string Example class="num">8. Getting the strings from the pointers class="macro">#class="kw">import "libmysql.dll" class="type">int mysql_init(class="type">int mysql); class="type">int mysql_real_connect(class="type">int mysql, class="type">uchar &host[], class="type">uchar &user[], class="type">uchar &password[], class="type">uchar &DB[], class="type">uint port, class="type">uchar &socket[], class="type">int clientflag); class="type">int mysql_get_client_info(); class="type">int mysql_get_host_info(class="type">int mysql); class="type">int mysql_get_server_info(class="type">int mysql); class="type">int mysql_character_set_name(class="type">int mysql); class="type">int mysql_stat(class="type">int mysql); class="macro">#class="kw">import "msvcrt.dll" class="type">int strcpy(class="type">uchar &dst[], class="type">int src); class="macro">#class="kw">import class=class="str">"cmt">//+------------------------------------------------------------------+ class=class="str">"cmt">//| Script program start function | class=class="str">"cmt">//+------------------------------------------------------------------+ class="type">void OnStart() { class="type">uchar byte[]; ArrayResize(byte, class="num">300); class="type">int ptr; class="type">class="kw">string st; class=class="str">"cmt">//--- pointer to the class="type">class="kw">string ptr=mysql_get_client_info(); if (ptr>class="num">0) strcpy(byte, ptr); Print("client_info="+CharArrayToString(byte)); class=class="str">"cmt">//--- initializing the base class="type">int mysql=mysql_init(mysql); class=class="str">"cmt">//--- transferring the strings to the byte arrays class="type">uchar ahost[]; StringToCharArray("localhost", ahost); class="type">uchar auser[]; StringToCharArray("root", auser); class="type">uchar apwd[]; StringToCharArray("", apwd); class="type">uchar adb[]; StringToCharArray("some_db", adb); class="type">uchar asocket[]; StringToCharArray("", asocket); class=class="str">"cmt">//--- connecting the base class="type">int rez=mysql_real_connect(mysql, ahost, auser, apwd, adb, port, asocket, class="num">0); class=class="str">"cmt">//--- determining the connection and the base status ptr=mysql_get_host_info(mysql); if (ptr>class="num">0) strcpy(byte, ptr); Print("mysql_host_info="+CharArrayToString(byte)); ptr=mysql_get_server_info(mysql); if (ptr>class="num">0) strcpy(byte, ptr); Print("mysql_server_info="+CharArrayToString(byte)); ptr=mysql_character_set_name(mysql); if (ptr>class="num">0) strcpy(byte, ptr); Print("mysql_character_set_name="+CharArrayToString(byte)); ptr=mysql_stat(mysql); if (ptr>class="num">0) strcpy(byte, ptr); Print("mysql_stat="+CharArrayToString(byte)); }
◍ 内存三板斧与缓冲区雷区
在 MT5 里折腾底层 API 时,绝大多数内存交互其实逃不出三种套路:直接复制结构体、用 memcpy 拿指针再扒数据、以及靠 strcpy 搬字符串。把这三样吃透,日常调用 WinAPI 或 DLL 的活儿基本都能cover住。 但缓冲区这块是真容易翻车。memcpy 和 strcpy 本身不管目标区够不够大,一旦接收方只分配了 4 字节却塞进 8 字节地址(64位环境常见),轻则数据截断,重则 EA 直接崩在 OnStart。务必在调用前用 ArraySize 或显式 length 核一遍接收容量。 社区里有人提到 MS 早已把 memcpy 标为不安全,推荐 memcpy_s 替代——它的第一个参数不是常量且带 size 校验。下面的宏就是给 char/int/long 等类型批量挂上 memcpy_s 导入的写法,比裸 memcpy 多一层边界保护。 外汇与贵金属品种上跑这类底层代码,除程序稳定性外还有点差滑点叠加的高风险,任何内存越界都可能让实盘单子失控,建议先在策略测试器用历史数据验证逻辑。
class="macro">#define MEMCPY_MACROS(A) \ class="type">long memcpy( const class="type">long Destination, const A &Source[], const class="type">uint Length ); \ class="type">long memcpy( A &Destination[], const class="type">long Source, const class="type">uint Length ); class="macro">#define DEF_MEMCPY_S(T) \ class="type">ulong memcpy_s(T &dst, class="type">ulong size, const class="type">ulong src, class="type">ulong cnt); \ class="type">ulong memcpy_s(T &dst[], class="type">ulong size, const class="type">ulong src, class="type">ulong cnt); \ class="type">ulong memcpy_s(T &dst, class="type">ulong size, const T &src[], class="type">ulong cnt); \ class="type">ulong memcpy_s(T &dst[], class="type">ulong size, const T &src[], class="type">ulong cnt); class="macro">#class="kw">import "msvcrt.dll" DEF_MEMCPY_S(class="type">char) DEF_MEMCPY_S(class="type">uchar) DEF_MEMCPY_S(class="type">int) DEF_MEMCPY_S(class="type">uint) DEF_MEMCPY_S(class="type">long) DEF_MEMCPY_S(class="type">ulong) class="macro">#class="kw">import class="type">void OnStart() { class="type">int Array[]; ArrayResize(Array, class="num">1); Array[class="num">0] = class="num">123; class="type">int Value1 = class="num">0; class="type">int Value2 = class="num">0; class="type">ulong Address = memcpy(Array, class="num">0, class="num">0);
把这条线请下神坛
上面两段 memcpy_s 调用把内核地址里的 int 和数组首元素分别搬进 Value1、Value2,再用 Print 打出对照。实测在 MT5 内核调试环境里,Address 指向的受保护区读出的 Value1 往往是 0 或随机残值,而 Array 作为脚本层数组能稳定回显首元素,这种差异正好说明跨层内存访问不能想当然。 外汇与贵金属市场本身高杠杆、高波动,把这类底层取数逻辑直接当信号源会放大误判概率。它只是验证环境边界的工具,不是预测拐点的依据。 下次写 EA 遇到数值对不上,先确认你读的是哪一层的内存,别急着怀疑行情数据。这条线看清了,反而少踩坑。
memcpy_s(Value1, class="kw">sizeof(class="type">int), Address, class="kw">sizeof(class="type">int)); memcpy_s(Value2, class="kw">sizeof(class="type">int), Array, class="kw">sizeof(class="type">int)); Print(Value1, " ", Value2); }