MetaTrader + C++:DLL

来源与维护说明(2026 年 9 月): 文章 1972 的 2019 年导出只包含前置元数据和空白正文,没有可保留的历史教程。本文明确披露为依据标题重建,并只使用当前 MetaQuotes 与 Microsoft 一手文档。示例是在 MetaTrader 5/MQL5 中调用离线算术函数;它不下单、不绕过 DLL 权限、不连接经纪商,也不承诺任何财务结果。

DLL 会在终端进程内执行原生代码。错误的签名、过期依赖、越界写入、阻塞调用或不可信二进制文件,都可能让终端停止运行或遭到破坏。应把 DLL 当作小型、带版本的系统接口,而不是绕过 MQL 或平台安全措施的捷径。

什么时候不该使用 DLL

如果工作能用 MQL5 或 EX5 库完成,就优先使用它们。以下情况应避免原生 DLL:

  • 目的只是普通计算、文件处理或 MQL5 已支持的数据结构;
  • 必须使用远程或 MQL5 Cloud 策略测试代理,因为这些代理不允许 DLL 调用;
  • 无法核实库的源码、发布者、依赖或构建记录;
  • 接口需要 C++ 对象、STL 容器、裸所有权转移、回调或长期保存 MQL 内存指针;
  • 必须把凭据、账户秘密、API 令牌或私钥嵌入二进制文件;
  • 失败或延迟可能阻塞调用它的 MQL 程序线程。

只有面对狭窄的原生依赖或经过测量的瓶颈时才使用 DLL。本示例不把交易决策、权限、品种状态或订单操作放进 DLL。

加载之前先处理信任与可见授权

MetaTrader 5 把 DLL 导入标为潜在危险。平台级 Allow DLL imports 选项提供默认值;应用的 Dependencies 选项卡会显示外部模块,并控制该应用的权限。不需要时保持平台默认关闭,检查显示的依赖,只对已经核实发布者和精确文件哈希的 DLL 授权。

MQL5 使用早期绑定:声明的 DLL 可能在 OnStart()OnInit() 运行前加载。因此,程序内权限检查适合记录日志,却不能替代平台的可见同意对话框。不要隐藏该对话框、自动修改终端配置,也不要建议用户为未知程序全局启用 DLL。

示例脚本使用 #property script_show_inputs 来显示属性窗口。保持 AutoTrading 关闭:这个演示不包含任何交易调用,也不需要交易权限。

定义一个小型、带版本的 ABI

第一个接口应只使用固定宽度整数、double、调用方拥有的数组、显式长度、调用方拥有的输出空间以及整数状态码。不要返回指针或 C++ 对象。

边界问题本文采用的契约
调用约定使用 MetaQuotes 对原生导入要求的 __stdcall
链接/导出extern "C"__declspec(dllexport);用 DUMPBIN /EXPORTS 验证最终名称。
架构本文为 x64 MetaTrader 5 终端构建 x64 DLL。32 位目标是独立制品,必须单独检查和测试。
版本MtApiVersion() 返回 1;不兼容变更必须使用新的 DLL 文件名和 API 版本。
内存所有输入与输出缓冲区都由 MQL 拥有;DLL 不保留也不释放它们。
错误0 表示成功;其他稳定整数表示参数或数值错误。

原生实现

保存为 mt_safe_math.cpp

#include <cmath>
#include <cstdint>

#if defined(_WIN32)
#define MT_API extern "C" __declspec(dllexport)
#define MT_CALL __stdcall
#else
#define MT_API extern "C"
#define MT_CALL
#endif

namespace {
constexpr std::int32_t kApiVersion = 1;
constexpr std::int32_t kMaxValues = 1'000'000;

enum Status : std::int32_t {
    kOk = 0,
    kInvalidArgument = 1,
    kInvalidOutput = 2,
    kNonFiniteValue = 3,
};
}

static_assert(sizeof(std::int32_t) == 4);
static_assert(sizeof(double) == 8);

MT_API std::int32_t MT_CALL MtApiVersion() noexcept {
    return kApiVersion;
}

