文章 "用 Delphi 为 MQL5 编写 DLL 指南"

 

新文章 用 Delphi 为 MQL5 编写 DLL 指南已发布:

本文说明在 Delphi 编程环境中使用流行编程语言 ObjectPascal 创建 DLL 模块的机制。本文提供的材料主要针对初学者而设计,这些初学者面临因为连接外部 DLL 模块而突破了嵌入式编程语言 MQL5 边界的问题。

作者:Andrey Voytenko

 
期待这样一篇文章已经很久了。感谢作者。
 
DC2008:
期待这样一篇文章已经很久了。感谢作者。

谢谢。你不是第一个感谢我的人。我很乐意倾听大家的愿望和对文章材料的批评意见。

今后,我将在Delphi 中开发MT5 编程主题,为网站添加新信息。

 
你至少应该增加一段关于调试的内容。文章提到了 AV 可能发生的一种情况,但即使撇开大量其他潜在的错误源不谈,试图手动(通过眼睛或头脑)搜索错误位置也可能需要很长时间,而且不会成功。
 

我认为这篇文章对很多人都很有用。有几点意见

1. SysUtils 和 Classes 单元本应留在项目中。尽管它们的存在在某种程度上 "臃肿 "了项目,但它们有许多小而重要的功能。例如,SysUtils 的存在自动为项目增加了例外情况处理功能。大家都知道,如果一个 excepcion 没有在 dll 中处理,它就会被传递到 mt5,导致 mql5 程序停止执行。

2.您不应该在 DllEntryPoint(又称 DllMain)中使用各种程序。正如微软在其文档中指出的那样,这会带来各种令人不快的后果。以下是有关此主题的一小部分文章:

微软创建 DLL 的最佳实践 - http://www.microsoft.com/whdc/driver/kernel/DLL_bestprac.mspx。

DllMain 和分娩前的生活 -http://transl-gunsmoker.blogspot.com/2009/01/dllmain.html

DllMain - 睡前故事 -http://transl-gunsmoker.blogspot.com/2009/01/dllmain_04.html

不要在您的 DllMain 中做任何可怕事情的几个理由 -http://transl-gunsmoker.blogspot.com/2009/01/dllmain_05.html

不要在 DllMain 中做任何可怕事情的更多原因:意外锁定 -

http://transl-gunsmoker.blogspot.com/2009/01/dllmain_7983.html

 

我已经在某个地方摘录了一篇未完成的文章,我想是在 quad 论坛上。我在此重复一遍。

开始......结束

在创建用于DLL 编译的 Delphi 项目时,begin...end 部分会出现在 .DPR 项目文件中。当 DLL 首次投射到进程地址空间时,该部分总是被执行。换句话说,它可以被视为所有单元都有的初始化部分。在这里,你可以执行一些需要在一开始就执行的操作,而且对当前进程只执行一次。当把 DLL 载入另一个进程的地址空间时,该部分将再次被执行。但由于进程的地址空间是相互分离的,因此一个进程中的初始化操作不会对另一个进程产生任何影响。

本节有一些限制,你应该注意并加以考虑。这些限制与 Windows DLL 加载机制的微妙之处有关。稍后我们将详细讨论。

 

初始化/最终确定

Delphi的每个 单元 有特殊的部分,即所谓的初始化和最终化部分。一旦任何单元连接到项目,这些部分就会连接到主模块的特殊加载和卸载机制。这些部分在主 模块的 begin...end 部分 开始工作 之前 结束 工作之后 执行。 这样做非常方便,因为它消除了在程序中编写初始化和终结的需要。同时,连接和断开都是自动进行的,只需将单元连接或断开到项目中即可。这不仅发生在传统的 EXE 文件中,也发生在 DLL 文件中。 DLL"加载 "到内存中时,其 初始化顺序 如下:首先执行单元的所有初始化部分,顺序与项目用途中标记的顺序一致,然后执行 begin...end 部分。终结的顺序正好相反,只是在 DLL 项目文件中没有专门设计的终结函数。一般来说,这也是建议将 DLL 项目分为项目文件和用途单元的另一个原因。

 

DllMain

