Обсуждение статьи "Руководство по написанию DLL для MQL5 на Delphi"

 

Опубликована статья Руководство по написанию DLL для MQL5 на Delphi:

Статья рассматривает механизм написания модудя DLL на популярном языке программирования ObjectPascal в среде разработки Delphi. Изложенный в статье материал ориентирован в первую очередь на начинающих программистов, решающих задачи, выходящие за рамки встроенного языка программирования MQL5, путем подключения внешних DLL модулей.

Автор: Андрей

 
Давно ждал подобной статьи. Спасибо автору.

 
DC2008:
Давно ждал подобной статьи. Спасибо автору.

Благодарю Вас. Вы не первый поблагодаривший. Буду рад выслушать все пожелания и критические замечания по материалу статьи.

В будущем хочется развить тему программирования на Delphi под MT5, пополнив сайт новой информацией.


 
По поводу отладки нужно бы добавить хоть параграф. В статье упоминается одна ситуация, когда может происходить AV, но даже оставив в стороне море прочих потенциальных источников эксепшенов, пытаться вручную (глазами или мысленно) искать место ошибки можно очень долго и безуспешно.
 

Думаю, полезная многим статья. Пара замечаний:

1. Юниты SysUtils и Classes  нужно было оставить в проекте. Не смотря на то, что их присутствие несколько "раздувает" проект, они несут много мелких, но важных функций. Например, присутствие SysUtils автоматически добавляет в проект обработку эксепшенов. Как известно, если эксепшен не обработан в dll, то он передаётся в mt5, где приводит к остановке выполнения mql5 программы.

2. Не следует использовать в рамках DllEntryPoint (он же DllMain) всякие процедуры. Как утверждает Майкрософт, в своих документах, это чревато разными неприятными эффектами. Небольшой список статей, по этому поводу:

Best Practices for Creating DLLs by Microsoft - 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

 

Уже где то, кажется на четвёрочном форуме, давал отрывок незаконченной статьи. Повторю здесь.

beginend

 

При создании проекта Delphi, предназначенного для компиляции DLL, в файле проекта .DPR появляется секция begin...end. Эта секция выполняется всегда при первой проекции DLL в адресное пространство процесса. Другими словами, можно считать, что это своеобразная секция initialization, которая есть у всех unit. В этом месте можно провести какие то действия, которые нужно выполнить в самом начале и только один раз, для текущего процесса. При загрузке DLL в адресное пространство другого процесса эта секция будет выполнена там повторно. Но так как адресные пространства процессов разделены между собой, то начальная инициализация в одном процессе ни как не скажется на другой процесс.

 

У этой секции есть некоторые ограничения о которых нужно знать и учитывать. Ограничения эти связаны с тонкостями функционирования механизма загрузки DLL Windows. Более подробно о них поговорим позднее.

 

initialization/finalization

 

У каждого unit Delphi есть особые секции, так называемые секции инициализации и завершения. Как только любой unit подключается к проекту эти секции подключаются к специальному механизму загрузки и выгрузки главного модуля. И эти секции выполняются до того как главная секция beginend начнет свою работу, и после того как работа будет завершена. Это бывает очень удобно, так как избавляет от необходимости прописывать инициализацию и финализацию в самой программе. При этом, подключение и отключение проводится автоматически, стоит только подключить или отключить unit к проекту. И это происхоит не только в традиционных EXE файлах, но и в DLL так же. Очередность инициализации DLL, при «загрузке» её в память следующая, сначала выполняются все секции инициализации unit, в том порядке как они обозначены в uses проекта, затем выполняется секция begin...end. Финализация происходит в обратном порядке, за исключением того, что в файле проекта DLL нет специально предназначенной функции завершения. Это, в общем то, ещё одна причина, почему проекты DLL рекомендуется разделять на файл проекта и используемые unit.

 

DllMain

 

Это так называемая точка входа DLL. Дело в том, что у Windows переодически возникает необходимость сообщить о каком либо событии, происходящем в пределах процесса, самой DLL. Для того что бы это сделать и существует точка входа. То есть специально предопределенная функция, которая есть у каждой DLL и которая может обрабатывать сообщения. И хотя мы до сих пор не видили этой функции в DLL написанной на Delphi, тем не менее такая точка есть и у неё. Просто механизм её функционирования завуалирован, но до него всегда можно добраться. Ответ на вопрос, - а нужно ли это вообще? – не так очевиден как кажется.

 