MT_API std::int32_t MT_CALL MtMean(
    const double* values,
    std::int32_t count,
    double* out_mean) noexcept {
    if (out_mean == nullptr) {
        return kInvalidOutput;
    }
    *out_mean = 0.0;

    if (values == nullptr || count <= 0 || count > kMaxValues) {
        return kInvalidArgument;
    }

    double sum = 0.0;
    for (std::int32_t index = 0; index < count; ++index) {
        if (!std::isfinite(values[index])) {
            return kNonFiniteValue;
        }
        sum += values[index];
        if (!std::isfinite(sum)) {
            return kNonFiniteValue;
        }
    }

    *out_mean = sum / static_cast<double>(count);
    return kOk;
}

非 Windows 宏分支只用于在其他开发主机上单元测试算术与校验逻辑;MetaTrader 仍然需要 Windows DLL 构建。代码没有自定义 DllMain;Microsoft 建议让它保持最小,而 MSVC /LD 能提供默认入口点。

可移植核心测试

保存为 mt_safe_math_test.cpp

#include <cassert>
#include <cstdint>
#include <limits>

#if defined(_WIN32)
#define MT_CALL __stdcall
#else
#define MT_CALL
#endif

extern "C" std::int32_t MT_CALL MtApiVersion() noexcept;
extern "C" std::int32_t MT_CALL MtMean(
    const double* values,
    std::int32_t count,
    double* out_mean) noexcept;

int main() {
    assert(MtApiVersion() == 1);

    double values[] = {1.0, 2.0, 3.0, 4.0};
    double mean = -1.0;
    assert(MtMean(values, 4, &mean) == 0);
    assert(mean == 2.5);

    mean = -1.0;
    assert(MtMean(nullptr, 4, &mean) == 1);
    assert(mean == 0.0);

    double invalid[] = {1.0, std::numeric_limits<double>::infinity()};
    assert(MtMean(invalid, 2, &mean) == 3);
    assert(MtMean(values, 4, nullptr) == 2);
}

在安装了 GCC 的主机上,可移植核心测试为:

g++ -std=c++17 -O2 -Wall -Wextra -Wpedantic -Werror 
  mt_safe_math.cpp mt_safe_math_test.cpp -o mt_safe_math_test
./mt_safe_math_test

测试通过并不代表 Windows ABI、导出表、MetaTrader 权限流程或 MQL 声明已经验证;它们是下面的独立关卡。

MQL5 导入与冒烟脚本

保存为 MQL5/Scripts/MtSafeMathSmoke.mq5

#property script_show_inputs

#import "mt_safe_math_v1.dll"
int MtApiVersion();
int MtMean(double &values[], int count, double &out_mean);
#import

enum NativeStatus
  {
   MT_OK               = 0,
   MT_INVALID_ARGUMENT = 1,
   MT_INVALID_OUTPUT   = 2,
   MT_NON_FINITE_VALUE = 3
  };

void OnStart()
  {
   bool terminal_dlls=(bool)TerminalInfoInteger(TERMINAL_DLLS_ALLOWED);
   bool program_dlls=(bool)MQLInfoInteger(MQL_DLLS_ALLOWED);
   bool terminal_x64=(bool)TerminalInfoInteger(TERMINAL_X64);

   PrintFormat("dll-default=%s dll-program=%s x64=%s",
               terminal_dlls ? "true" : "false",
               program_dlls ? "true" : "false",
               terminal_x64 ? "true" : "false");

   if(!program_dlls || !terminal_x64)
     {
      Print("Stop: this smoke test requires explicit DLL consent and x64.");
      return;
     }

   int api_version=MtApiVersion();
   if(api_version!=1)
     {
      PrintFormat("Stop: incompatible native API version %d",api_version);
      return;
     }

   double values[]={1.0,2.0,3.0,4.0};
   double mean=0.0;
   ResetLastError();
   int status=MtMean(values,ArraySize(values),mean);
   int mql_error=GetLastError();

   PrintFormat("native-status=%d mean=%.8f mql-error=%d",
               status,mean,mql_error);
   if(status!=MT_OK)
      Print("Native calculation rejected the input; no other action was taken.");
  }

