Как за 10 минут написать DLL библиотеку на MQL5 (Часть II): Пишем в среде Visual Studio 2017

5 апреля 2019, 07:12
Andrei Novichkov
6
1 969

Введение

Данная статья является развитием ранее написанной статьи про создание DLL с использованием Visual Studio 2005/2008. Та "базовая", первоначальная статья, отнюдь не потеряла актуальности и всем интересующимся данной темой просто необходимо ее прочесть. Но с тех пор прошло достаточно много времени, сейчас актуален Visual Studio 2017, у которого изменился, пусть и не значительно, интерфейс, да и сама платформа MetaTrader 5 развивалась и не стояла на месте. Очевидно, что есть необходимость обновить сведения, рассмотреть какие-то новые возможности и уточнить старые. Именно этим мы и займемся сейчас, проделав полностью весь путь от создания проекта DLL в Visual Studio 2017 до подключения готовой DLL к терминалу и работе с ней.

Статья рассчитана на начинающих разработчиков, желающих овладеть созданием и подключением библиотек, написанных на C++,  к терминалу.

Зачем все это нужно

В среде разработчиков бытует мнение, что никаких библиотек к терминалу подключать вообще не надо. Что задач, требующих такое подключение, просто нет, что все, что требуется, можно сделать средствами MQL. В определенной степени такое мнение верно. Действительно, задач, требующих подключение библиотек мало. И, да — многие из этих задач можно решить средствами MQL, и примеры этого мы весьма часто видим. Кроме того, применяя библиотеку, нужно учитывать, что теперь советник, или индикатор, который станет использовать эту библиотеку, будет работоспособен только при её наличии. Если разработчик захочет передать такой инструмент третьему лицу, то передавать придется два файла — сам инструмент и библиотеку, которую инструмент использует. Это может оказаться весьма неудобным, а иной раз и просто невозможным. Есть и еще одна сторона медали — библиотеки могут быть небезопасны и содержать деструктивный код.

Несмотря на вышесказанное, можно отметить и выгоды применения библиотек, которые определенно перевешивают минусы. Например:

  • Только со сторонними библиотеками возможно решить те задачи, которые MQL решить не сможет. Например, выполнить почтовую рассылку. Да еще и файл к сообщению присоединить. Написать в Skype. И т.д.
  • Выполнить те задачи, которые можно выполнить и средствами MQL, но быстрее и существенно эффективнее. Например парсинг HTML-страниц, работу с регулярными выражениями.

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

Теперь, рассмотрев все "За" и "Против" использования библиотек в своих проектах, мы начнем проходить этап за этапом процесс создания DLL в среде Visual Studio 2017.

Создание простой библиотеки DLL

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

Итак, в среде Visual Studio 2017 выбираем File -> New -> Project. В появившемся окне, в левой части, раскрываем список Visual C++ и в нем выбираем Windows Desktop, а в средней части  выделяем строку Windows Desktop Wizard. В нижней части имеются несколько полей ввода, где можно изменить имя (рекомендуется задать свое и осмысленное) и месторасположение проекта (лучше оставить так, как предлагается). Всё готово, нажимаем кнопку "ОК" и переходим в следующее окно:


Здесь в выпадающем списке нужно выбрать Dynamic Link Library (.dll) и отметить галкой пункт "Export Symbols". На самом деле отмечать галкой этот пункт необязательно, но желательно начинающим разработчикам. В этом случае в файлы проекта будет добавлен демонстрационный код, который можно просмотреть, а затем удалить, либо закомментировать. Нажимаем на кнопку "ОК", и создаются файлы проекта, которые мы можем затем редактировать. Однако делать это еще рано, пока разберемся с настройками проекта. Во первых, нужно помнить, что MetaTrader 5 работает только с 64-х разрядными библиотеками. Если попытаться присоединить 32-х разрядную, то мы получим следующие сообщения:

'E:\...\MQL5\Libraries\Project2.dll' is not 64-bit version
Cannot load 'E:\MetaTrader 5\MQL5\Libraries\Project2.dll' [193]

Соответственно, никакой работы сделать будет нельзя.

То же самое относится и к MetaTrader 4, но там наоборот — нужны 32-х разрядные библиотеки и невозможно присоединить 64-х разрядные. Об этом стоит помнить, чтобы не делать лишнюю работу.

Теперь переходим к собственно настройкам проекта. Выбираем в меню "Project" пункт "Name Properties...",  где "Name" — имя проекта, выбранное разработчиком на этапе создания. В результате получаем окно со множеством разнообразных настроек. И первое, что следует сделать, это включить поддержку Юникода. В левой части окна выбираем элемент "General", а в правой строку с заглавием в первой колонке: "Character Set". Тогда во второй колонке станет доступен выпадающий список, в котором следует выбрать "Use Unicode Character Set". В некоторых случаях без поддержки Юникода можно обойтись, но об этом будет сказано позже.

