使用非托管导出将 C# 代码运用到 MQL5·进阶篇
(2/3)· 为什么 MetaTrader 直连 .NET DLL 会失败,以及用非托管导出模板破局的具体做法
「用 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 双侧代码,覆盖了整型、浮点、双精度的导出与导入:
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 调用非托管代码时最容易漏,漏了就会越界读脏数据。
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,就能验证你本地环境的数组回读是否一致。
[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 在极端行情中断算,实盘前务必在策略测试器跑通。
[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 值。
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; }