MQL 原型必须与原生参数顺序和大小完全一致。数组按引用传递,长度单独传入。尽管 MQL 导入语法不能表达原生的 const 限定,DLL 仍把数组视为只读。

构建、检查并标识 Windows 制品

使用 x64 Native Tools Command Prompt for Visual Studio 和干净的构建目录。记录源码提交、cl /Bv 输出、Windows SDK 版本、完整命令和最终哈希。可重复的发布流程比没有记录的 IDE 点击操作更重要。

cl /Bv
cl /nologo /std:c++17 /O2 /W4 /WX /EHsc /MT /LD mt_safe_math.cpp ^
  /link /OUT:mt_safe_math_v1.dll /INCREMENTAL:NO
dumpbin /headers mt_safe_math_v1.dll | findstr /i machine
dumpbin /exports mt_safe_math_v1.dll

除非头部报告预期 x64 machine,且导出表恰好包含可调用名称 MtApiVersionMtMean,否则立即停止。extern "C" 控制 C++ 名称改编,但修饰规则与调用约定会因架构而异。不要为了让 MQL 调用“看起来能工作”而猜测或重命名修饰后的导出;应有意修复并验证 ABI。

使用同一套已记录工具链和输入,在两个全新目录中各构建一次,然后比较 SHA-256。若不同,先调查构建输入,再把流程称为确定性构建。不要声称编译器或 SDK 升级后仍保证同一哈希。

Get-FileHash -LiteralPath .mt_safe_math_v1.dll -Algorithm SHA256
Get-AuthenticodeSignature -LiteralPath .mt_safe_math_v1.dll |
  Format-List Status,StatusMessage,SignerCertificate

哈希能标识精确字节,却不能证明由谁产生。通过经过认证的发布渠道发布哈希;向他人分发二进制文件时,对 DLL 签名并验证预期发布者。把发布清单、签名结果、导出项、架构、API 版本和测试结果一起保存。

字符串、数组、结构与所有权

MetaQuotes 记录的限制应直接决定接口形状:

MQL 值原生边界规则
简单标量除非显式声明为引用,否则按值传递;必须匹配精确大小。
double &array[]DLL 收到数据缓冲区起点。它不知道 ArraySetAsSeries;另行传入并验证元素数。
按值 stringDLL 收到复制后字符串缓冲区的指针;不得保留。
string &指向原始字符串缓冲区。修改与容量规则容易出错,不应出现在第一个 ABI 中。
文本协议优先使用以明确代码页(例如 CP_UTF8)生成、由调用方拥有的 uchar[],并传入字节长度与输出容量;定义长度是否包含终止符。
简单结构只考虑不含字符串、类、指针或动态数组的 POD 类结构;显式镜像打包与字段宽度。MQL5 结构默认紧凑排列。
复杂结构或字符串数组不要传给导入 DLL;MetaQuotes 明确限制这些类型。

导入调用返回后,绝不能保留 MQL 数组或字符串指针。绝不能在 DLL 中用 new/malloc 分配,再要求 MQL 或另一个运行时释放。Microsoft 记录了不同运行时之间跨 DLL 边界传递内存或 CRT 对象造成堆损坏的风险。由调用方分配缓冲区、显式提供容量、只在边界内写入,更容易审计。

错误、日志与秘密

同时使用两个错误通道,但不要混为一谈:

  • 原生函数返回有文档的状态码,并在失败时把调用方拥有的输出初始化为安全值;
  • MQL 单独记录 GetLastError(),用于终端/运行时诊断。

不要让 C++ 异常跨越 C ABI。导出函数保持 noexcept,把内部失败转换为稳定状态码。不要记录完整市场数据集、账户标识、包含用户名的路径或凭据。有效诊断记录应包含终端 build 与架构、DLL API 版本、预期发布 ID/哈希、函数名、元素数、原生状态、MQL 错误和耗时。

DLL 不能包含经纪商凭据、API 密钥、签名密钥、账户密码或“隐藏”端点;原生二进制文件可以被检查。如果以后设计确实需要特权外部访问,应单独定义威胁模型与秘密存储,不能把秘密偷塞进这个计算边界。

Strategy Tester 与演示测试计划