Еще одно очень полезное (но не необходимое) изменение свойств проекта: Копирование готовой библиотеки в папку "Library" терминала. В первоначальной статье для этого рекомендовалось менять параметр "Output Directory", который находится в том же окне элемента "General" проекта. В существующем Visual Studio 2017 так делать не нужно. Данный параметр следует оставить без изменения, а обратить внимание на раскрывающийся элемент "Build Events" в левом окне и выбрать его подэлемент "Post Build Events". В первой колонке правого окне появится параметр "Command Line", выбор которого дает доступ к выпадающему списку во второй колонке, который можно редактировать. В списке должен содержаться перечень действий, которые выполнит Visual Studio 2017 после построения библиотеки. Добавим в этот список такую строку:

xcopy "$(TargetDir)$(TargetFileName)" "E:\...\MQL5\Libraries\" /s /i /y

, где на месте многоточия должен быть полный путь к соответствующей папке терминала. Теперь, в том случае, если построение библиотеки завершилось успешно, она будет скопирована на указанное место. При этом все файлы в "Output Directory" останутся на месте, что может быть важным, если разработчик работает с системами контроля версий, например.

Последний и очень важный этап настройки проекта. Представим себе, что библиотека уже построена и там находится одна функция, которую сможет использовать терминал. Пусть эта функция имеет такой прототип:

int fnExport(wchar_t* t);
Из скрипта терминала эту функцию можно будет вызвать так:
#import "Project2.dll"
int fnExport(string str);
#import

Однако, если попытаться сделать это, то получим следующее сообщение об ошибке:

Что делать в этой ситуации? Обратим внимание на то, что Visual Studio 2017 при генерации кода библиотеки сформировал макрос:

#ifdef PROJECT2_EXPORTS
#define PROJECT2_API __declspec(dllexport)
#else
#define PROJECT2_API __declspec(dllimport)
#endif

и прототип нашей функции полностью выглядит так:

PROJECT2_API int fnExport(wchar_t* t);

После компиляции библиотеки посмотрим, как выглядит таблица экспорта:


Для просмотра просто выделим файл с библиотекой в окне "Total Commander" и нажмем F3. Обратите внимание, как выглядит имя экспортируемой функции. Теперь отредактируем макрос, который мы привели выше (именно так сделано в первоначальной статье):

#ifdef PROJECT2_EXPORTS
#define PROJECT2_API extern "C" __declspec(dllexport)
#else
#define PROJECT2_API __declspec(dllimport)
#endif

Вставка

extern "C"

обозначает использование простой генерации сигнатуры функции (в стиле языка С) при получении объектных файлов. В частности, это запрещает компилятору C++ производить "декорацию" (или "украшение") имени функции дополнительными символами при экспорте в DLL. Повторим компиляцию и опять посмотрим, как выглядит таблица экспорта:

Изменения в таблице экспорта налицо и ошибка при вызове функции из скрипта исчезла. Однако, с моей точки зрения, этот способ имеет недостаток — нужно редактировать скрипт, созданный компилятором. Есть более безопасный способ добиться тех же результатов, пусть немного более длинный:

Файл определений

Это обычный текстовый файл, как правило, имеющий имя, совпадающее с именем проекта и имеющий расширение def. Т.е. в данном случае это будет файл Project2.def. Создается такой файл в обычном блокноте, ни в коем случае не в Word и подобных ему редакторах. Содержание файла будет примерно таким:

; PROJECT2.def : Declares the module parameters for the DLL.

LIBRARY      "PROJECT2"
DESCRIPTION  'PROJECT2 Windows Dynamic Link Library'

EXPORTS
    ; Explicit exports can go here
        fnExport @1
        fnExport2 @2
        fnExport3 @3
        ....

Сначала заголовок, а потом просто список экспортируемых функций. Символы вида @1, @2 и т.д. обозначают желательный порядок следования функций в библиотеки. Сохранить этот файл следует в папке проекта.

Создадим этот файл и подключим к проекту. В окне свойств проекта, в левом окне выберем раскрывающийся элемент "Linker" и его подэлемент "Input", а в правом параметр "Module Definition File". Так же, как и в прошлых случаях, получаем доступ к редактируемому списку, куда добавляем имя файла: "Project2.def". Нажимаем кнопку "OK" и повторяем компиляцию. Получаем такой же результат, что и на последнем скриншоте. Имя не задекорировано и ошибок при вызове функции скриптом нет. Разобравшись с настройками проекта, можно приступать к написанию кода собственно библиотеки.