Сначала попытаемся разобраться, что же такое стремиться Windows сообщать DLL. Всего существует 4 сообщения, с которыми операционная система приходит к DLL. Первое, уведомление DLL_PROCESS_ATTACH – присылается всякий раз, когда система присоединяет DLL к адресному пространству вызывающего процесса. В случае MQL4 это неявная загрузка. При этом, не важно что данная DLL уже была загружена в адресное пространство другого процесса, сообщение всё равно прийдет. И неважно, то что на самом деле фактически Windows грузит конкретную DLL в память всего один лишь раз, все процессы желающие загрузить эту DLL к себе в адресное пространство получают всего лишь отражение этой DLL. Это один и тот же код, но данные которые могут быть у DLL уникальны для каждого процесса (хотя возможно и существование общих данных). Второе, уведомление DLL_PROCESS_DETACH - сообщает DLL, что необходимо произвести отсоединение от адресного пространства вызывающего процесса. Фактически, это сообщение поступает перед тем, как Windows начнет выгружать DLL. На самом деле, если DLL используется другими процессами, то ни какой выгрузки не происходит, просто Windows «забывает» что некая DLL существовала в адресном пространстве процесса. Ещё два уведомления, DLL_THREAD_ATTACH и DLL_THREAD_DETACH поступают в тот момент когда у процесса, который загрузил DLL, возникают или уничтожаются потоки в пределах процесса. Есть несколько тонких моментов, связанных с очередностью поступления уведомлений о потоках, но мы их рассматривать не будем.

 

Теперь о том, как устроены DLL написанные на Delphi и то что обычно скрыто от программистов. После того, как Windows произвел «проецирование DLL на адресное пространство вызывающего процесса», а по простому говоря загрузил DLL в память, в этот момент происходит вызов функции расположенной в точке входа и передача уведомления DLL_PROCESS_ATTACH этой функции. В DLL написанном на Delphi в этой точке входа расположен специальный код, который делает много разных вещей, включая и запуск инициализации units. Запоминает что инициализация и первый запуск DLL были произведены, и выполняет begin...end главного файла проекта. Таким образом, этот код первоначальной загрузки выполняется только один раз, все остальные обращения Windows в точку входа происходят уже к другой функции, которая и обрабатывает последующие уведомления, - фактически игнорирует их, кроме сообщения DLL_PROCESS_DETACH, по которому производит финализацию unit. Так, в общих чертах, выглядит механизм загрузки DLL написанной на Delphi. В большенстве случаев этого достаточно для написания и использования DLL в MQL4.

 

Если всё же нужен DllMain точно такой же как и в C, то его несложно организовать. Делается это следующим образом. При первой загрузке DLL, в числе прочего, модуль System (он всегда присутствует в программе или DLL) автоматически создает глобальную процедурную переменную DllProc, которая инициализируется nil. Это означает что никакой дополнительной обработки уведомлений DllMain, кроме той которая существует, не требуется. Как только этой переменной будет присвоен адрес функции, то все уведомления для DLL от Windows станут поступать к этой функции. Что и требуется от точки входа. При этом, уведомление DLL_PROCESS_DETACH всё равно будет отслеживаться функцией завершения DLL, как и раньше, для того что бы была возможность провести финализацию.

 

procedure DllEntryPoint(Reason: DWORD);

begin

  case Reason of

    DLL_PROCESS_ATTACH: ;    //'Подключение процесса'

    DLL_THREAD_ATTACH: ;     //'Подключение потока'

    DLL_THREAD_DETACH: ;     //'Отключение потока'

    DLL_PROCESS_DETACH: ;    //'Отключение процесса'

  end;

end;

 

begin

  if not Assigned(DllProc) then begin

    DllProc:= @DllEntryPoint;

    DllEntryPoint(DLL_PROCESS_ATTACH);

  end;

end.

 

В том случае, если нас не интересуют уведомления о потоках, всё это излишне. Стоит только в unit организовать секции initialization/finalization как события подключения и отключения процесса будут отслеживаться автоматически.

 

Коварство и вероломство DllMain

 

Теперь, пожалуй, пришло время коснуться вопроса который удивительно мало освещен в литературе по программированию. Эта тема касается не только Delphi или С, но вообще любого языка программирования, способного создавать DLL. Это свойство загрузчика DLL Windows. Из переведенной серьёзной и широкораспостраненной литературе по программирования в среде Windows, только у одного автора удалось найти упоминание об этом и то, в самых туманных выражениях. Этот автор Дж. Рихтер, и ему простительно, так как его замечательная книга вышла в 2001 году, когда в общем то и 32 разрядный Windows не был так распостранен.

 