这就是所谓的 DLL 入口点。这是因为 Windows 偶尔需要向 DLL 本身报告进程中发生的任何事件。要做到这一点,就需要一个入口点。也就是说,每个 DLL 都有一个专门的预定义函数,可以处理消息。虽然我们还没有在 Delphi 编写的 DLL 中看到过这种函数,但它确实有这样一个入口点。只是它的运行机制被掩盖了,但你总能找到它。问题的答案--它到底有没有必要?- 这个问题的答案并不像看上去那么明显。

首先,让我们来了解一下 Windows 试图告诉 DLL 什么。操作系统总共会向 DLL 发送 4 条信息。第一条是 DLL_PROCESS_ATTACH 通知,每当系统将 DLL 附加到调用进程的地址空间时就会发送。在 MQL4,这是一种隐式加载。即使该 DLL 已被加载到另一个进程的地址空间中,也不会影响消息的发送。事实上,Windows 只将特定 DLL 加载到内存中一次,但这并不重要,所有希望将此 DLL 加载到其地址空间的进程都只会收到此 DLL 的映像。这是相同的代码,但 DLL 可能拥有的数据对每个进程来说都是独一无二的(尽管可能存在共同的数据)。第二个是 DLL_PROCESS_DETACH 通知,它告诉 DLL 从调用进程的地址空间分离。事实上,在 Windows 开始卸载 DLL 之前,就已经收到了这条信息。事实上,如果 DLL 正被其他进程使用,则不会发生卸载,Windows 会 "忘记 "该 DLL 曾存在于进程的地址空间中。 当加载 DLL 的进程在进程内生成或销毁线程时,会收到 另外两个通知: DLL_THREAD_ATTACH DLL_THREAD_DETACH 。与接收线程通知的顺序有关的一些微妙问题,我们将不予考虑。

现在我们来了解一下用 Delphi 编写的 DLL 是如何排列的,以及通常会被程序员隐藏的内容。在 Windows 将 DLL "投影到调用进程的地址空间",或者简单地说,将 DLL 加载到内存中后,此时需要调用 位于入口点的函数,并将通知 DLL_PROCESS_ATTACH 传递给该函数。在用 Delphi 编写的 DLL 中,这个入口点包含特殊代码,可以完成许多不同的工作,包括启动单元的初始化。它会记住 DLL 的初始化和首次运行已经完成,并执行主项目文件的 begin...end。因此,这段初始加载代码只执行一次,Windows 对入口点的所有其他调用都是向另一个函数发出的,该函数负责处理后续通知--事实上,除了 DLL_PROCESS_DETACH 消息之外,它都会忽略这些通知,而 DLL_PROCESS_DETACH 消息则会最终确定单元。这就是用 Delphi 编写的 DLL 的一般加载机制。在大多数情况下,在 MQL4 中编写和使用 DLL 就足够了。

如果您仍然需要一个与 C 语言完全相同的 DllMain,也不难组织它。具体做法如下。首次加载 DLL 时,除其他事项外,System 模块(它总是存在于程序或 DLL 中)会自动创建一个全局过程变量 DllProc,该变量的初始化值为 nil。这意味着,除了现有的 DllMain 通知外,无需对其进行其他处理。只要将函数的地址分配给该变量,所有来自 Windows 的 DLL 通知都将转到该函数。这也是对入口点的要求。不过,DLL_PROCESS_DETACH 通知仍将和以前一样,由 DLL 终止函数跟踪,以便最终完成。

过程DllEntryPoint(Reason: DWORD);

开始

case Reason of

DLL_PROCESS_ATTACH : ;//'Connection 进程

DLL_THREAD_ATTACH : ;//'连接线 线程

DLL_THREAD_DETACH : ;//'断开线程连接'

DLL_PROCESS_DETACH : ;//'断开 进程 ' 进程

结束

结束

开始

if not Assigned(DllProc) then begin

DllProc :=@DllEntryPoint

DllEntryPoint (DLL_PROCESS_ATTACH);

end

结束

如果我们对线程通知不感兴趣,所有这些都是不必要的。只需在单元中组织初始化/最终化部分,因为进程连接和断开事件将被自动跟踪。

 

DllMain 的背信弃义和背信弃义

现在,也许是时候谈谈编程文献中出人意料地很少涉及的一个问题了。这个问题不仅涉及 Delphi 或 C 语言,还涉及任何能够创建 DLL 的编程语言。这是 Windows DLL 加载器的一个特性。在翻译的有关 Windows 环境下编程的严肃而广泛的文献中,只有一位作者设法提到了这一点,而且是以最模糊的措辞提到的。这本书的作者是 J. Richter,我们可以原谅他,因为他的这本精彩的书是在 2001 年出版的,当时 32 位 Windows 还没有如此普及。