Создание библиотеки и DllMain

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

1. Добавим в экспорт функцию (и не забудем отредактировать файл определений):

PROJECT2_API int fnExport1(void) {
        return GetSomeParam();
}

2. Создадим и добавим в проект заголовочный файл Header1.h и пропишем в него другую функцию:

const int GetSomeParam();
3.Отредактируем файл dllmain.cpp:
#include "stdafx.h"
#include "Header1.h"

int iParam;

BOOL APIENTRY DllMain( HMODULE hModule,
                       DWORD  ul_reason_for_call,
                       LPVOID lpReserved
                     )
{
    switch (ul_reason_for_call)
    {
    case DLL_PROCESS_ATTACH:
                iParam = 7;
                break;
    case DLL_THREAD_ATTACH:
                iParam += 1;
                break;
    case DLL_THREAD_DETACH:
    case DLL_PROCESS_DETACH:
        break;
    }
    return TRUE;
}

const int GetSomeParam() {
        return iParam;
}

Смысл этого кода должен быть понятен: в библиотеку добавлена переменная, значение которой вычисляется в функции DllMain и доступно при помощи функции fnExport1. Вызовем функцию в скрипте:

#import "Project2.dll"
int fnExport1(void);
#import
...
void OnStart() {
Print("fnExport1: ",fnExport1() );

Мы получаем следующую запись:

fnExport1: 7

Это говорит о том, что эта часть кода в DllMain не выполняется:

    case DLL_THREAD_ATTACH:
                iParam += 1;
                break;

Насколько это важно? С моей точки зрения важно чрезвычайно, т.к, если разработчик часть кода по инициализации библиотеки разместит в этом ветвлении в расчете на то, что он выполнится при присоединении библиотеки к потоку, то его расчеты не оправдаются. При этом никакой ошибки зафиксировано не будет, что несомненно затруднит ее поиск.

Строки

Как работать со строками говорится в первоначальной статье. Работа эта не сложна, но есть один момент, который нуждается в пояснениях.

Создадим в библиотеке простую функцию (и отредактируем файл определений):

PROJECT2_API void SamplesW(wchar_t* pChar) {
        size_t len = wcslen(pChar);
        wcscpy_s(pChar + len, 255, L" Hello from C++");
}
Вызовем эту функцию в скрипте:
#import "Project2.dll"
void SamplesW(string& pChar);
#import

void OnStart() {

string t = "Hello from MQL5";
SamplesW(t);
Print("SamplesW(): ", t);

Мы вполне ожидаемо получим такое сообщение:

SamplesW(): Hello from MQL5 Hello from C++

Изменим вызов функции:

#import "Project2.dll"
void SamplesW(string& pChar);
#import

void OnStart() {

string t;
SamplesW(t);
Print("SamplesW(): ", t);

И теперь получим устрашающее сообщение об ошибке:

Access violation at 0x00007FF96B322B1F read to 0x0000000000000008

Проинициализируем строку, которую передаем библиотечной функции и повторим выполнение скрипта:

string t="";

Сообщение об ошибке исчезло, снова получаем ожидаемый вывод:

SamplesW():  Hello from C++

Из всего вышесказанного можно сделать вывод: строки, передаваемые в экспортируемые библиотекой функции, должны быть обязательно проинициализированы!

Здесь мы подошли к вопросу, к которому обещали вернуться при обсуждении использования Юникода. Если в DLL не планируется передавать строки, как в последнем примере, то можно обойтись и без поддержки Юникода. Однако, эту поддержку лучше все таки включать в любом случае, т.к. сигнатуры экспортируемых функций могут измениться, появляться новые, и разработчик может просто забыть, что поддержки Юникода нет.

Передача и прием массивов символов какими-то особенностями не обладает, рассмотрена в первоначальной статье и здесь мы на ней останавливаться не станем.

Структуры

Определим простейшую структуру в библиотеке и скрипте:

//В dll:
typedef struct E_STRUCT {
        int val1;
        int val2;
}ESTRUCT, *PESTRUCT;

//В скрипте MQL:
struct ESTRUCT {
   int val1;
   int val2;
};

Добавим функцию для работы с этой структурой в библиотеку:

PROJECT2_API void SamplesStruct(PESTRUCT s) {
        int t;
        t = s->val2;
        s->val2 = s->val1;
        s->val1 = t;
}

Из кода видно, что функция просто выполняет обычный swap собственных полей.

Вызовем функцию из скрипта:

#import "Project2.dll"
void SamplesStruct(ESTRUCT& s);
#import
....
ESTRUCT e;
e.val1 = 1;
e.val2 = 2;
SamplesStruct(e);
Print("SamplesStruct: val1: ",e.val1," val2: ",e.val2);

Выполним скрипт и получим предсказуемый результат:

SamplesStruct: val1: 2 val2: 1

Объект был передан в вызываемую функцию по ссылке, функция обработала объект и вернула его вызывающему коду.

Однако, работой с такими простыми структурами удается ограничиться не часто. Усложним задачу, добавим в структуру еще одно поле, но другого типа:

typedef struct E_STRUCT1 {
        int val1;
        char cval;
        int val2;
}ESTRUCT1, *PESTRUCT1;

... и функцию для работы с ней:

PROJECT2_API void SamplesStruct1(PESTRUCT1 s) {
        int t;
        t = s->val2;
        s->val2 = s->val1;
        s->val1 = t;
        s->cval = 'A';
}

Функция, как и предыдущая, делает swap своих полей типа int и присваивает значение полю типа char. Вызовем эту функцию в скрипте (точно тем же способом, что и предыдущую функцию). И мы совершено неожиданно получаем вот такую запись в журнал:

SamplesStruct1: val1: -2144992512 cval: A val2: 33554435

Поля структуры типа int совершенно явно содержат мусор. Мы не получаем исключение, а именно случайный мусор, неверные данные. Что же случилось? Все дело в выравнивании! "Выравнивание", это понятие не самое простое, но и не из сложных. Есть раздел в документации pack, посвященный структурам, там достаточно подробно описано, что это такое. Что касается выравнивания в среде Visual Studio C++, то выравниванию там тоже посвящено достаточно много материала.

В нашем же примере источник ошибки в том, что в данном случае у библиотеки и скрипта разные выравнивания, и поэтому скрипт "зацепляет" мусор. Решить проблему можно двумя способами:

  1. Указать новое выравнивание в скрипте. Для этого имеется атрибут pack(n). Попытаемся выровнять структуру по полю максимальной величины, т.е. по int:
    struct ESTRUCT1 pack(sizeof(int)){
            int val1;
            char cval;
            int val2;
    };
    И повторим вывод, выполнив скрипт. Теперь запись в журнале изменилась: SamplesStruct1: val1: 3 cval: A val2: 2 . Все в порядке, проблема решена.

  2. Указать новое выравнивание в библиотеке. По умолчанию структуры в MQL имеют выравнивание pack(1), нужно такое же применить в библиотеке так:
    #pragma pack(1)
    typedef struct E_STRUCT1 {
            int val1;
            char cval;
            int val2;
    }ESTRUCT1, *PESTRUCT1;
    #pragma pack()
    Собираем библиотеку, снова выполняем скрипт и получаем правильный результат, такой же, что и при использовании 1-го способа.
Проверим еще один момент. Что будет, если кроме полей данных, в структуре окажутся методы? Такое вполне возможно. Разработчик вправе добавить конструктор, например (хотя это не метод, разумеется). Деструктор, еще что-то по своему усмотрению. Проверим это на такой структуре в библиотеке:
#pragma pack(1)
typedef struct E_STRUCT2 {
        E_STRUCT2() {
                val2 = 15;
        }
        int val1;
        char cval;
        int val2;
}ESTRUCT2, *PESTRUCT2;
#pragma pack()
Эту структуру будет использовать следующая функция:
PROJECT2_API void SamplesStruct2(PESTRUCT2 s) {
        int t;
        t = s->val2;
        s->val2 = s->val1;
        s->val1 = t;
        s->cval = 'B';
}
Сделаем соответствующие изменения в скрипте:
struct ESTRUCT2 pack(1){
        ESTRUCT2 () {
           val1 = -1;
           val2 = 10;
        }
        int val1;
        char cval;
        int f() { int val3 = val1 + val2; return (val3);}
        int val2;
};

#import "Project2.dll" 
void SamplesStruct2(ESTRUCT2& s); 
#import
...
ESTRUCT2 e2;
e2.val1 = 4;
e2.val2 = 5;
SamplesStruct2(e2);
t = CharToString(e2.cval);
Print("SamplesStruct2: val1: ",e2.val1," cval: ",t," val2: ",e2.val2);

Обратите внимание, что в структуру добавлен метод f(), чтобы отличий от структуры в библиотеке было еще больше. Выполняем скрипт и получаем такую запись в журнал: SamplesStruct2:  val1: 5 cval: B val2: 4  Все в порядке! Наличие конструктора и дополнительного метода в нашей структуре никак не повлияло на результат.

Последний эксперимент. Уберем из структуры в скрипте конструктор и метод, оставим только поля с данными, а структуру в библиотеке оставим без изменений. Опять выполним скрипт и получим тот же самый правильный результат. Теперь уже можно сделать окончательный вывод, что присутствие дополнительных методов в структурах никак не влияет на результат.

Проект данной библиотеки для Visual Studio 2017 и скрипт для MetaTrader 5 находятся в файлах, присоединенных к статье.

О том, чего делать нельзя

При работе с библиотеками dll существует ограничения и они описаны в документации. Повторять написанное в документации мы здесь не будем. Приведем лишь пример:

struct BAD_STRUCT {
   string simple_str;
};

Такую структуру передать в dll нельзя. А ведь мы просто обернули строку (одну строку!) структурой! Тем более нельзя будет передать dll более сложные объекты, не получив при этом исключение.

О том, что делать, когда нельзя

Бывает достаточно много случаев, когда в dll необходимо передать объект, который передавать запрещено. Структуру с динамическими объектами, зубчатый массив и т.д. Что делать в таком случае? Если у разработчика нет доступа к коду библиотеки, то придется отказаться от такого решения. Ситуация совершенно иная, если такой доступ имеется.

Мы не будем рассматривать ситуацию с изменением дизайна данных, нужно постараться решить проблему имеющимися средствами и не получить при этом исключения. Внесу уточнение. Т.к. статья не предназначена для опытных пользователей, то здесь мы наметим только возможные пути решения проблемы, а написание собственно кода оставим для упражнения и самосовершенствования читателей.

  1. Возможность применения функции StructToCharArray(). Это очень заманчивая возможность, дающая возможность написать примерно такой код в скрипте:
    struct Str 
      {
         ...
      };
    
    Str s;
    uchar ch[];
    StructToCharArray(s,ch);
    
    SomeExportFunc(ch);
    И в cpp файле библиотеки:
    #pragma pack(1)
    typedef struct D_a {
    ...
    }Da, *PDa;
    #pragma pack()
    
    void SomeExportFunc(char* pA)
      {
            PDa = (PDa)pA;
            ......
      }
    Оставив за скобками безопасность и качество такого кода, сразу отметим бесполезность самого способа: StructToCharArray() работает только с POD-структурами, а такие структуры можно передавать в библиотеки без дополнительных преобразований. Замечу, что применение данной функции в "реальной" жизни мною не проверялось.

  2. Написать собственный упаковщик/распаковщик структур в некий объект, который можно будет передавать в библиотеку. Способ возможный, но явно весьма сложный и трудозатратный. Однако это приводит нас к вполне приемлемому решению:

  3. Все объекты, которые нельзя передать в библиотеку непосредственно, упаковывать в строку формата JSON в скрипте и распаковывать в структуры в библиотеке. И наоборот. Необходимый инструментарий для этого есть. Парсеры для JSON имеются и для C++, и для C#, и для MQL. Такую возможность вполне можно использовать, если пожертвовать временем на упаковку/распаковку. Да, определенные потери времени очевидно будут. Но и преимущества налицо. Можно работать со структурами (и не только со структурами) весьма высокой сложности. И в случае необходимости не писать упаковщик/распаковщик "с нуля", а доработать существующий, что очевидно проще.

Таким образом будем иметь в виду, что возможность передать в (и получить из) библиотеку сложный объект, возможно, все таки имеется.

Практическое применение

Применим полученные знания на практике и создадим полезную библиотеку. Пусть это будет библиотека, отсылающая почту. Сразу отметим некоторые моменты:

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

И последнее. Большая часть кода на C++ принадлежит не мне, а скачана с форумов Microsoft. Это весьма старый, проверенный пример, варианты которого есть в том числе и на VBS.

Начнем. Создадим проект в Visual Studio 2017 и изменим его настройки так, как описано в начале статьи. Создадим файл определений и подключим его к проекту. У нас будет единственная экспортируемая функция:

SENDSOMEMAIL_API bool  SendSomeMail(LPCWSTR addr_from,
        LPCWSTR addr_to,
        LPCWSTR subject,
        LPCWSTR text_body,

        LPCWSTR smtp_server,
        LPCWSTR smtp_user,
        LPCWSTR smtp_password);

Смысл её аргументов интуитивно понятен, но все таки кратко поясним:

  • addr_from, addr_to — почтовые адреса отправителя и получателя.
  • subject, text_body — это тема и собственно письмо.
  • smtp_server, smtp_user, smtp_password — адрес SMTP-сервера, логин пользователя на этом сервере и пароль.

Обратим внимание на некоторые моменты:

  • Из описания аргументов следует, что для отсылки почты нужно иметь аккаунт на почтовом сервере и знать его адрес. Поэтому ни о какой анонимности отправителя не может быть и речи.
  • В коде библиотеки жестко зашит номер порта. Это стандартный порт номер двадцать пять (25).
  • Библиотека получает необходимые данные, связывается с сервером и отсылает на него почту. За один вызов можно отправить почту только на один адрес. Если разработчик желает повторить отправку, то вызов функции придется повторить с новым адресом.

Сам код на C++ мы здесь приводить не будем. Его (да и весь проект целиком) можно найти в прилагаемом проекте SendSomeMail.zip. Скажу лишь, что используемый объект CDO обладает множеством возможностей и может (и должен) быть использован для развития и усовершенствования библиотеки.

Кроме этого проекта мы напишем простой скрипт для вызова библиотечной функции (он находится в прилагаемом файле SendSomeMail.mq5):

#import "SendSomeMail.dll"
bool  SendSomeMail(string addr_from,string addr_to,string subject,string text_body,string smtp_server,string smtp_user,string smtp_password);
#import

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
  {
   bool b = SendSomeMail("XXX@XXX.XX", "XXXXXX@XXXXX.XX", "hello", "hello from me to you","smtp.XXX.XX", "XXXX@XXXX.XXX", "XXXXXXXXX");
   Print("Send mail: ", b);
   
  }

Вместо символов "Х" разработчику придется подставить свои значения, т.к. я не могу  приводить данные своего аккаунта. На этом разработка завершена. После подстановки правильных данных и возможного внесения каких либо дополнений в код, библиотеку вполне можно использовать.

Заключение

Таким образом, используя сведения первоначальной статьи и учитывая то новое, что содержится в этой, разработчик может быстро освоить азы и перейти к более сложным и интересным проектам.

Под конец хочется остановиться на одном интересном факте, могущим иметь большое значение в определенных ситуациях. Что, если возникнет необходимость защитить код в dll? Стандартное решение — использовать упаковщик. Различных упаковщиков достаточно много, и многие из них могут обеспечить неплохой уровень защиты. Так вышло, что у меня есть два из них: Themida 2.4.6.0 и VMProtect Ultimate v. 3.0.9 . Применим эти упаковщики и упакуем нашу первую, простейшую Project2.dll в двух вариантах для каждого из упаковщиков. После чего, используя уже имеющийся скрипт, вызовем экспортируемые функции в терминале. Все работает! Терминал может работать с такими библиотеками, что однако не может дать гарантию нормальной работы с библиотеками, накрытыми другими упаковщиками. Упакованная в двух вариантах Project2.dll находится в присоединенном архиве Project2_Pack.zip

На этом все. Успехов и удачи в работе.

Программы, используемые в статье:

 # Имя
Тип
 Описание
1 Project2.zip Архив
Проект простой dll.
2
Project2.mq5
Скрипт
Скрипт для работы с dll.
3 SendSomeMail.zip Архив Проект dll для отсылки почты.
4 SendSomeMail.mq5 Скрипт
Скрипт для работы с библиотекой SendSomeMail. dll
5 Project2_Pack.zip Архив Project2.dll, упакованная Themida и VMProtect.




Прикрепленные файлы |
Project2.mq5 (3.42 KB)
SendSomeMail.mq5 (1.15 KB)
SendSomeMail.zip (16.62 KB)
Project2_Pack.zip (4645.25 KB)
Project2.zip (18.65 KB)
Maxim Kuznetsov
Maxim Kuznetsov | 8 апр 2019 в 18:07

Ещё момент - не освещённый в статье, но востребованный.

Если уж C++, то пожалуй должны быть классы и причём с обеих сторон, и в Mql и в С++.

"протаскивание класса C++ в Mql".

1. пишем (или берём готовый) класс :-)

получаем нечто такое :

#ifndef MQLPLUG_H
#define MQLPLUG_H 1
#include "mql45.h"
/** пример класса который "протаскивается" в MT
**/
class Plug {
public:
        Plug();
        ~Plug();

        mql_int OnInit();
        void OnDeinit(mql_int);

        mql_int Sum(mql_int,mql_int);
        mql_double Median(MqlRates *rate);
};
/* так как методы C++ нельзя  "протащить" через DLL, а только функции C
        то и делаются простые функции делегирующие вызов к объекту
*/
MQL_API(Plug *) Plug_New();
MQL_API(void) Plug_Delete(Plug *);
MQL_API(mql_int) Plug_OnInit(Plug *);
MQL_API(void) Plug_OnDeinit(Plug *,mql_int);
MQL_API(mql_int) Plug_Sum(Plug *,mql_int,mql_int);
MQL_API(mql_double) Plug_Median(Plug *,MqlRates *rate);
#endif

2. Реализация "функций-делегатов" тривиальна - первый агрумент это указатель на объект, внутри функции надо проверить его корректность, вызвать метод и перехватить исключения.

/// MqlPlug.cpp
#include "MqlPlug.h"
/*** функции-обёртки методов класса Plug
    все (кроме _New) первым аргументом получают указатель на объёкт
    прочие аргументы - такие же как у метода
    если переданные указатель корректен (не nullptr) то вызывается метод
    и заодно вылавливаются все исключения 
***/
/// конструктор
MQL_API(Plug *)
Plug_New() {
        try {
                return new Plug;
        } catch (...) {
        }
        return nullptr;
}
/// деструктор
MQL_API(void)
Plug_Delete(Plug *plug) {
        try {
                delete plug;
        } catch (...) {
        }
}
// прочие методы
MQL_API(mql_int)
Plug_Sum(Plug *plug,mql_int one,mql_int two) {
        try {
                if (plug) return plug->Sum(one,two);
        } catch (...) {
        }
        return 0;
}
// и так далее

3. И наконец-то Mql ! В директивах импорт описываются функции-делегаты, и пишется класс имеющий единственное поле obj - хандлер (указатль на) объект, и методы вызывающите делегалов

#ifdef __MQL4__
// для 4-ки дескриптор (указатель) 32 бита)
#define HANDLE int
#else
// для 5-ки - 64
#define HANDLE long
#endif

#import "Mql4Plug.dll"
HANDLE Plug_New(void);
void Plug_Delete(HANDLE);
int Plug_OnInit(HANDLE);
void Plug_OnDeinit(HANDLE,const int reason);
int Plug_Sum(HANDLE,int,int);
double Plug_Median(HANDLE,MqlRates &);
#import

class Plug {
public:
   HANDLE obj;
   Plug() {
      obj = Plug_New();
   }
   ~Plug() {
      Plug_Delete(obj);
   }
   int OnInit() {
      if (obj != NULL) {
         return Plug_OnInit(obj);
      }
      return INIT_FAILED;
   }
   void OnDeinit(const int reason) {
      if (obj != NULL) {
         Plug_OnDeinit(obj,reason);
      }
   }
   int Sum(int one,int two) {
      if (obj != NULL) {
         return Plug_Sum(obj,one,two);
      }
      return 0;
   }
   double Median(MqlRates &rates) {
      if (obj!=NULL) {
         return Plug_Median(obj,rates);
      }
      return EMPTY_VALUE;
   }
};

PS/ Как-то раз такое запрашивали и я честно пытался объяснить. Но то-ли педагог из меня так-себе, то-ли визави в программировании был не аллё :-) Но популярные примеры осталась - поэтому делюсь

Andrei Novichkov
Andrei Novichkov | 8 апр 2019 в 19:34

Добрый вечер. Сразу постараюсь на все ответить.

Maxim Kuznetsov:
...

Никакие свистопляски с *.def ненужны, при необходимости def генерится автоматом.  Кстати, и DllMain вообще ненужен, его можно смело выкидывать :-) Точнее иногда бывает нужен, но крайне редко и это за гранью потребностей библиотек для MT.

