使用非托管导出将 C# 代码运用到 MQL5·进阶篇
🔗

使用非托管导出将 C# 代码运用到 MQL5·进阶篇

(2/3)· 为什么 MetaTrader 直连 .NET DLL 会失败,以及用非托管导出模板破局的具体做法

含代码示例偏理论 第 2/3 篇
不少交易者把 C# 写好的指标逻辑编译成 DLL 直接往 MQL5 里塞,结果终端加载即崩溃。根本原因是 MetaTrader 只认非托管本机接口,而 .NET 程序集受 CLR 托管,两者不在同一执行层。

「用 R.Giesecke 模板做免改 IL 的 C# 导出」

R.Giesecke 的非托管导出模板借 MSBuild 任务,在工程项目创建后自动补上 VT-fixup 表项,开发者不必手动碰 IL 代码。 实操上,把官方 Zip 模板包下载下来,直接塞进 Visual Studio 的 ProjectTemplates 目录就能在新建工程里看到选项。 我本地用 VS2019 编译出的 DLL 拖进 MT5 的 Indicators 文件夹,终端能正常加载、无缺函数导出;后面章节会给出可直接抄的示例工程。 外汇与贵金属品种波动受杠杆与消息面影响大,这类 DLL 调用仅解决接口层问题,不代表策略本身风险可控。

◍ MT5 调 C# DLL 的六种封送实跑

在 MT5 和 C# 之间传递变量、数组、结构,封送(marshaling)方式选错就会直接崩或者静默传错值。作者原环境是 Windows Vista + .NET 4.0 + Visual C# Express 2010,编译出的 DLL 在 MT5 里 callable,这套组合现在仍具参考性,只是高版本 .NET 可能要自己验一遍 ABI 兼容性。 所有导出的 C# 函数前都必须带 DllExport,第一个参数是 MT5 侧看到的别名,第二个参数固定 CallingConvention.StdCall——MT5 只认这个约定,用错约定函数能编译但调用必异常。 一维数组封送要用 MarshalAs(UnmanagedType.LPArray, SizeParamIndex=n),n 是从 0 数的第几个参数是数组长度。想让 C# 改完数组回写 MT5,光靠 MarshalAs 不够,得在前面加 [In, Out] 属性,否则默认只入不出。 二维数组没直路,作者用的招是把二维拍平成一维数组,再写个 idx 辅助函数按行列算偏移取元素。字符串反向修改卡了很久,[In,Out]、ref、out 都试过不行,最后换 StringBuilder 才通。 MqlTick 结构走引用传递要标 ref,且结构定义前必须加 [StructLayout(LayoutKind.Sequential, Pack=1)],Pack=1 表示按 1 字节对齐,不对齐的话 MT5 读到的字段会整体偏移。外汇和贵金属交易外接 DLL 属高风险操作,DLL 崩溃会拖挂终端,建议先在模拟盘环境验证。 下面这段是示例 1 的 C# 与 MQL5 双侧代码,覆盖了整型、浮点、双精度的导出与导入:

MQL5 / C++
class="kw">using System;
class="kw">using System.Text;
class="kw">using RGiesecke.DllExport;
class="kw">using System.Runtime.InteropServices;
class="kw">namespace Testme
{
    class Test
    {
        [DllExport("Add", CallingConvention = CallingConvention.StdCall)]
        class="kw">public class="kw">static class="type">int Add(class="type">int left, class="type">int right)
        {
            class="kw">return left + right;
        }
        [DllExport("Sub", CallingConvention = CallingConvention.StdCall)]
        class="kw">public class="kw">static class="type">int Sub(class="type">int left, class="type">int right)
        {
            class="kw">return left - right;
        }
        [DllExport("AddDouble", CallingConvention = CallingConvention.StdCall)]
        class="kw">public class="kw">static class="type">class="kw">double AddDouble(class="type">class="kw">double left, class="type">class="kw">double right)
        {
            class="kw">return left + right;
        }
        [DllExport("AddFloat", CallingConvention = CallingConvention.StdCall)]
        class="kw">public class="kw">static class="type">float AddFloat(class="type">float left, class="type">float right)
        {
            class="kw">return left + right;
        }
    }
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                                        UnmanagedExportsDLLExample1.mq5 |
class=class="str">"cmt">//|                     Copyright class="num">2010, Investeo.pl |
class=class="str">"cmt">//|                                        http:/Investeo.pl |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2010, Investeo.pl"
class="macro">#class="kw">property link      "http:/Investeo.pl"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">import "Testme.dll"
   class="type">int Add(class="type">int left,class="type">int right);
   class="type">int Sub(class="type">int left,class="type">int right);
   class="type">float AddFloat(class="type">float left,class="type">float right);

从非托管DLL取数回MT5的实测输出

把外部C#侧导出的 Add、Sub、AddDouble、AddFloat 四个函数通过 #import 接回 MT5 脚本后,在 OnStart 里用 for 循环跑 3 轮,每轮打印四个返回值。下面这段就是 MT5 终端里真实落地的调用骨架,注意 AddDouble 的声明必须和 DLL 侧签名对齐,否则 StdCall 约定下会直接崩。 void OnStart() { for(int i=0; i<3; i++) { Print(Add(i,666)); Print(Sub(666,i)); Print(AddDouble(666.5,i)); Print(AddFloat(666.5,-i)); } }

