简介
长期以来我一直在寻找一个简单的解决方案,让我能够在 MQL5 中使用托管模式的 C# DLL。在阅读很多文章之后,我准备好了针对托管 DLL 实施 C++ 包装程序,此时我想到了一个超群的解决方案,为我节省了很多工作时间。
该解决方案是导出托管 C# 代码以供非托管应用程序使用的一个简单示例。在本文中,我将提供有关托管模式 DLL 的背景知识,说明为什么不能直接从 MetaTrader 访问它们的原因,并且介绍我发现的能够从 MetaTrader 使用托管代码的解决方案。
我将提供一个非托管导出模板的简单运用的例子,并且继续介绍我的全部发现。这应该为试图在 MetaTrader 5 中使用 C# DLL 代码的任何人提供良好的背景知识。
1. 托管代码与非托管代码的比较
因为大多数读者可能不知道托管代码和非托管代码之间的差异,我将用几句话进行说明。基本而言,MetaTrader 使用 MQL 语言来实施交易规则、指标、EA 交易和脚本。然而,它可以使用已经以其他语言实施的库,在运行时动态地链接它们。这些库也被称为 DLL,即动态链接库。
事实上,库是二进制文件,包含编译后的源代码,可被若干外部程序调用以执行具体操作。例如,神经网络库可以导出用于神经网络培训与测试的函数,派生库可导出不同派生的计算,矩阵库可导出有关矩阵的操作。用于 MetaTrader 的 DLL 变得越来越流行,因为它们让隐藏指标或 EA 交易的实施部分成为可能。使用库的一个主要原因是重复使用现有代码而无需一次又一次地实施它。
在 .NET 出现之前,用 Visual Basic、Delphi、VC++ 编译的所有 DLL,无论是 COM、Win32 还是普通 C++,都可直接由操作系统执行。我们将此代码称为非托管代码或本机代码。之后,.NET 出现,并且带来迥然不同的环境。
代码受 .NET 公共语言运行时 - CLR (Common Language Runtime)的控制(或管理)。需要 CLR 编译器才能从用几种不同的语言编写的源代码生成元数据和公共中间语言 - CIL (Common Intermediate Language)。
CIL 是不依赖于机器的高级语言,元数据依照“公共类型规范” - CTS (Common Type Specification) 充分描述 CIL 描述的对象的类型。因为 CLR 知道类型的一切,它可以向我们提供托管执行环境。可把管理当做是垃圾收集 - 自动内存管理、对象删除以及提供安全性 - 防止本机语言中会导致与管理员权限相冲突的代码执行或内存覆盖等常见错误。
必须指出,CIL 代码绝对不会被直接执行 - 它始终通过 JIT(Just-In-Time,准时)编译转换为本机代码,或通过预编译 CIL 转换为程序集。对于第一次阅读本文的人而言,托管模式代码的概念可能会让人产生混淆,因此我将在下面介绍 CLR 内的一般流程:
图 1. 公共语言运行时
2. 从 MQL5 访问托管代码的可能实施
在下段中,我将介绍能够从非托管代码访问托管代码的方法。
我认为,全部指出这些方法是值得的,因为可能有某人宁愿使用其他方法来代替我正在使用的方法。使用的方法包括 COM Interop、Reverse P/Invoke、C++ IJW、C++/Cli 包装程序类和非托管导出。
2.1. COM Interop
Component Object Model(COM,组件对象模型)是微软在二十世纪九十年代初期提出的二进制接口标准。此技术的核心理念是以不同编程语言创建的对象能够被任何其他 COM 对象使用而无需了解其内部实施。此要求强制实施严格定义的、与实施完全分开的 COM 接口。
事实上,COM 被 .NET 技术取代,并且微软推动用 .NET 代替 COM。为了提供与旧代码的后向兼容性,.NET 可以在两个方向上与 COM 合作,即 .NET 能够调用 COM 方法,而 COM 对象也能使用 .NET 托管代码。
此功能被称为 COM 互操作(COM Interop)。COM Interop API 位于托管 System.Runtime.InteropServices 命名空间中。
图 2. COM 互操作模型
以下 COM interop 代码调用一个函数 raw_factorial。
请注意 CoInitialize()、CoCreateInstance() 和 CoUninitialize() 函数以及调用函数的接口:
#include "windows.h" #include <stdio.h> #import "CSDll.tlb" named_guids int main(int argc, char* argv[]) { HRESULT hRes = S_OK; CoInitialize(NULL); CSDll::IMyManagedInterface *pManagedInterface = NULL; hRes = CoCreateInstance(CSDll::CLSID_Class1, NULL, CLSCTX_INPROC_SERVER, CSDll::IID_IMyManagedInterface, reinterpret_cast<void**> (&pManagedInterface)); if (S_OK == hRes) { long retVal =0; hRes = pManagedInterface->raw_factorial(4, &retVal); printf("The value returned by the dll is %ld\n",retVal); pManagedInterface->Release(); } CoUninitialize(); return 0; }
为了进一步理解 COM Interop,请阅读《Introduction to COM Interop》(COM Interop 介绍)中的详细说明文档以及我在 msdn 博客找到的应用实例: 《How to call C++ code from Managed, and vice versa (Interop)》(如何从托管代码调用 C++ 代码或从 C++ 调用托管代码)。
2.2. Reverse P/Invoke
平台调用,英文简写为 P/Invoke,使 .NET 能够调用以任何非托管语言编写的任何函数——只要重新声明了这些函数的签名。这通过从 .NET 执行本机函数指针来实现。在《平台调用教程》中详细介绍了这一应用。
基本运用是使用 DllImport 属性来标记导入的函数:
// PInvokeTest.cs using System; using System.Runtime.InteropServices; class PlatformInvokeTest { [DllImport("msvcrt.dll")] public static extern int puts(string c); [DllImport("msvcrt.dll")] internal static extern int _flushall(); public static void Main() { puts("Test"); _flushall(); } }
反转操作可被描述为提供一个对非托管代码的托管分配回调。
这被称为反转平台调用(Reverse P/Invoke),通过在托管环境中实施一个公共委托函数并导入在本机 DLL 中实施的调用函数来实现:
#include <stdio.h> #include <string.h> typedef void (__stdcall *callback)(wchar_t * str); extern "C" __declspec(dllexport) void __stdcall caller(wchar_t * input, int count, callback call) { for(int i = 0; i < count; i++) { call(input); } }
以下是托管代码示例:
using System.Runtime.InteropServices; public class foo { public delegate void callback(string str); public static void callee(string str) { System.Console.WriteLine("Managed: " +str); } public static int Main() { caller("Hello World!", 10, new callback(foo.callee)); return 0; } [DllImport("nat.dll",CallingConvention=CallingConvention.StdCall)] public static extern void caller(string str, int count, callback call); }
此解决方案的要点是要求托管方开始互动。
欲知更多参考,请阅读《Gotchas with Reverse Pinvoke (unmanaged to managed code callbacks)》[反转平台调用的陷阱(无托管到托管代码回调)] 和《PInvoke-Reverse PInvoke and stdcall - cdecl》(平台调用-反转平台调用与 stdcall - cdecl)。
2.3. C++ IJW
C++ interop,称为 It Just Works (IJW)),是 C++ 的具体特性,由C++ 托管扩展提供:
#using <mscorlib.dll> using namespace System; using namespace System::Runtime::InteropServices; #include <stdio.h> int main() { String * pStr = S"Hello World!"; char* pChars = (char*)Marshal::StringToHGlobalAnsi(pStr).ToPointer(); puts(pChars); Marshal::FreeHGlobal(pChars); }
此解决方案对希望在非托管应用程序中使用他们的托管 C++ 的人可能会有用处。要获得完整的参考,请阅读 《Interoperability in Managed Extensions for C++》(C++ 的托管扩展中的互操作) 和 《Using IJW in Managed C++》(在托管 C++ 中使用 IJW)。
2.4. C++/Cli 包装程序类
C++/Cli 包装程序类实施通过以 C++/Cli 模式编写的另一个类从嵌入或包装托管类获得其名称。编写包装程序 DLL 的第一步是编写对原来的托管类的方法进行包装的 C++ 类。
包装程序类必须包含使用 gcroot<> 模板的 .NET 对象的句柄,并且必须从原来的类分配所有调用。包装程序类被编译到 IL(中间语言)格式,因此是一个托管类。
下一步是编写含有 #pragma 非托管指令的本机 C++ 类,对 IL 类进行包装,并用 __declspec(dllexport) 指令分配所有调用。这些步骤使本机 C++ DLL 能被任何非托管应用程序使用。
请看实施示例。第一步是实施 C# 代码。
示例 calculator 类包含两个公共方法:
public class Calculator { public int Add(int first, int second) { return first + second; } public string FormatAsString(float i) { return i.ToString(); } }
下一步是编写从 calculator 类分配所有方法的托管包装程序:
#pragma once #pragma managed #include <vcclr.h> class ILBridge_CppCliWrapper_Calculator { private: //Aggregating the managed class gcroot<CppCliWrapper::Calculator^> __Impl; public: ILBridge_CppCliWrapper_Calculator() { __Impl = gcnew CppCliWrapper::Calculator; } int Add(int first, int second) { System::Int32 __Param_first = first; System::Int32 __Param_second = second; System::Int32 __ReturnVal = __Impl->Add(__Param_first, __Param_second); return __ReturnVal; } wchar_t* FormatAsString(float i) { System::Single __Param_i = i; System::String __ReturnVal = __Impl->FormatAsString(__Param_i); wchar_t* __MarshaledReturnVal = marshal_to<wchar_t*>(__ReturnVal); return __MarshaledReturnVal; } };
请注意,使用 gcnew 指令来存储到原来的 Calculator 类的引用,并且存储为 gcroot<> 模板。包装后的所有方法可以具有与原方法相同的名称,并且参数和返回值的名称前面分别加有 __Param 和 __ReturnVal 前缀。
现在,必须实施对 C++/Cli 进行包装并导出本机 C++ DLL 方法的非托管 C++。
头文件应包含具有 __declspec(dllexport) 指令的类定义并将指针存储到包装程序类。
#pragma once #pragma unmanaged #ifdef THISDLL_EXPORTS #define THISDLL_API __declspec(dllexport) #else #define THISDLL_API __declspec(dllimport) #endif //Forward declaration for the bridge class ILBridge_CppCliWrapper_Calculator; class THISDLL_API NativeExport_CppCliWrapper_Calculator { private: //Aggregating the bridge ILBridge_CppCliWrapper_Calculator* __Impl; public: NativeExport_CppCliWrapper_Calculator(); ~NativeExport_CppCliWrapper_Calculator(); int Add(int first, int second); wchar_t* FormatAsString(float i); };
其实施如下:
#pragma managed #include "ILBridge_CppCliWrapper_Calculator.h" #pragma unmanaged #include "NativeExport_CppCliWrapper_Calculator.h" NativeExport_CppCliWrapper_Calculator::NativeExport_CppCliWrapper_Calculator() { __Impl = new ILBridge_CppCliWrapper_Calculator; } NativeExport_CppCliWrapper_Calculator::~NativeExport_CppCliWrapper_Calculator() { delete __Impl; } int NativeExport_CppCliWrapper_Calculator::Add(int first, int second) { int __ReturnVal = __Impl->Add(first, second); return __ReturnVal; } wchar_t* NativeExport_CppCliWrapper_Calculator::FormatAsString(float i) { wchar_t* __ReturnVal = __Impl->FormatAsString(i); return __ReturnVal; }
在 《.NET to C++ Bridge》(从 .NET 到 C++ 的桥梁)一文中说明了创建此包装程序类的分步指南。
在 《Mixing .NET and native code》(.NET 和本机代码的融合)一文中提供了有关创建包装程序的完整参考;要获得以本机类型声明句柄的一般信息,请阅读 《How to:Declare Handles in Native Types》(如何以本机类型声明句柄)一文。
2.5. 非托管导出
《Expert .NET 2.0 IL Assembler》》一书详细介绍了此技术,我向愿意了解 .NET 编译器详情的每一个人都推荐该书。主要理念是通过使用 ILDasm 将已经编译的模块反编译到 IL 代码,更改模块的 VTable 和 VTableFixup 表,然后使用 ILAsm 重新编译 DLL,从而使托管方法成为托管 DLL 的非托管导出。
此任务看起来有点令人生畏,但是此操作将创建一个能够从任何非托管应用程序内使用的 DLL。必须记住,它仍然是托管组件,因此必须安装 .NET 框架。在《Export Managed Code as Unmanaged》(将托管代码导出为非托管代码)一文中提供了分步教程。
在使用 ILDasm 反编译 DLL 之后,我们获得以 IL 语言表示的源代码。请观察粘贴在下面的含有非托管导出的 IL 代码的一个简单例子:
assembly extern mscorlib {} ..assembly UnmExports {} ..module UnmExports.dll ..corflags 0x00000002 ..vtfixup [1] int32 fromunmanaged at VT_01 ..data VT_01 = int32(0) ..method public static void foo() { ..vtentry 1:1 ..export [1] as foo ldstr "Hello from managed world" call void [mscorlib]System.Console::WriteLine(string) ret }
负责实施非托管导出的 IL 源代码行为:
..vtfixup [1] int32 fromunmanaged at VT_01 ..data VT_01 = int32(0)
以及
..vtentry 1:1 ..export [1] as foo
第一部分负责在 VTableFixup 表中添加函数条目,并将 VT_01 虚拟地址设置到函数。第二部分指定哪一 VTEntry 将用于此函数,并为要导出的函数指定导出别名。
此解决方案的优点是,在 DLL 实施期间,我们不需要实施普通托管 C# DLL 以外的任何其他代码,并且如该书所述,此方法向非托管客户端完全打开了拥有全部安全性和类库的托管世界。
缺点是并非所有人都适合了解 .NET 汇编语言。我相信我会代之以编写 C++ 包装程序,直到我找到 Robert Giesecke 编写的非托管导出模板:http://sites.google.com/site/robertgiesecke/,该模板能够使用非托管导出而无需了解 IL 代码。
3. 非托管导出 C# 模板
R.Giesecke 编写的非托管导出 C# 项目的模板使用 MSBuild 任务,在创建之后自动添加相应的 VT-fixup,因此根本不需要更改 IL 代码。只需要作为一个 Zip 文件下载模板包,并将其复制到 Visual Studio 的 ProjectTemplates 文件夹。
在编译项目之后,生成的 DLL 文件能够毫无缺陷的导入到 MetaTrader;我将在后续部分提供示例。
4. 示例
指出在 MetaTrader 和 C# 之间如何使用正确的封送方法传递变量、数组和结构是一项颇具挑战性的任务,并且我认为在这里提供的信息会节省您很多时间。所有示例都是在安装有 .NET 4.0 和 Visual C# Express 2010 的 Windows Vista 操作系统中进行编译的。我还将一个示例 DLL 附加到文本,该示例含有调用来自 C# DLL 的函数的 MQL 5 代码。
4.1. 示例 1. 在 DLL 函数中添加两个整数、一个双精度变量和一个浮点变量并将结果返回到 MetaTrader。
using System; using System.Text; using RGiesecke.DllExport; using System.Runtime.InteropServices; namespace Testme { class Test { [DllExport("Add", CallingConvention = CallingConvention.StdCall)] public static int Add(int left, int right) { return left + right; } [DllExport("Sub", CallingConvention = CallingConvention.StdCall)] public static int Sub(int left, int right) { return left - right; } [DllExport("AddDouble", CallingConvention = CallingConvention.StdCall)] public static double AddDouble(double left, double right) { return left + right; } [DllExport("AddFloat", CallingConvention = CallingConvention.StdCall)] public static float AddFloat(float left, float right) { return left + right; } } }
您可能注意到,每一个导出的函数在前面都有 DllExport 指令。第一个参数描述导出函数的别名,第二个参数描述调用约定,对于 MetaTrader,我们必须使用 CallingConvention.StdCall。
导入并使用从 DLL 导出的函数的 MQL5 代码很简单,与用本机 C++ 编写的任何其他 DLL 没有区别。首先,必须在 #import 块内声明导入的函数,并且指出以后可以从 MQL5 代码使用来自 DLL 的哪些函数:
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample1.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int Add(int left,int right); int Sub(int left,int right); float AddFloat(float left,float right); double AddDouble(double left,double right); #import //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ 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)); } } //+------------------------------------------------------------------+
结果
2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 664.50000 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 668.5 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 664 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 668 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 665.50000 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 667.5 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 665 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 667 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 666.50000 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 666.5 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 666 2011.01.30 21:28:18 UnmanagedExportsDLLExample1 (EURUSD,M1) 666
4.2. 示例 2. 一维数组访问
[DllExport("Get1DInt", CallingConvention = CallingConvention.StdCall)] public static int Get1DInt([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] tab, int i, int idx) { return tab[idx]; } [DllExport("Get1DFloat", CallingConvention = CallingConvention.StdCall)] public static float Get1DFloat([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] float[] tab, int i, int idx) { return tab[idx]; } [DllExport("Get1DDouble", CallingConvention = CallingConvention.StdCall)] public static double Get1DDouble([MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] double[] tab, int i, int idx) { return tab[idx]; }
为了封送一维数组,MarshalAs 指令必须将 UnmanagedType.LPArray 作为第一个参数,将 SizeParamIndex 作为第二个参数传递。SizeParamIndex 指出哪个参数(从 0 开始计数)是包含数组大小的参数。
在以上例子中,i 是数组大小,idx 是要返回的元素的索引。
以下是使用数组访问的 MQL5 示例代码:
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample2.mq5 | //| Copyright 2010, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2010, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int Get1DInt(int &t[],int i,int idx); float Get1DFloat(float &t[],int i,int idx); double Get1DDouble(double &t[],int i,int idx); #import //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- int tab[3]; tab[0] = 11; tab[1] = 22; tab[2] = 33; float tfloat[3]={0.5,1.0,1.5}; double tdouble[3]={0.5,1.0,1.5}; for(int i=0; i<3; i++) { Print(tab[i]); Print(Get1DInt(tab,3,i)); Print(Get1DFloat(tfloat,3,i)); Print(Get1DDouble(tdouble,3,i)); } } //+------------------------------------------------------------------+
结果
2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 1.5 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 1.50000 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 33 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 33 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 1 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 1.00000 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 22 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 22 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 0.5 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 0.50000 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 11 2011.01.30 21:46:25 UnmanagedExportsDLLExample2 (EURUSD,M1) 11
4.3. 示例 3. 填入一维数组并将其返回到 MetaTrader
[DllExport("SetFiboArray", CallingConvention = CallingConvention.StdCall)] 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; res[1] = 1; if (len < 3) return -1; for (int i=2; i<len; i++) res[i] = res[i-1] + res[i-2]; return 0; }
此示例使用两个输入数组,以比较输入参数协定。如果更改后的元素要返回到 Metatrader(通过引用传递),则推送 MarshalAs 属性之前的 [In, Out,] 属性就足够了。
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample3.mq5 | //| Copyright 2011, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int SetFiboArray(int& t[], int i, int& o[]); #import //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- int fibo[10]; static int o[10]; for (int i=0; i<4; i++) { fibo[i]=i; o[i] = i; } SetFiboArray(fibo, 6, o); for (int i=0; i<6; i++) Print(IntegerToString(fibo[i])+":"+IntegerToString(o[i])); } //+------------------------------------------------------------------+
结果
2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 0:5 2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 0:3 2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 3:2 2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 2:1 2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 1:1 2011.01.30 22:01:39 UnmanagedExportsDLLExample3 (EURUSD,M1) 0:0
4.4. 示例 4. 二维数组访问
public static int idx(int a, int b) {int cols = 2; return a * cols + b; } [DllExport("Set2DArray", CallingConvention = CallingConvention.StdCall)] public static int Set2DArray([In, Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] int[] tab, int len) { tab[idx(0, 0)] = 0; tab[idx(0, 1)] = 1; tab[idx(1, 0)] = 2; tab[idx(1, 1)] = 3; tab[idx(2, 0)] = 4; tab[idx(2, 1)] = 5; return 0; }
对封送而言,二维数组并不是如此简单,但是我使用了一个技巧 - 即将二维数组作为一维数组传递并通过辅助 idx 函数访问数组元素。
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample4.mq5 | //| Copyright 2011, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int Set2DArray(int &t[][2],int i); #import //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- int t2[3][2]; Set2DArray(t2,6); for(int row=0; row<3; row++) for(int col=0; col<2; col++) Print("t2["+IntegerToString(row)+"]["+IntegerToString(col)+"]="+IntegerToString(t2[row][col])); } //+------------------------------------------------------------------+
结果
2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[2][1]=5 2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[2][0]=4 2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[1][1]=3 2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[1][0]=2 2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[0][1]=1 2011.01.30 22:13:01 UnmanagedExportsDLLExample4 (EURUSD,M1) t2[0][0]=0
4.5. 示例 5. 替换字符串内容
[DllExport("ReplaceString", CallingConvention = CallingConvention.StdCall)] public static int ReplaceString([In, Out, MarshalAs(UnmanagedType.LPWStr)] StringBuilder str, [MarshalAs(UnmanagedType.LPWStr)]string a, [MarshalAs(UnmanagedType.LPWStr)]string b) { str.Replace(a, b); if (str.ToString().Contains(a)) return 1; else return 0; }
此示例很短,但是让我花了相当长的时间来实施,因为我尝试运用使用 [In,Out] 属性、或具有 ref 或 out 关键字的字符串参数,但是没有成功。
解决方法是使用 StringBuilder 而不是字符串变量。
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample5.mq5 | //| Copyright 2011, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int ReplaceString(string &str,string a,string b); #import //+------------------------------------------------------------------+ //| Script program start function | //+------------------------------------------------------------------+ void OnStart() { //--- string str="A quick brown fox jumps over the lazy dog"; string stra = "fox"; string strb = "cat"; Print(str); Print(ReplaceString(str,stra,strb)); Print(str); } //+------------------------------------------------------------------+
结果
2011.01.30 22:18:36 UnmanagedExportsDLLExample5 (EURUSD,M1) A quick brown cat jumps over the lazy dog 2011.01.30 22:18:36 UnmanagedExportsDLLExample5 (EURUSD,M1) 0 2011.01.30 22:18:36 UnmanagedExportsDLLExample5 (EURUSD,M1) A quick brown fox jumps over the lazy dog
4.6. 示例 6. 发送并更改 MqlTick 结构
private static List<MqlTick> list; [StructLayout(LayoutKind.Sequential, Pack = 1)] public struct MqlTick { public Int64 Time; public Double Bid; public Double Ask; public Double Last; public UInt64 Volume; } [DllExport("AddTick", CallingConvention = CallingConvention.StdCall)] public static int AddTick(ref MqlTick tick, ref double bidsum) { bidsum = 0.0; if (list == null) list = new List<MqlTick>(); tick.Volume = 666; list.Add(tick); foreach (MqlTick t in list) bidsum += t.Ask; return list.Count; }
MqlTick 结构作为引用传递,通过 ref 关键字标记。必须在 MqlTick 结构本身的前面添加 [StructLayout (LayoutKind.Sequential, Pack =1)] 属性。
Pack 参数说明结构中的数据对齐,有关详情请参阅 《StructLayoutAttribute.Pack Field》(StructLayoutAttribute.Pack 字段)。
//+------------------------------------------------------------------+ //| UnmanagedExportsDLLExample6.mq5 | //| Copyright 2011, Investeo.pl | //| http:/Investeo.pl | //+------------------------------------------------------------------+ #property copyright "Copyright 2011, Investeo.pl" #property link "http:/Investeo.pl" #property version "1.00" #import "Testme.dll" int AddTick(MqlTick &tick, double& bidsum); #import //+------------------------------------------------------------------+ //| Custom indicator initialization function | //+------------------------------------------------------------------+ int OnInit() { //--- indicator buffers mapping //--- return(0); } //+------------------------------------------------------------------+ //| Custom indicator iteration function | //+------------------------------------------------------------------+ int OnCalculate(const int rates_total, const int prev_calculated, const datetime& time[], const double& open[], const double& high[], const double& low[], const double& close[], const long& tick_volume[], const long& volume[], const int& spread[]) { //--- MqlTick newTick; double bidsum; SymbolInfoTick(Symbol(), newTick); Print("before = " + IntegerToString(newTick.volume)); Print(AddTick(newTick, bidsum)); Print("after = " + IntegerToString(newTick.volume) + " : " + DoubleToString(bidsum)); //--- return value of prev_calculated for next call return(rates_total); } //+------------------------------------------------------------------+
结果
2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 8.167199999999999 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 6 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) before = 0 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 6.806 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 5 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) before = 0 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 5.4448 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 4 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) before = 0 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 4.0836 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 3 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) before = 0 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 2.7224 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 2 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) before = 0 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) after = 666 : 1.3612 2011.01.30 23:59:05 TickDLLSend (EURUSD,M1) 1 2011.01.30 23:59:04 TickDLLSend (EURUSD,M1) before = 0
总结
在本文中,我介绍了在 MQL5 代码和托管 C# 代码之间进行互动的几种方法。
我还提供了如何针对 C# 封送 MQL5 结构以及如何在 MQL5 脚本中调用导出的 DLL 函数的几个示例。我相信提供的例子能用作以后研究用托管代码编写 DLL 的基础。
本文也为 MetaTrader 使用已经在 C# 中实施了的多个库打开了大门。欲了解更多参考,请阅读在“参考”部分中链接的文章。
要对其进行测试,请将文件放在以下文件夹中:
MQL5\Libraries\testme.dllMQL5\Scripts\unmanagedexportsdllexample1.mq5
MQL5\Scripts\unmanagedexportsdllexample2.mq5
MQL5\Scripts\unmanagedexportsdllexample3.mq5
MQL5\Scripts\unmanagedexportsdllexample4.mq5
MQL5\Scripts\unmanagedexportsdllexample5.mq5
MQL5\Experts\unmanagedexportsdllexample6.mq5
参考文献
- Exporting .NET DLLs with Visual Studio 2005 to be Consumed by Native Applications(使用 Visual Studio 2005 导出 .NET DLL 以供本机应用程序使用)
- Interoperating with Unmadged Coded(与非托管代码的互操作)
- Introduction to COM Interop(COM Interop 介绍)
- Component Object Model (COM)(组件对象模型 (COM))
- Exporting from a DLL Using __declspec(dllexport)(使用 __declspec(dllexport) 从 DLL 导出)
- How to:Declare Handles in Native Types(如何以本机类型声明句柄)
- How to call C++ code from Managed, and vice versa (Interop)((如何从托管代码调用 C++ 代码或从 C++ 调用托管代码))
- Reverse P/Invoke and exception(Reverse P/Invoke 及例外)
- How to call a managed DLL from native Visual C++ code in Visual Studio.NET or in Visual Studio 2005(如何从用 Visual Studio.NET 或 Visual Studio 2005 编写的本机 Visual C++ 代码调用托管 DLL)
- Platform Invoke Tutorial(平台调用教程)
- PInvoke-Reverse PInvoke and __stdcall - __cdecl
- Gotchas with Reverse Pinvoke (unmanaged to managed code callbacks)[反转平台调用的陷阱(无托管到托管代码回调)]
- Mixing .NET and native code( .NET 和本机代码的融合)
- Export Managed Code as Unmanaged(将托管代码导出为非托管代码)
- Understanding Classic COM Interoperability With .NET Applications(理解经典 COM 与 .NET 应用程序的互操作)
- Managed Extensions for C++ Programming(C++ 编程的托管扩展)
- Robert Giesecke 的网站
- MSBuild Tasks(MSBuild 任务)
- Common Language Runtime(公共语言运行时)