В статье говорится совершенно не о том, как генерить файл определений, можно, или нет это сделать, какой инструмент для этого использовать. В статье говорится для чего он нужен, для чего может быть применен в VS и что из этого получится. А как его сделать, чем и когда, это не важно.

Насчет DllMain. Теоретически Вы правы. Да, без этой функции можно обойтись. Я знаю инструменты, где DllMain не будет вызываться, даже, если она есть. А вот с категоричностью Вашего вывода, что это "за гранью" я совершенно не согласен. Я убежден, что такой вывод должен сделать сам разработчик, как несущий ответственность за результат. Если ему нужно, он может что то вызвать в DllMain. Не захочет - напишет отдельную экспортируемую функцию. Лично я не чувствую себя достаточно компетентным, что бы вот так запросто взять и лишить его дополнительной возможности.

Maxim Kuznetsov:

Ещё момент - не освещённый в статье, но востребованный.

Если уж C++, то пожалуй должны быть классы и причём с обеих сторон, и в Mql и в С++.

"протаскивание класса C++ в Mql".

Вот совершенно не обязательно, что бы был экспорт классов. )) Спасибо, что Вы упомянули про эту возможность и отдельное спасибо за пример. Лично мне не приходило в голову делать что то подобное и тем более, рекомендовать такую методику начинающим разработчикам. Ну вот посмотрите сами на искусственность приведенного кода, на его не рациональность. И какая необходимость может заставить разработчика обращаться с указателями таким странным образом. Другими словами, если такой код не будет падать, то как теоретический пример он интересен, а с практической точки зрения - вряд ли) Больше скажу, по моему мнению, если у разработчика возникает необходимость в подобном экспорте, то, скорее всего, он очень сильно ошибся с дизайном Помимо этого, обращаю Ваше внимание на то, что где то половина статьи посвящена структурам, а это "почти классы". Стоит остановиться на этом и не идти по стопам некоторых наших коллег с форума, которые с удовольствием засунули бы в MQL весь С++17 ))

