Як за 10 хвилин написати DLL бібліотеку для MQL5 і обмінюватися даними?

  1. 3. Способи передачі параметрів і обмін даними
  2. 4. Перехоплення винятків в DLL функції
  3. 5. врапперов DLL викликів і втрати швидкості на виклики

Так уже склалося, що зараз мало хто з розробників пам'ятає, як написати просту DLL бібліотеку і в чому особливості зв'язування різнорідних систем.

Я постараюся за 10 хвилин на прикладах показати весь процес створення простих DLL бібліотек і розкрию деякі технічні деталі нашої реалізації зв'язування. Демонстрація буде на прикладі Visual Studio 2005/2008, безкоштовні Express-версії яких можна вільно скачати з сайту Microsoft .

1. Створення проекту DLL на С ++ в Visual Studio 2005/2008

Запустіть візард через меню 'File -> New', виберіть тип проекту 'Visual C ++', шаблон 'Win32 Console Application' і вкажіть ім'я проекту (наприклад, 'MQL5DLLSamples'). Виберіть окремий кореневої каталог зберігання проектів 'Location' замість пропонованого за замовчуванням, вимкніть галочку 'Create directory for solution' і натисніть на кнопку 'OK':

Рис 1. Win32 Application Wizard, створення проекту DLL

На наступному кроці просто натисніть на кнопку 'Next' для переходу на сторінку налаштувань:

Рис 2. Win32 Application Wizard, параметри проекту

На фінальній сторінці виберіть тип 'DLL', залишивши інші поля порожніми як є, і натисніть на 'Finish'. Ставити галочку на 'Export symbols' не потрібно, щоб потім не видаляти автоматично доданий демонстраційний код:

Рис 3. Win32 Application Wizard, н астройка властивостей додатки

В результаті отримаєте порожній проект:

Рис 4. Порожній проект DLL

Для зручності тестування найкраще прямо в настройках 'Output Directory' вказати викладку DLL файлів безпосередньо в каталог '... \ MQL5 \ Libraries' клієнтського терміналу. Це заощадить багато часу в подальшій роботі:

Рис 5. Каталог викладки DLL файлів


2. Підготовка до додавання функцій

Додайте макрос '_DLLAPI' в кінець файлу stdafx.h, щоб можна було зручно і просто описувати експортовані функції:

#pragma once #include "targetver.h" #define WIN32_LEAN_AND_MEAN // Exclude rarely-used stuff from Windows headers #include <windows.h> #define _DLLAPI extern "C" __declspec (dllexport)

У викликах функцій MQL5 використовується Угода про зв'язки __stdcall і __cdecl . Хоча виклики stdcall і cdecl відрізняються варіантами вилучення параметрів з стека, але виконуючого середовища MQL5 дозволяє безболісно використовувати обидва варіанти за рахунок спеціального врапперов DLL викликів.

За замовчуванням в налаштуваннях компілятора С ++ для функцій використовується __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);

Після складання проекту DLL ця stdcall функція буде видна в таблиці експорту під ім'ям _fnCalculateSpeed @ 8, де компілятором додаються знак підкреслення і кількість переданих через стек даних в байтах. Таке декорування дозволяє краще контролювати безпеку викликів DLL функцій за рахунок того, що викликає сторона точно знає, скільки (але не будь!) Даних потрібно поміщати в стек.

Якщо при описі імпорту DLL функції буде помилка в підсумковому розмірі блоку параметрів, то функція не викликана, а в журналі з'явиться повідомлення виду 'Can not find' fnCrashTestParametersStdCall 'in' MQL5DLLSamples.dll ''. В цьому випадку треба ретельно перевірити ще раз всі параметри як в протопите функції, так і в самій DLL.

При відсутності повного імені функції в таблиці експорту для сумісності використовується пошук спрощеного опису без декорування. Такі імена виду fnCalculateSpeed ​​створюються при описах функції в форматі __cdecl:

_DLLAPI int fnCalculateSpeed (int & res1, double & res2) {return (0); }

3. Способи передачі параметрів і обмін даними