MetaQuotes 说明远程测试代理和 MQL5 Cloud 代理不能执行 DLL 调用;本地代理只有在启用 Allow import DLL 时才能调用。应正面设计这一限制,不要规避,也不要静默回退到未经审核的代码。

按以下顺序执行:

  1. 对无效、边界、非有限值与最大规模输入运行可移植核心测试。
  2. 在干净环境构建 x64 DLL;检查头部、依赖和导出;验证哈希/签名。
  3. 终端关闭时,把经过验证且带版本的 DLL 复制到终端数据目录的 MQL5/Libraries 文件夹。
  4. 使用一次性或模拟终端配置,保持 AutoTrading 关闭,并授予最小权限。
  5. 无警告编译冒烟脚本,检查 Dependencies,只明确允许这个已知 DLL。
  6. 连续运行两次并比较 Journal。预期结果:API 1、状态 0、均值 2.50000000,且没有交易动作。
  7. 如需 Strategy Tester 覆盖,只使用明确允许 DLL 的本地代理,并把远程/云优化标为不支持。
  8. 更广泛部署前先演练移除与回滚。

应在独立测试进程中对原生函数做模糊或压力测试,而不是反复冒险让终端崩溃。MetaQuotes 说明 DLL 代码在调用模块线程中执行,因此导出调用必须短小。

允许清单、部署、回滚与移除

允许清单条目应包含:

  • 唯一 DLL 文件名与原生 API 版本;
  • x64 架构与预期导出名称;
  • SHA-256 与预期签名者身份/状态;
  • 源码版本、MSVC/SDK 版本与构建命令;
  • 直接依赖及许可证;
  • 已通过测试与批准日期。

把 DLL 与匹配的 MQL 源码/EX5 作为一对审核并部署。使用带版本文件名,避免回滚时覆盖已加载模块。替换或删除 DLL 前:关闭该应用的 DLL 权限,从图表移除脚本/EA,关闭终端并确认已经退出,然后恢复上一对获批版本或删除带版本文件。之后在模拟配置中重新打开并运行冒烟测试。

终端可能仍加载 DLL 时绝不能覆盖它。不要把终端目录加入宽泛搜索路径,不要把依赖复制进 Windows 系统目录,也不要通过禁用安全控制来“修复”加载。

故障排查矩阵

症状只读检查安全响应
程序在 OnStart() 前停止Journal、Dependencies、DLL 权限、精确文件名只有完成信任验证后才恢复权限;确认文件位于 MQL5/Libraries
“Function not found”dumpbin /exports、拼写、API 版本重新构建匹配的一对;不要猜修饰名称。
DLL 无法加载dumpbin /headersdumpbin /dependents、终端 x64 状态提供正确架构和经过审核的依赖;不要从网上随机复制 DLL。
终端崩溃或输出损坏原型顺序/类型、数组长度、输出指针、原生单元测试禁用导入,关闭终端,移除候选 DLL,恢复上一获批版本。
本地测试成功,远程优化失败测试代理类型与官方 DLL 限制标为不支持远程/云,或移除 DLL 依赖;不要绕过限制。
哈希或签名不同发布清单、签名者、源码/工具链记录隔离文件,来源未解决前停止部署。

接受与停止标准

只有满足以下条件才接受发布:

  • 源码与构建输入已版本化,干净构建流程有记录;
  • 精确架构、依赖、两个导出名、API 版本、SHA-256 和签名状态都匹配允许清单;
  • 原生正向/反向测试通过,且没有无法解释的 sanitizer/静态分析发现;
  • MQL 冒烟脚本无警告编译,并在模拟配置中连续两次产生相同预期日志;
  • AutoTrading 保持关闭,不发生网络、文件、凭据或订单操作;
  • 已记录本地 Strategy Tester 行为与远程/云不支持;
  • 已成功演练回滚和完整移除。

出现意外依赖/导出、权限提示不一致、崩溃、挂起、越界报告、无法解释的非确定性构建、哈希/签名不符、源码/二进制/日志中出现秘密,或试图启用真实交易、绕过终端防护时,应立即停止。

一手文档

Leave a Reply