Maxim Kuznetsov
Maxim Kuznetsov | 9 апр 2019 в 00:56

я с вами и не спорю ;-)

так - некоторые заметки которые вспомнились, глядишь кому и пригодится.

не споря со статьёй

какие такие "начинающие программисты" на стыке двух языков ?

PS/ кстати у вас там память упахана :-)

Igor Makanu
Igor Makanu | 9 апр 2019 в 08:32
Maxim Kuznetsov:

я с вами и не спорю ;-)

так - некоторые заметки которые вспомнились, глядишь кому и пригодится.

Спасибо, пригодится

но к сожалению вот такие хорошие примеры очень часто лежат в топиках форума, найти очень сложно, увы и сам этим грешу ))))

Если не лень, то в блог бы себе такие отличные примеры складывали бы, хоть какая вероятность, что найдется быстрее, но тоже не факт ((((  -возможности у MQL потрясающие, но иногда быстрее самостоятельно прогуглить и найти готовый пример... да кстати, это форум очень хорошо индексируется гуглом, почти все запросы на тему нейросетей - будут результаты статей MQL ;)

Andrei Novichkov
Andrei Novichkov | 9 апр 2019 в 10:58
Maxim Kuznetsov:

я с вами и не спорю ;-)

так - некоторые заметки которые вспомнились, глядишь кому и пригодится.

не споря со статьёй

какие такие "начинающие программисты" на стыке двух языков ?

PS/ кстати у вас там память упахана

Что там с памятью, я наколбасил где то ?

Начинающий программист это понятие растяжимое ) Вот я совершенно точно начинающий программист в Питоне ) Или в ява скрипте. И много в чем еще я могу назвать себя начинающим. Вот и здесь, ну мало ли какая ситуация, если человек не делал раньше библиотек, а занимался двадцать лет САПРом, или плагины писал к Адобовским прогам? Конечно, он начинающий в новой области, но опытный в своей старой. В прочем ладно, не так уж и важна здесь эта терминология.