Интересно, что сам MS ни когда не скрывал существование проблемы с DllMain и даже разместил у себя специальный документ, что то вроде – «Наилучший способ использования DllMain». В котором объяснил, что можно делать в DllMain, а что нерекомендуется. Причем, было указано, что нерекомендованные вещи ведут к трудноуловимым и непостоянным ошибкам. Желающие ознакомиться с этим документом могут заглянуть сюда. Более популярное изложение нескольких переводов алармистких сообщений по этому поводу изложено здесь.

 

Суть проблемы очень проста. Дело в том, что DllMain, особенно при загрузке DLL, это особое место. Место где нельзя делать ничего сложного и выдающегося. Например, не рекомендуется CreateProcess, или LoadLibrary другие DLL. Так же не рекомендуется CreateThread или проводить CoInitialize COM. И т.д.

 

Можно делать самые простые вещи. Иначе ничего не гарантируется. Поэтому, не помещайте ничего лишнего в DllMain, иначе будете с удивлением наблюдать крах приложений использующих вашу DLL. Лучше перестраховаться и создать специальные экспортированные функции инициализации и финализации, которые в нужные моменты будет вызывать главное приложение. Это, по крайней мере, поможет избежать проблем с DllMain.
 

ExitProc, ExitCode, MainInstance, HInstance

 

В модуле System, который всегда подцепляется к вашей DLL при компиляции, есть несколько полезных глобальных переменных, которые можно использовать.

 

ExitCode, - переменная в которую при загрузке можно занести число отличное от 0, в результате загрузка DLL прекратится.

 

ExitProc, - процедурная переменная, которая может хранить адрес функции, которая будет исполнена при выходе. Данная переменная пережиток далекого прошлого, в DLL не функционирует и более того, разработчики Delphi крайне нерекомендуют её использовать в DLL из-за возможных проблем.

 

HInstance, - переменная в которой после загрузки хранится дескрептор самой DLL. Бывает очень полезна.

 

MainInstance, - дескрептор приложения, которое загрузило к себе в адресное пространство DLL.

 

IsMultiThread, - переменная, которая автоматически устанавливается в True, если при компиляции в DLL обнаруживается работа с потоками. Исходя из значения этой переменной менеджер памяти DLL переключается в многопоточный режим. В принципе, существует возможность принудительно переключить менеджер памяти в многопоточный режим, даже если потоки в DLL явно не используются. IsMultiThread:=True; Естественно, многопоточный режим медленее однопоточного, за счет того, что проводится синхронизация потоков между собой.

 

MainThreadID, - дескрептор главного потока приложения.

 

И т.д. Вообще, модуль System выполняет примерно те же самые функции что и CRT в C. Включая и функции управления памятью. Список всех функций и переменных, которые присутствуют в откомпилированной DLL, не только экспортируемые, но вообще все, можно получить если включить в настройках Проекта опцию Линкера, Мап файл - Детальный.
 

Управление памятью

 

Следующий, довольно серьёзный вопрос, который часто вызывает трудности, это управление памятью в DLL. Точнее, само управление памятью ни каких проблем не вызывает, но как только DLL пытается активно работать с участками памяти распределенной менеджером памяти самого приложения, - вот тут обычно начинаются проблемы.

 

Дело в том, что обычно приложения компилируется уже со встроенным в него MemoryManager. Откомпилированное DLL так же содержит у себя свой MemoryManager. Особенно это актуально для приложений и DLL созданных в разных средах программирования. Так же как в нашем случае, терминал – в MSVC, DLL – в Delphi. Понятно, что это разные по своей структуре менеджеры, но вместе с этим это ещё и физически разные менеджеры, управляющий каждый своей памятью, в пределах общего адресного пространства процесса. В принципе, они не мешают друг другу, не отбирают друг у друга память, они существуют параллельно друг другу и обычно ничего «не знают» о существование конкурентов. Это возможно потому что, оба менеджера обращаются за памятью к одному источнику, - менеджеру памяти Windows.

 

Проблемы начинаются в том случае, когда функция DLL и приложения пытается управлять участками памяти распределенными не собой. По этому, в среде программистов, существует некое эмпирическое правило, которое гласит, что «память не должна пересекать границы модуля кода».

 

Хорошее правило, но оно не совсем корректное. Правильнее было бы просто использовать в DLL тот же самый MemoryManager, который использует приложение. На самом деле, мне больше нравится идея подключить менеджер памяти MT4 к менеджеру памяти Delphi FastMM, но это вообще малореализуемая идея. Как бы то ни было, управление памятью должно быть одно.

 

В Delphi есть возможность заменить стандартный менеджер памяти на любой другой, соответствующий некоторым требованиям. Таким образом можно сделать так, что у DLL и приложения будет будет единий менеджер памяти, и это будет менеджер памяти MT4.

 



Причина обращения: