Подключение библиотек и #import функций

Импорт функций осуществляется из откомпилированных модулей MQL5 (файлы *.ex5) и из модулей динамических библиотек Windows (файлы *.dll). Имя модуля указывается в директиве #import, после чего следуют описания прототипов импортируемых функций. Такой блок должен заканчиваться еще одной директивой #import, причем она может быть без имени и просто закрывать собой блок, либо в директиве может быть указано имя другой библиотеки, и тем самым одновременно начинается следующей блок импорта. В конце серии блоков импорта всегда должна идти директива без имени библиотеки.

В простейшем случае директива выглядит следующим образом:

#import "[путь⌡ имя_модуля [.расширение]"
   тип_функции имя_функции([список_параметров]); 
   [тип_функции имя_функции(⌠список_параметров]);]
   ... 
#import

Имя файла библиотеки можно указывать без расширения: тогда по умолчанию предполагается dll-библиотека. Расширение ex5 указывать обязательно.

Перед именем может идти путь размещения библиотеки. По умолчанию, если пути нет, библиотеки ищутся в папке MQL5/Libraries или в папке рядом с MQL-программой, куда подключена библиотека. В противном случае для поиска библиотек применяются разные правила в зависимости от типа — DLL или EX5. Эти правила освещены в отдельном разделе.

Вот пример последовательных блоков импорта из двух библиотек:

#import "user32.dll"
   int     MessageBoxW(int hWndstring szTextstring szCaptionint nType); 
   int     SendMessageW(int hWndint Msgint wParamint lParam); 
#import "lib.ex5" 
   double  round(double value); 
#import

При наличии таких директив импортируемые функции можно вызывать из исходного кода точно так же, как и функции, определенные непосредственно в самой MQL-программе. Все технические "сложности" с загрузкой библиотек и переадресации вызовов в сторонние модули берет на себя среда исполнения MQL-программ.

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

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

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

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

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

Например:

#import "kernel32.dll"
   int GetLastError();
#import "lib.ex5" 
   int GetLastError();
#import
  
class Foo
{
public
   int GetLastError() { return(12345); }
   void func() 
   { 
      Print(GetLastError());           // вызов метода класса 
      Print(::GetLastError());         // вызов встроенной (глобальной) функции MQL5 
      Print(kernel32::GetLastError()); // вызов функции из kernel32.dll 
      Print(lib::GetLastError());      // вызов функции из lib.ex5 
   }
};
   
void OnStart()
{
   Foo foo
   foo.func(); 
}

Покажем простой пример скрипта LibRandTest.mq5, использующего функции из EX5-библиотеки, созданной в предыдущем разделе.

#include <MQL5Book/LibRand.mqh>

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

input int N = 10000;
input double Mean = 0.0;
input double Sigma = 1.0;
input double HistogramStep = 0.5;
input int RandomSeed = 0;

Инициализация встроенного в MQL5 генератора случайных чисел (равномерного распределения) производится значением RandomSeed или, если здесь оставлен 0, берется GetTickCount (новое при каждом запуске).

Для построения гистограммы используем MapArray и QuickSortStructT (мы с ними уже работали в разделах о мультивалютных индикаторах и о сортировке массивов, соответственно). В карте будут накапливаться счетчики попадания случайных чисел в ячейки гистограммы с шагом HistogramStep.

#include <MQL5Book/MapArray.mqh>
#include <MQL5Book/QuickSortStructT.mqh>

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

#define COMMA ,
   
template<typename K,typename V>
class MyMapArraypublic MapArray<K,V>
{
public:
   void sort()
   {
      SORT_STRUCT(Pair<K COMMA V>, arraykey);
   }
};

Обратите внимание, что макрос COMMA становится альтернативным представлением символа запятой ',' и используется при вызове другого макроса SORT_STRUCT. Если бы не эта подстановка, запятая внутри пары Pair<K,V> трактовалась бы препроцессором, как обычный разделитель параметров макроса, в результате чего на входе SORT_STRUCT получилось бы 4 параметра вместо ожидаемых 3-х — это вызвало бы ошибку компиляции. Препроцессор ничего не знает о синтаксисе MQL5.

В начале OnStart, после инициализации генератора, проверим получение одиночной случайной строки и массива строк разной длины.

void OnStart()
{
   const uint seed = RandomSeed ? RandomSeed : GetTickCount();
   Print("Random seed: "seed);
   MathSrand(seed);
   
   // вызываем 2 библиотечных функции: StringPatternDigit и RandomString
   Print("Random HEX-string: "RandomString(30StringPatternDigit() + "ABCDEF"));
   Print("Random strings:");
   string text[];
   RandomStrings(text51020);         // 5 строк длиной от 10 до 20 символов
   ArrayPrint(text);
   ...

Далее тестируем нормально-распределенные случайные числа.

   // вызываем еще одну библиотечную функцию: PseudoNormalArray
   double x[];
   PseudoNormalArray(xNMeanSigma);   // заполнили массив x
   
   Print("Random pseudo-gaussian histogram: ");
   
   // берем 'long' как тип ключей, т.к. 'int' уже использован для доступа по индексу
   MyMapArray<long,intmap;
   
   for(int i = 0i < N; ++i)
   {
      // величина x[i] определяет ячейку гистограммы, где увеличиваем статистику
      map.inc((long)MathRound(x[i] / HistogramStep));
   }
   map.sort();                             // сортируем по ключу (т.е. по значению)
   
   int max = 0;                            // ищем максимум для нормировки
   for(int i = 0i < map.getSize(); ++i)
   {
      max = fmax(maxmap.getValue(i));
   }
   
   const double scale = fmax(max / 801); // в гистограмме максимум 80 символов
   
   for(int i = 0i < map.getSize(); ++i)  // печатаем гистограмму
   {
      const int p = (int)MathRound(map.getValue(i) / scale);
      string filler;
      StringInit(fillerp, '*');
      Print(StringFormat("%+.2f (%4d)",
         map.getKey(i) * HistogramStepmap.getValue(i)), " "filler);
   }

Вот какой результат получился при запуске с настройками по умолчанию (рандомизация таймером — каждый запуск будет выбирать новый seed).

Random seed: 8859858

Random HEX-string: E58B125BCCDA67ABAB2F1C6D6EC677

Random strings:

"K4ZOpdIy5yxq4ble2" "NxTrVRl6q5j3Hr2FY" "6qxRdDzjp3WNA8xV"  "UlOPYinnGd36"      "6OCmde6rvErGB3wG" 

Random pseudo-gaussian histogram: 

-9.50 (   2) 

-8.50 (   1) 

-8.00 (   1) 

-7.00 (   1) 

-6.50 (   5) 

-6.00 (  10) *

-5.50 (  10) *

-5.00 (  24) *

-4.50 (  28) **

-4.00 (  50) ***

-3.50 ( 100) ******

-3.00 ( 195) ***********

-2.50 ( 272) ***************

-2.00 ( 510) ****************************

-1.50 ( 751) ******************************************

-1.00 (1029) *********************************************************

-0.50 (1288) ************************************************************************

+0.00 (1457) *********************************************************************************

+0.50 (1263) **********************************************************************

+1.00 (1060) ***********************************************************

+1.50 ( 772) *******************************************

+2.00 ( 480) ***************************

+2.50 ( 280) ****************

+3.00 ( 172) **********

+3.50 ( 112) ******

+4.00 (  52) ***

+4.50 (  43) **

+5.00 (  10) *

+5.50 (   8) 

+6.00 (   8) 

+6.50 (   2) 

+7.00 (   3) 

+7.50 (   1)

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

При тестировании экспертов и индикаторов в тестере следует иметь в виду важный момент, связанный с библиотеками. Библиотеки, необходимые для основной тестируемой MQL-программы определяются автоматически из директив #import. Однако если из основной программы вызывается пользовательский индикатор, к которому подключена какая-либо библиотека, то необходимо в явном виде указать в свойствах программы, что она опосредованно зависит от конкретной библиотеки. Это делается с помощью директивы:
 
#property tester_library "путь_имя_библиотеки.расширение"