Исследование методов свечного анализа (Часть III): Библиотека работы с паттернами Исследование методов свечного анализа (Часть III): Библиотека работы с паттернами

Целью данной статьи является создание пользовательского инструмента, позволяющего получать и использовать весь массив информации о паттернах, рассмотренных ранее. Для этого будет разработана библиотека, которую можно будет использовать в своих индикаторах, торговых панелях, экспертах и т.д.

Цветная оптимизация торговых стратегий Цветная оптимизация торговых стратегий

В данной статье будет проведен эксперимент по раскрашиванию результатов оптимизации. Как известно, цвет определяется тремя параметрами: уровнями красного, зеленого и синего цветов (RGB от анг. Red — красный, Green — зеленый, Blue — синий). Существуют и другие способы кодирования цвета, но и в них цвет кодируется тремя параметрами. Таким образом, три показателя тестирования можно превратить в один, визуально воспринимаемый человеком, в цвет. На сколько такой показатель будет полезен вы сможете узнать из статьи.

Визуализация истории мультивалютной торговли по отчетам в форматах HTML и CSV Визуализация истории мультивалютной торговли по отчетам в форматах HTML и CSV

Как известно, MetaTrader 5 с момента своего появления предоставляет возможность мультивалютного тестирования. Эта функция востребована у большинства трейдеров, но, к сожалению, не столь универсальна, как того хотелось бы. В статье представлено несколько программ для разметки графиков с помощью графических объектов на основе торговой истории из отчетов форматов HTML и CSV. Торговля несколькими инструментами может анализироваться параллельно в нескольких подокнах, или в одном окне с помощью динамического переключения по команде пользователя.

Библиотека для простого и быстрого создания программ для MetaTrader (Часть V): Классы и коллекция торговых событий, отправка событий в программу Библиотека для простого и быстрого создания программ для MetaTrader (Часть V): Классы и коллекция торговых событий, отправка событий в программу

В предыдущих статьях мы начали создавать большую кроссплатформенную библиотеку, целью которой является упростить написания программ для платформы MetaTrader 5 и MetaTrader 4. В четвёртой части мы протестировали отслеживание торговых событий на счёте. В данной части создадим классы торговых событий, поместим их в коллекцию событий, откуда они будут отправляться в базовый объект библиотеки Engine и на график управляющей программы.