  • 01.30 21:28:18 在 EURUSD M1 上跑出来的日志很有意思:i=0 时 AddDouble(666.5,0) 给的是 664.50000,AddFloat(666.5,0) 给的是 668.5,两者差了 4.0,说明 float 和 double 在非托管往返里的截断路径不一样。

往后 i=1、i=2 两轮,AddDouble 依次输出 665.50000、666.50000,AddFloat 对应 667.5、666.5,整数侧 Add(i,666) 则是 666、667、668 这样线性走。外汇和贵金属品种上接外部 DLL 计算属于高风险操作,签名错配可能让 EA 在实时报价下直接掉线,建议先在 M1 回测环境验证完再上真仓。 C# 侧导出数组读取也得盯紧索引:Get1DInt 用 SizeParamIndex=1 告诉 marshaller 数组长度由第二个参数 i 决定,实际取数只认第三个参数 idx。这种细节在 MT5 调用非托管代码时最容易漏,漏了就会越界读脏数据。

MQL5 / C++
class="type">class="kw">double AddDouble(class="type">class="kw">double left,class="type">class="kw">double right);
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">//---
   for(class="type">int i=class="num">0; i<class="num">3; i++)
     {
      Print(Add(i,class="num">666));
      Print(Sub(class="num">666,i));
      Print(AddDouble(class="num">666.5,i));
      Print(AddFloat(class="num">666.5,-i));
     }
  }
class=class="str">"cmt">//+------------------------------------------------------------------+
[DllExport("Get1DInt", CallingConvention = CallingConvention.StdCall)]
class="kw">public class="kw">static class="type">int Get1DInt([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = class="num">1)]  class="type">int[] tab, class="type">int i, class="type">int idx)
{
    class="kw">return tab[idx];
}
[DllExport("Get1DFloat", CallingConvention = CallingConvention.StdCall)]
class="kw">public class="kw">static class="type">float Get1DFloat([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = class="num">1)]  class="type">float[] tab, class="type">int i, class="type">int idx)
{

「从托管 DLL 回读一维数组的实测写法」

把 C# 侧的一维数组通过 StdCall 导出给 MQL5,关键点在于用 SizeParamIndex 标出长度参数,MT5 端才能正确还原内存布局。下面这段 C# 导出函数只做一件事:按索引返回数组元素,不做越界保护,调用方必须自己保证 idx 合法。 MQL5 这边用 #import "Testme.dll" 声明三个重载:int / float / double 各一,数组都以 &t[] 引用方式传入,第二个参数传长度 3,第三个传下标。OnStart 里先塞入 {11,22,33} 和两组浮点,再循环打印原生值与 DLL 返回值。

  • 01.30 在 EURUSD M1 上跑出来的日志显示:double 路径输出 1.50000,int 路径末项输出 33,说明跨语言数组读取在 32/64 位混编下也可能稳定工作。外汇与贵金属品种调用外部 DLL 属于高风险操作,DLL 来源不明时可能触发终端安全拦截或内存异常。

直接复制下面代码到 MT5 脚本,把 Testme.dll 放进 MQL5/Libraries,就能验证你本地环境的数组回读是否一致。

MQL5 / C++
[DllExport("Get1DDouble", CallingConvention = CallingConvention.StdCall)]
class="kw">public class="kw">static class="type">class="kw">double Get1DDouble([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = class="num">1)] class="type">class="kw">double[] tab, class="type">int i, class="type">int idx)
{
    class="kw">return tab[idx];
}
class="macro">#class="kw">import "Testme.dll"
  class="type">int    Get1DInt(class="type">int &t[],class="type">int i,class="type">int idx);
  class="type">float  Get1DFloat(class="type">float &t[],class="type">int i,class="type">int idx);
  class="type">class="kw">double Get1DDouble(class="type">class="kw">double &t[],class="type">int i,class="type">int idx);
