事实上,很少有开发人员确切知道如何编写简单的 DLL 库,他们也不清楚绑定不同系统的特性。
通过多个示例,我将展示在 10 分钟内创建简单 DLL 的整个过程,并讨论我们绑定实施的一些技术细节。我们将使用 Visual Studio 2005/2008;其 Express 版本可免费从
Microsoft 的网站
上下载。
1. 使用 C++ 在 Visual Studio 2005/2008 中创建 DLL 项目
使用 "
File -> New
"(文件 -> 新建)菜单运行 Win32 应用程序向导,选择 "
Visual C++
" 作为项目类型,然后选择 "
Win32 Console Application
"(Win32 控制台应用程序)模板并定义项目名称(如 "
MQL5DLLSamples
")。在 "
Location
"(位置)中选择存储项目的根目录,而不是使用提供的默认选项,取消选择 "
Create directory for solution
"(创建解决方案的目录)复选框并单击 "
OK
"(确定):
图 1. Win32 应用程序向导,DLL 项目创建
在下一步骤中按 "
Next
"(下一步)转到设置页面:
图 2. Win32 应用程序向导,项目设置
在最后一页,选择 "
DLL
" 应用程序类型,保持其他字段为空,然后单击 "
Finish
"。如果您不希望删除自动添加的演示代码,不要勾选 "
Export symbols
"(导出交易品种)选项:
图 3.
Win32 应用程序向导,应用程序设置
最后您将得到一个空项目:
图 4. 向导生成的空 DLL 项目
要简化测试,最好在 "
Output Directory
"(输出目录)选项中将 DLL 文件的输出直接指定至客户端的 "
...\MQL5\Libraries
" - 接下来,这会节省您大量的时间:
图 5. DLL 输出目录
2. 准备添加函数
在
stdafx.h
文件的末尾添加 "
_DLLAPI
" 宏,这样您便可以轻松方便地说明导出的函数:
#pragma once
#include "targetver.h"
#define WIN32_LEAN_AND_MEAN // 从Windows头中排除极少使用的
#include <windows.h>
#define _DLLAPI extern "C" __declspec(dllexport)
在 MQL5 中调用的 DLL 导入函数应具有 stdcall 和 cdecl 调用约定。即使 stdcall 和 cdecl 从堆栈提取参数的方式不同,由于 DLL 调用的特殊包装程序,MQL5 运行时环境可安全地使用两种版本。
C++ 编译器默认使用 __cdecl 调用,但我建议对于导出函数明确指定 __stdcall 模式。
正确编写的导出函数必须具有以下形式:
_DLLAPI int __stdcall fnCalculateSpeed(int &res1,double &res2)
return(0);
在 MQL5 程序中,函数必须如下定义和调用:
#import "MQL5DLLSamples.dll"
int fnCalculateSpeed(int &res1,double &res2);
#import
speed=fnCalculateSpeed(res_int,res_double);
项目编译后,该 stdcall 将在导出表中显示为 _fnCalculateSpeed@8,在此编译器添加下划线以及通过堆栈传递的字节数。这一修饰使我们可以更好地控制 DLL 函数调用的安全性,因为调用方确切知道应在堆栈中放置的数据数量(但不是类型)。
如果参数块的最终大小在 DLL 函数导入说明中存在错误,函数不会被调用,且日志中将出现新消息:" Cannot find 'fnCrashTestParametersStdCall' in 'MQL5DLLSamples.dll"(在 MQL5DLLSamples.dll 中找不到 "fnCrashTestParametersStdCall")。在这种情况下,需要仔细检查函数原型和 DLL 源中的所有参数。
无修饰的简化说明的搜索用于导出表未包含完整函数名情形下的兼容性。如果函数以 __cdecl 格式定义,则类似 fnCalculateSpeed 的名称将被创建。_DLLAPI int fnCalculateSpeed(int &res1,double &res2)
return(0);
3. 传递参数和交换数据的方法
接下来我们讨论传递参数的几个变体:
- 接收和传递简单变量
简单变量的情形十分简单 - 它们可以通过值或使用 & 通过引用传递。
- 使用元素填充接收和传递数组
与其他 MQL5 程序不同,数组传递通过直接引用数据缓冲区执行,无需访问有关维度和大小的专有信息。这正是数组维度和大小应分别传递的原因。
_DLLAPI void __stdcall fnFillArray(int *arr,const int arr_size)
if(arr==NULL || arr_size<1) return;
for(int i=0;i<arr_size;i++) arr[i]=i;
从 MQL5 调用:
#import "MQL5DLLSamples.dll"
void fnFillArray(int &arr[],int arr_size);
#import
int arr[];
string result="Array: ";
ArrayResize(arr,10);
fnFillArray(arr,ArraySize(arr));
for(int i=0;i<ArraySize(arr);i++) result=result+IntegerToString(arr[i])+" ";
Print(result);
输出如下所示:
MQL5DLL Test (GBPUSD,M1) 20:31:12 Array: 0 1 2 3 4 5 6 7 8 9
- 传递和修改字符串
Unicode 字符串使用其缓冲区地址的直接引用传递,无需传递任何其他信息。
_DLLAPI void fnReplaceString(wchar_t *text,wchar_t *from,wchar_t *to)
wchar_t *cp;
if(text==NULL || from==NULL || to==NULL) return;
if(wcslen(from)!=wcslen(to)) return;
搜索字串
if((cp=wcsstr(text,from))==NULL) return;
memcpy(cp,to,wcslen(to)*sizeof(wchar_t));
从 MQL5 调用:
#import "MQL5DLLSamples.dll"
void fnReplaceString(string text,string from,string to);
#import
string text="A quick brown fox jumps over the lazy dog";
fnReplaceString(text,"fox","cat");
Print("Replace: ",text);
结果如下所示:
MQL5DLL Test (GBPUSD,M1) 19:56:42 Replace: A quick brown fox jumps over the lazy dog
结果表明线并未改变!这是初学者传送而不是引用对象(字符串是对象)的副本时常见的错误。系统自动创建字符串 "text" 的副本并在 DLL 中对其进行修改,然后自动删除副本而不会影响原字符串。
为了改变这种状况,需要通过引用传递一个字符串。为此,我们可通过将 & 添加至 "text" 参数来简单地修改导入代码块:
#import "MQL5DLLSamples.dll"
void fnReplaceString(string &text,string from,string to);
#import
编译并运行后,我们将得到正确的结果:
MQL5DLL Test (GBPUSD,M1) 19:58:31 Replace: A quick brown cat jumps over the lazy dog
4. 捕获 DLL 函数中的异常
为防止终端崩溃,每个 DLL 调用自动受到“未处理异常包装”的保护。这一机制可保护调用免受大多数标准错误的影响(如内存访问错误、除数为零等)。
要查看该机制如何工作,让我们创建以下代码:
_DLLAPI void __stdcall fnCrashTest(int *arr)