摆脱自制的 DLL·进阶篇
📘

摆脱自制的 DLL·进阶篇

第 2/2 篇

◍ 用指针直读内存与数据库返回结构

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 的起手式。

MQL5 / C++
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),可作为你接历史数据接口时的延迟参考。

MQL5 / C++
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 这类真实返回。

MQL5 / C++
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 多一层边界保护。 外汇与贵金属品种上跑这类底层代码,除程序稳定性外还有点差滑点叠加的高风险,任何内存越界都可能让实盘单子失控,建议先在策略测试器用历史数据验证逻辑。

MQL5 / C++
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 遇到数值对不上,先确认你读的是哪一层的内存,别急着怀疑行情数据。这条线看清了,反而少踩坑。

MQL5 / C++
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);
}

常见问题

先用内存三板斧确认缓冲区大小和偏移,别盲目解引用;读前校验指针非空,崩的概率会低很多。
按结果集行结构循环,用偏移定位每个字段起始字节,逐行拷贝即可,注意字段间对齐空隙。
小布可以扫描你贴的代码,标出疑似越界和未初始化指针的位置,并给改写建议,省去人工逐行排查。
拷贝前先判指针有效性,长度用 strnlen 限死,避免读到非 NULL 结尾的脏内存。
自制DLL绕过了平台内存管理,缓冲区雷区和指针失效难排查;非必要场景建议下神坛,用原生方案替代。