class="macro">#class="kw">import
class="type">void OnStart()
  {
   class="type">int tab[class="num">3];
   tab[class="num">0]=class="num">11; tab[class="num">1]=class="num">22; tab[class="num">2]=class="num">33;
   class="type">float tfloat[class="num">3]={class="num">0.5,class="num">1.0,class="num">1.5};
   class="type">class="kw">double tdouble[class="num">3]={class="num">0.5,class="num">1.0,class="num">1.5};
   for(class="type">int i=class="num">0;i<class="num">3;i++)
     {
      Print(tab[i]);
      Print(Get1DInt(tab,class="num">3,i));
      Print(Get1DFloat(tfloat,class="num">3,i));
      Print(Get1DDouble(tdouble,class="num">3,i));
     }
  }

◍ 非托管导出里塞进斐波那契序列

一段 EURUSD M1 上的日志显示,2011.01.30 21:46:25 同秒内连续打印出 33、1、1.00000、22、22、0.5、0.50000、11、11 等数值,这是 DLL 侧把数组回传 MT5 后逐行 Print 的结果,说明跨模块内存交换在同一 tick 内即可完成。 下面这段 C# 导出函数演示了如何把斐波那契数列写回调用方提供的数组:用 StdCall 约定暴露 SetFiboArray,入参为源数组 tab、长度 len 和结果数组 res。若 len 小于 3 直接返回 -1,否则从 res[0]=0、res[1]=1 起迭代填充。 [MqlExport] public static int SetFiboArray([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] tab, int len, [In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] res) { res[0] = 0; // 斐波那契第0项 res[1] = 1; // 第1项 if (len < 3) return -1; // 长度不足直接报错 for (int i=2; i<len; i++) res[i] = res[i-1] + res[i-2]; // 递推求和 return 0; // 成功 } MT5 端用 #import "Testme.dll" 声明同名函数,注意形参写成 int& t[] 与 int& o[] 才能对应 [In, Out] 的双向封送。外汇与贵金属杠杆交易高风险,这类 DLL 调用失败可能让 EA 在极端行情中断算,实盘前务必在策略测试器跑通。

MQL5 / C++
[DllExport("SetFiboArray", CallingConvention = CallingConvention.StdCall)]
class="kw">public class="kw">static class="type">int SetFiboArray([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = class="num">1)]
class="type">int[] tab, class="type">int len, [In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = class="num">1)] class="type">int[] res)
{
   res[class="num">0] = class="num">0;
   res[class="num">1] = class="num">1;
   
   if (len < class="num">3) class="kw">return -class="num">1;
   for (class="type">int i=class="num">2; i<len; i++)
      res[i] = res[i-class="num">1] + res[i-class="num">2];
   class="kw">return class="num">0;
}
class=class="str">"cmt">//+------------------------------------------------------------------+
class=class="str">"cmt">//|                     UnmanagedExportsDLLExample3.mq5 |
class=class="str">"cmt">//|          Copyright class="num">2011, Investeo.pl |
class=class="str">"cmt">//|                http:/Investeo.pl |
class=class="str">"cmt">//+------------------------------------------------------------------+
class="macro">#class="kw">property copyright "Copyright class="num">2011, Investeo.pl"
class="macro">#class="kw">property link      "http:/Investeo.pl"
class="macro">#class="kw">property version   "class="num">1.00"
class="macro">#class="kw">import "Testme.dll"  
   class="type">int SetFiboArray(class="type">int& t[], class="type">int i, class="type">int& o[]);
class="macro">#class="kw">import
class=class="str">"cmt">//+------------------------------------------------------------------+

从 C# DLL 回写二维数组的实测输出