Давайте подивимося на кілька варіантів переданих параметрів:

  1. Прийом і передача простих змінних
    З простими змінними все просто - їх можна передавати за значенням або за посиланням через &.
    _DLLAPI int __stdcall fnCalculateSpeed ​​(int & res1, double & res2) {int res_int = 0; double res_double = 0,0; int start = GetTickCount (); for (int i = 0; i <= 10000000; i ++) {res_int + = i * i; res_int ++; res_double + = i * i; res_double ++; } Res1 = res_int; res2 = res_double; return (GetTickCount () -start); } Виклик з MQL5:
    #import "MQL5DLLSamples.dll" int fnCalculateSpeed ​​(int & res1, double & res2); #import int speed = 0; int res_int = 0; double res_double = 0,0; speed = fnCalculateSpeed ​​(res_int, res_double); Print ( "Time", speed, "msec, int:", res_int, "double:", res_double); результат:
    MQL5DLL Test (GBPUSD, M1) 19: 56: 42 Time 16 msec, int: - 752584127 double: 17247836076609
  2. Прийом і передача масиву із заповненням елементів

    Передача масиву в DLL, на відміну від інших 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
  3. Передача і модифікація рядків
    Рядки (unicode) також передаються через прямі посилання на робочі буфери без службової інформації. Зверніть увагу, що функція для прикладу описана в форматі cdecl:
    _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 Виявилося, що рядок не змінилася! Це звичайна помилка початківців програмістів, коли вони передають копії об'єктів (а string - це об'єкт) замість посилання на них. Для рядка '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 автоматично захищається обгорткою Unhandled Exception. Цей механізм дозволяє уберегтися від більшості стандартних помилок (звернення в недоступну пам'ять, ділення на нуль і т.д.)

Для перевірки працездатності цього механізму створимо наступний код:

_DLLAPI void __stdcall fnCrashTest (int * arr) {* arr = 0; }

і викличемо його з терміналу:

#import "MQL5DLLSamples.dll" void fnCrashTest (int arr); #import fnCrashTest (NULL); Print ( "Цього тексту не побачите!");

В результаті відбудеться спроба запису в нульовий адресу з генерацією винятку. Термінал його перехопить, повідомить в журналі і продовжить роботу:

MQL5DLL Test (GBPUSD, M1) 20: 31: 12 Access violation write to 0x00000000


5. врапперов DLL викликів і втрати швидкості на виклики

Як вже було розказано вище, для забезпечення безпеки кожен виклик DLL функції обертається в спеціальний врапперов. Ця обв'язка маскує основний код, підміняє стек, підтримує stdcall / cdecl угоди і контролює виключення всередині викликаються функцій.

Такий обсяг виконуваної роботи не призводить до істотного уповільнення виконання функцій.


6. Фінальна збірка

Зберіть всі вищенаведені приклади DLL функцій в файлі 'MQL5DLLSamples.cpp', а MQL5 приклади в скрипт 'MQL5DLL Test.mq5'. Готовий проект для Visual Studio 2008 і скрипт на MQL5 прикладені до статті.

#include "stdafx.h" _DLLAPI int __stdcall fnCalculateSpeed ​​(int & res1, double & res2) {int res_int = 0; double res_double = 0,0; int start = GetTickCount (); for (int i = 0; i <= 10000000; i ++) {res_int + = i * i; res_int ++; res_double + = i * i; res_double ++; } Res1 = res_int; res2 = res_double; return (GetTickCount () -start); } _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; } _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)); } _DLLAPI void __stdcall fnCrashTest (int * arr) {* arr = 0; } #Property copyright "2010 MetaQuotes Software Corp." #property link "https://www.mql5.com" #property version "1.00" #import "MQL5DLLSamples.dll" int fnCalculateSpeed ​​(int & res1, double & res2); void fnFillArray (int & arr [], int arr_size); void fnReplaceString (string text, string from, string to); void fnCrashTest (int arr); #import void OnStart () {int speed = 0; int res_int = 0; double res_double = 0,0; speed = fnCalculateSpeed ​​(res_int, res_double); Print ( "Time", speed, "msec, int:", res_int, "double:", res_double); 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); string text = "A quick brown fox jumps over the lazy dog"; fnReplaceString (text, "fox", "cat"); Print ( "Replace:", text); fnCrashTest (NULL); Print ( "Цього тексту не побачите!"); }

Дякую за увагу! Буду радий відповісти на питання.