有趣的是,MS 从来没有隐瞒过 DllMain 问题的存在,甚至还专门发布了一份类似于 "使用 DllMain 的最佳方法 "的文档。他在文件中解释了什么可以在 DllMain 中完成,什么不推荐。他还指出,不推荐的操作会导致难以察觉和不一致的错误。希望阅读此文档的读者可点击此处。此处概述了几份危言耸听的报告的几种译文,是一份更受欢迎的摘要。

问题的实质非常简单。问题的关键在于,DllMain,尤其是在加载 DLL 时,是一个特殊的地方。在这里,你不应该做任何复杂和突出的事情。例如,不建议 CreateProcessLoadLibrary 其他 DLL。也不建议 创建线程(CreateThread )或 初始化 COM。等等。

你可以做最简单的事情。否则,一切都无法保证。因此,不要在 DllMain 中放置不必要的东西,否则你会惊讶地发现使用你的 DLL 的应用程序会崩溃。为了安全起见,最好创建特殊的初始化和最终化导出函数,由主应用程序在适当的时候调用。这样至少可以避免 DllMain 出现问题。
 

ExitProc, ExitCode,MainInstance,HInstance....

System 模块在编译时总是与 DLL 挂钩,它有一些有用的全局变量 可供使用。

ExitCode, - 一个变量,在加载时,你可以在其中输入一个 0 以外的数字,这样 DLL 加载就会停止。

ExitProc- 一个程序变量,用于存储退出时将执行的函数的地址。这个变量已经过时了,它在 DLL 中不起作用,而且 Delphi 开发人员不建议在 DLL 中使用它,因为可能会出现问题。

HInstance,--一个变量,在加载后,DLL 本身的描述符被存储在其中。它可能非常有用。

MainInstance(主实例)--将 DLL 载入其地址空间的应用程序的描述符。

IsMultiThread(是否多线程)--如果 DLL 的编译过程中检测到有线程工作,该变量将自动设置为 True。根据该变量的值, DLL内存管理器 将切换到多线程模式。原则上,即使 DLL 中没有明确使用线程,也可以强制内存管理器切换到多线程模式。IsMultiThread:=True; 当然,多线程模式比单线程模式慢,因为线程之间是同步的。

MainThreadID, - 应用程序主线程的描述符。

等等。一般来说,System 模块执行的功能与 C 语言中的 CRT 大致相同。包括内存管理功能。如果在 "项目设置 "中打开 "链接器 "选项 "映射文件--详细",就能获得编译后 DLL 中所有函数和变量的列表,不仅包括导出的函数和变量,还包括所有函数和变量。
 

内存管理

下一个经常造成困难的严重问题是 DLL 中的内存管理。更确切地说,内存管理本身不会造成任何问题,但一旦 DLL 试图主动使用应用程序本身的内存管理器分配的内存时,问题通常就开始了。

问题在于,应用程序在编译时通常内置了内存管理器。编译后的 DLL 也包含自己的内存管理器。这对于在不同编程环境下创建的应用程序和 DLL 尤为如此。在我们的例子中,终端使用的是 MSVC,而 DLL 使用的是 Delphi。从结构上看,它们显然是不同的管理器,但同时它们又是物理上不同的管理器,各自管理着进程公共地址空间内的内存。原则上,它们互不干扰,互不占用对方的内存,彼此并行存在,通常不 "知道 "竞争对手的存在。之所以能做到这一点,是因为这两个内存管理器都从同一个源(Windows 内存管理器)访问内存。

当 DLL 函数和应用程序试图管理由不同内存管理器分配的内存部分时,问题就开始出现了。因此,程序员有一条经验法则,即 "内存不应跨越代码模块的边界"。

这是一条好规则,但并不完全正确。更正确的做法是,在应用程序使用的 DLL 中使用相同的内存管理器。事实上,我比较喜欢将 MT4内存管理器连接 Delphi内存管理器 FastMM 的想法 ,但这根本不是一个非常可行的想法。总之,内存管理应该是一回事。

Delphi ,可以用符合某些要求的任何其他内存管理器替换默认内存管理器。因此,我们可以让 DLL 和应用程序使用一个内存管理器,它就是 MT4 内存管理器。