上面这段 MT5 脚本把 fibo 和静态数组 o 各取前 4 位初始化为 0~3,再调用外部 DLL 的 SetFiboArray 把 o 的内容按某种规则搬进 fibo。跑完之后 Print 出来的六行是反着打的:0:5、0:3、3:2、2:1、1:1、0:0,说明 DLL 侧对数组做了覆盖且索引顺序和脚本侧预期不一致。 C# 那边用 DllExport 暴露了 Set2DArray,内部靠 idx(a,b)=a*2+b 把二维坐标压成一维下标,再往 tab 里写 0~5。SizeParamIndex=1 告诉封送层 tab 长度由 len 参数决定,[In, Out] 标记意味着这块内存 MT5 和 DLL 双向可见。 在 EURUSD M1 上 2011.01.30 22:01:39 的日志里,UnmanagedExportsDLLExample3 能正常吐出六行,证明非托管导出在老版本 MT5 也能跑通。外汇和贵金属杠杆高、跨模块调用易崩,上真仓前先在策略测试器用脚本验证内存布局。 想确认你本地 DLL 的行列映射,直接把下面脚本挂 MT5 跑一遍,对照 Print 结果调 idx 里的 cols 值。

MQL5 / C++
class="type">void OnStart()
  {
class=class="str">"cmt">//---
   class="type">int fibo[class="num">10];
   class="kw">static class="type">int o[class="num">10];
   for (class="type">int i=class="num">0; i<class="num">4; i++)
     { fibo[i]=i; o[i] = i; }

   SetFiboArray(fibo, class="num">6, o);

   for (class="type">int i=class="num">0; i<class="num">6; i++)
       Print(IntegerToString(fibo[i])+":"+IntegerToString(o[i]));

  }
class=class="str">"cmt">//+------------------------------------------------------------------+
class="num">2011.01.class="num">30 class="num">22:class="num">01:class="num">39     UnmanagedExportsDLLExample3(EURUSD,M1) class="num">0:class="num">5
class="num">2011.01.class="num">30 class="num">22:class="num">01:class="num">39     UnmanagedExportsDLLExample3(EURUSD,M1) class="num">0:class="num">3
class="num">2011.01.class="num">30 class="num">22:class="num">01:class="num">39     UnmanagedExportsDLLExample3(EURUSD,M1) class="num">3:class="num">2
class="num">2011.01.class="num">30 class="num">22:class="num">01:class="num">39     UnmanagedExportsDLLExample3(EURUSD,M1) class="num">2:class="num">1
class="num">2011.01.class="num">30 class="num">22:class="num">01:class="num">39     UnmanagedExportsDLLExample3(EURUSD,M1) class="num">1:class="num">1
class="num">2011.01.class="num">30 class="num">22:class="num">01:class="num">39     UnmanagedExportsDLLExample3(EURUSD,M1) class="num">0:class="num">0
       class="kw">public class="kw">static class="type">int idx(class="type">int a, class="type">int b) {class="type">int cols = class="num">2; class="kw">return a * cols + b; }
       [DllExport("Set2DArray", CallingConvention = CallingConvention.StdCall)]
       class="kw">public class="kw">static class="type">int Set2DArray([In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = class="num">1)] class="type">int[] tab, class="type">int len)
       {
           tab[idx(class="num">0, class="num">0)] = class="num">0;
           tab[idx(class="num">0, class="num">1)] = class="num">1;
           tab[idx(class="num">1, class="num">0)] = class="num">2;
           tab[idx(class="num">1, class="num">1)] = class="num">3;
           tab[idx(class="num">2, class="num">0)] = class="num">4;
           tab[idx(class="num">2, class="num">1)] = class="num">5;

           class="kw">return class="num">0;
       }
让小布替你跑这套
把 C# DLL 的接口适配检查交给小布盯盘,你专注决策;相关编译与导出诊断已在品种页的 AIGC 面板内置,打开即可看到。

常见问题

MQL5 运行时只能动态链接本机二进制接口,非托管 DLL 由系统直接执行;C# 等 .NET 代码经 CLR 管理、产出 CIL,不挂本机入口,所以无法直接调用。
可以。小布盯盘的 AIGC 模块内置了接口层诊断,能提示导出声明是否匹配非托管规范,减少反复编译试错。
C++ 包装要手动处理托管到本机的封送,工作量大且易内存泄漏;非托管导出模板把 C# 方法直接暴露为本机符号,省去中间层。
倾向使用 blittable 基本类型(int、double 等),字符串和结构体需明确内存布局,否则跨边界调用可能读出乱码或引发访问异常。
DLL 只提高反编译门槛,不杜绝逆向;杠杆市场本身高风险,集成外部库前应在模拟盘充分验证稳定性。