English 中文 Español Deutsch 日本語 Português 한국어 Français Italiano Türkçe
Избавляемся от балласта самодельных DLL

Избавляемся от балласта самодельных DLL

MetaTrader 5Примеры | 31 января 2012, 11:33
8 604 18
---
---


Вы все еще делаете свои DLL?
Тогда мы идем к вам!

Введение

Всегда наступает момент, когда MQL5-программисту недостаточно функционала языка, которым он располагает, и он вынужден обращаться к дополнительным инструментам. Например, работать с базой данных или использовать сокеты для связи, или работать с функциями операционной системы. Чтобы расширить возможности своей MQL5-программы ему приходится обращаться к различным API. Но по некоторым причинам программист не может напрямую из MQL5 обращаться к требуемым функциям, так как он не знает:

  • Как передать сложный тип данных (например, структуру) в API-функцию;
  • Как работать с указателем, который возвращает API-функция.

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

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


Содержание

1. Память - это наше все

  • Получение указателей
  • Копирование областей памяти

2. Передача структур в API функции

  • Преобразование структур средствами MQL5
  • Пример передачи структуры для сокетов

3. Работа с указателями от API функций

  • Примеры для Memory Mapping File,
  • Пример для MySQL

4. Чтение NULL-terminated строк из API функций



1. Память - это наше все

Как известно, любая переменная (в том числе и переменные сложных типов данных) имеет вполне конкретный адрес, с которого эта переменная располагается в памяти. Этот адрес является целым четырехбайтовым числом (типа int), значение которого есть адрес первого байта этой переменной.

А раз все вполне определено, значит можно работать с этим участком памяти. В библиотеке языка С (msvcrt.dll) есть функция memcpy. Ее предназначение является тем недостающим элементом, который связывает в одно целое MQL5 и различные API библиотеки, и которая открывает широкие возможности для программиста.


Обратимся к знанию предков

Функция memcpy копирует указанное число байт из одного буфера в другой и возвращает указатель на буфер-приемник.

void *memcpy(void *dst, const void *src, int cnt);
dst - указатель на буфер-приемник
src - указатель на буфер-источник
cnt - число байт для копирования

Другими словами – участок памяти размером cnt байт, начиная с адреса src, копируется в участок памяти, начиная с адреса dst.

Данные, которые располагаются по адресу src, могут быть самыми разными. Это может быть однобайтовая char переменная, восьмибайтовое double число, массив, любая структура, и вообще, любой объем памяти. То есть, зная адреса и размер, вы можете свободно выполнять перенос данных из одной области памяти в другую.

Как это работает

На схеме 1 показаны сравнительные размеры некоторых типов данных.

Размеры некоторых типов данных в MQL5


Назначение функции memcpy – копирование данных из одного участка памяти в другой.
На схеме 2 показан пример копирования четырех байт.

Пример копирования 4 байт при помощи функции memcpy

На языке MQL5 это будет выглядеть следующим образом.

Пример 1. Использование memcpy
#import "msvcrt.dll"
  int memcpy(int &dst, int &src, int cnt);
#import
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  int dst, src=4, cnt=sizeof(int);
  int adr=memcpy(dst, src, cnt);
  Print("Значение dst="+string(dst)+"   Адрес dst="+string(adr));
}

Важно понимать, что в качестве области памяти, на которые указывают dst и src, могут находиться абсолютно разные типы данных (главное одинакового размера cnt). Например, указатель src может ссылаться на double переменную (cnt=8 байт) а dst – на массив аналогичного размера char[8] или int[2].

Для памяти все равно, какое представление о ней имеет программист в данный момент. Будь это массив char[8] или просто одна long переменная или структура { int a1; int a2; }.

Это значит, что между собой можно копировать не только данные одного типа, но и разных типов. Например, пятибайтовый массив перегонять в структуру {int i; char c;} или обратно структуру в массив. Именно эта связь открывает возможность для прямой работы с API функциями.

Рассмотрим по порядку варианты использования memcpy.


Получение указателей

В примере 1 мы показали, что функция memcpy возвращает адрес переменной dst.

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

Например, получим адрес double переменной и short массива:

Пример 2. Получение указателей на переменную
#import "msvcrt.dll"
  int memcpy(short &dst[], short &src[], int cnt);
  int memcpy(double &dst,  double &src, int cnt);
#import

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  short src[5];
  //--- получим адрес массива src (т.е. адрес его первого элемента)
  int adr=memcpy(src, src, 0);
  double var;
  //--- получим адрес переменной var
  adr=memcpy(var, var, 0); 
}

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


Копирование массивов

Как вы знаете, массив - это выделенный кусок в памяти. Размер выделенной памяти зависит от типа элементов и от их числа. Например, если тип элементов массива short и число этих элементов 10, то такой массив занимает в памяти 20 байт (так как размер short 2 байта).

Но эти 20 байт также представляются массивами из 20 char или из 5 int. В любом случае они занимают в памяти все те же 20 байт.

Для копирования массивов необходимо:

  • Выделить для памяти dst требуемое число элементов (не менее чем результирующие cnt байт);
  • Задать в cnt число байт, которое необходимо скопировать из src.
Пример 3. Копирование массивов
#import "msvcrt.dll"
  int memcpy(double &dst[],  double &src[], int cnt);
#import

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  double src[5];
  //--- считаем число байт!!!
  int cnt=sizeof(double)*ArraySize(src);
  double dst[]; 
  ArrayResize(dst, 5);
  //--- скопировали массив из src в dst
   memcpy(dst, src, cnt); 
}



2. Передача структур в API функции

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

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

На схеме 3 показано представление структуры из 5 переменных разных типов и ее аналог в виде char массива.

Представление структуры из 5 переменных разных видов и ее аналог в виде массива char[]

Пример 4. Копирование структур средствами MQL5
struct str1
{
  double d; // 8 байт
  long l;   // 8 байт
  int i[3]; // 3*4=12 байт
};
struct str2
{
  uchar c[8+8+12]; // размер структуры str1
};
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  str1 src; 
  src.d=-1;
  src.l=20;
  //--- заполняем параметры структуры
  ArrayInitialize(src.i, 0); 
  str2 dst;
  //--- превратили структуру в байтовый массив
  dst=src; 
}

Таким вот нехитрым способом мы скопировали структуру в байтовый массив.

Чтобы пример получился более жизненным, рассмотрим функцию создания сокета.

int connect(SOCKET s, const struct sockaddr *name, int namelen);

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

1. Запишем функцию connect для импорта допустимым в MQL5 способом:

int connect(int s, uchar &name[], int namelen);

2. Смотрим на требуемую структуру в документации:

struct sockaddr_in
{
  short   sin_family;
  u_short sin_port;
  in_addr sin_addr; // дополнительная 8 байтовая структура
  char sin_zero[8];
};

3. Создаем структуру с массивом аналогичного размера:

struct ref_sockaddr_in
{
  uchar c[2+2+8+8];
};

4. После заполнения требуемой sockaddr_in структуры переводим ее в байтовый массив и передаем его в качестве параметра connect.

Ниже приведен участок кода, сделанный по этим пунктам.

Пример 5. Обращение клиентского сокета к серверу
#import "Ws2_32.dll"
  ushort htons(ushort hostshort);
  ulong inet_addr(char &cp[]);
  int connect(int s, char &name[], int namelen);
#import
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  //--- после иницализации сокета выполняем соединение с хостом

  char ch[];
  StringToCharArray("127.0.0.1", ch);
  //--- подготавливаем структуру
  sockaddr_in addrin;
  addrin.sin_family=AF_INET;
  addrin.sin_addr=inet_addr(ch);
  addrin.sin_port=htons(1000);
  //--- копируем структуру в массив
  ref_sockaddr_in ref=addrin; 
  //--- соединяемся с хостом
  res=connect(asock, ref.c, sizeof(addrin)); 

  //--- дальнейшая работа с сокетом
}

Как видите, для работы с сокетами абсолютно не нужно делать свою DLL. Структуры напрямую передаются в API.


3. Работа с указателями от API функций

Зачастую API функции возвращают указатель на данные: на структуры или массивы. Средствами MQL5 вытянуть эти данные не представляется возможным, здесь на помощь приходит функция memcpy.

Пример работы с массивами памяти от Memory Mapping File (MMF)



При работе с MMF используется функция, которая возвращает указатель на выделенный массив памяти.

int MapViewOfFile(int hFile, int DesiredAccess, int OffsetHigh, int OffsetLow, int NumOfBytesToMap);

Чтение данных из этого массив производится простым копированием требуемого числа байт функцией memcpy.
Запись данных в массив производится аналогичным использованием memcpy.

Пример 6. Запись и чтение данных из памяти MMF
#import "kernel32.dll"
  int OpenFileMappingW(int dwDesiredAccess, int bInheritHandle,  string lpName);
  int MapViewOfFile(int hFileMappingObject, int dwDesiredAccess, 
                      int dwFileOffsetHigh, int dwFileOffsetLow, int dwNumberOfBytesToMap);
  int UnmapViewOfFile(int lpBaseAddress);
  int CloseHandle(int hObject);
#import "msvcrt.dll"
  int memcpy(uchar &Destination[], int Source, int Length);
  int memcpy(int Destination, int &Source, int Length);
  int memcpy(int Destination, uchar &Source[], int Length);
#import

#define FILE_MAP_ALL_ACCESS   0x000F001F

//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  //--- открываем объект памяти
  int hmem=OpenFileMappingW(FILE_MAP_ALL_ACCESS, 0, "Local\\file");
  //--- получаем указатель на память
  int view=MapViewOfFile(hmem, FILE_MAP_ALL_ACCESS, 0, 0, 0); 
  //--- читаем первые 10 байт из памяти
  uchar src[10];
  memcpy(src, view, 10);
  int num=10;
  //--- записываем в начало памяти 4 байтовое int число
  memcpy(view, num, 4);
  //--- закрываем просмотр
  UnmapViewOfFile(view); 
  //--- закрываем объект
  CloseHandle(hmem); 
}

Как видите, сложностей по работе с указателями на массив памяти нет. И главное - для этого не надо создавать свою дополнительную DLL.




Пример работы с возвращаемыми структурами для MySQL

Одной из наболевших проблем при работе с MySQL была получение из нее данных. Функция mysql_fetch_row возвращает массив строк. Каждая строка это массив полей. Получается, что данная функция возвращает указатель на указатель. Наша задача - вытянуть все эти данные из возвращаемого указателя.

Задача немного осложнена тем, что поля являются различными типами данных, в том числе и бинарными. Это значит, что в виде string массива их представить не получится. Для получения информации о строках и размерах полей имеются функции mysql_num_rows, mysql_num_fields, mysql_fetch_lengths.

На схеме 4 показана структура представления результата в памяти.
Адреса начала трех строк собираются в массив. И адрес начала этого массива (в примере = 94) и есть то, что вернет функция mysql_fetch_row.

Структура представления результата запроса в памяти

Ниже показан пример кода для получения данных из запроса к базе.

Пример 7. Получение данных из MySQL
#import "libmysql.dll"
  int mysql_real_query(int mysql, uchar &query[], int length);
  int mysql_store_result(int mysql);
  int mysql_field_count(int mysql);
  uint mysql_num_rows(int result);
  int mysql_num_fields(int result);
  int mysql_fetch_lengths(int result);
  int mysql_fetch_row(int result);
#import 
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  //--- ... предварительно проинициализированная база данных mysql
  //--- запрос на получение всех строк из таблицы table
  string query="SELECT * FROM table"; 
  uchar aquery[]; 
  StringToCharArray(query, aquery);

  //--- отправляем запрос
  err=mysql_real_query(mysql, aquery, StringLen(query)); 
  int result=mysql_store_result(mysql);

  //--- если содержит строки
  if (result>0) 
  {
    ulong num_rows=mysql_num_rows(result);
    int num_fields=mysql_num_fields(result);    

    //--- получим указатель первую строку
    int r=0, row_ptr=mysql_fetch_row(result);
    while(row_ptr>0)
    {

       //--- получим указатель на длины столбцов текущей строки
      int len_ptr=mysql_fetch_lengths(result); 
      int lens[]; 
       ArrayResize(lens, num_fields);
      //--- получим размеры полей строки
      memcpy(lens, len_ptr, num_fields*sizeof(int));
      //--- получаем поля данных   
      int field_ptr[];
      ArrayResize(field_ptr, num_fields);
      ArrayInitialize(field_ptr, 0);

      //--- получим указатели на поля
      memcpy(field_ptr, row_ptr, num_fields*sizeof(int)); 
      for (int f=0; f<num_fields; f++)
      {
        ArrayResize(byte, lens[f]);
        ArrayInitialize(byte, 0);
         //--- скопируем поле в байтовый массив byte
        if (field_ptr[f]>0 && lens[f]>0) memcpy(byte, field_ptr[f], lens[f]);
      }
      r++;
      //--- получим указатель на указатель на следующую строку
      row_ptr=mysql_fetch_row(result); 
    }
  }
}



4. Чтение NULL-terminated строк из API функций

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

Представление NULL-terminated строки в памяти

В библиотеке C (msvcrt.dll) уже есть функция, которая копирует из указателя на NULL-terminated строку ее содержимое в другую строку. При этом размер исходной строки она определяет сама. В качестве приемника нам лучше использовать байтовый массив, так как API часто возвращают многобайтовые строки, а не юникод.

strcpy - копирует NULL-terminated строки

char *strcpy(char *dst, const char *src);
dst - указатель на строку назначения
src - указатель на Null-terminated строку источник

По сути она является частным случаем функции memcpy. Так как система сама останавливает копирование на найденном нуле в строке. Эту функцию придется использовать всегда именно при работе с такими указателями.

Например, в API от MySQL есть несколько функций, которые возвращают указатели на строки. И получение данных из них c использованием strcpy - тривиальная задача.

Пример 8. Получение строк из указателей
#import "libmysql.dll"
  int mysql_init(int mysql);
  int mysql_real_connect(int mysql, uchar &host[], uchar &user[], uchar &password[], 
                            uchar &DB[], uint port, uchar &socket[], int clientflag);
  int mysql_get_client_info();
  int mysql_get_host_info(int mysql);
  int mysql_get_server_info(int mysql);
  int mysql_character_set_name(int mysql);
  int mysql_stat(int mysql);
#import "msvcrt.dll"
  int strcpy(uchar &dst[], int src);
#import
//+------------------------------------------------------------------+
//| Script program start function                                    |
//+------------------------------------------------------------------+
void OnStart()
{
  uchar byte[];
  ArrayResize(byte, 300);

  int ptr;
  string st;
  //--- указатель на строку
  ptr=mysql_get_client_info();

  if (ptr>0) strcpy(byte, ptr);
  Print("client_info="+CharArrayToString(byte));
  //--- инициализируем базу
  int mysql=mysql_init(mysql);

  //--- переводим строки в байтовые массивы
  uchar ahost[]; 
  StringToCharArray("localhost", ahost);
  uchar auser[];
  StringToCharArray("root", auser);
  uchar apwd[];
  StringToCharArray("", apwd);
  uchar adb[];
  StringToCharArray("some_db", adb);
  uchar asocket[];
  StringToCharArray("", asocket);
  //--- соединяемся с базой
  int rez=mysql_real_connect(mysql, ahost, auser, apwd, adb, port, asocket, 0);
  //--- узнаем состояние соединения и базы
  ptr=mysql_get_host_info(mysql);
  if (ptr>0) strcpy(byte, ptr);
  Print("mysql_host_info="+CharArrayToString(byte));
  ptr=mysql_get_server_info(mysql);
  if (ptr>0) strcpy(byte, ptr);
  Print("mysql_server_info="+CharArrayToString(byte));
  ptr=mysql_character_set_name(mysql);
  if (ptr>0) strcpy(byte, ptr);
  Print("mysql_character_set_name="+CharArrayToString(byte));
  ptr=mysql_stat(mysql);
  if (ptr>0) strcpy(byte, ptr);
  Print("mysql_stat="+CharArrayToString(byte));
}


Заключение

Таким образом, использование трех базовых механизмов по работе с памятью – копирование структур, получение указателей и их данных по memcpy и получение строк по strcpy покрывает практически все задачи работы с различными API-функциями.

Предупреждение. Работа с memcpy и strcpy является небезопасной, если для буфера-приемника не выделен достаточный объем данных. Поэтому будьте внимательны к размеру выделяемых объемов для приема данных.


Последние комментарии | Перейти к обсуждению на форуме трейдеров (18)
Yrii Kuksov
Yrii Kuksov | 21 июл. 2018 в 16:27
_SERG_:


uchar по ходу неправильно,  и там double присутствует тоже подозрительно.

И по ходу src надо указать что именно ей принадлежит и будет передаваться.

Вам уже кто-то подправил так же как и предположил. Хорошо. Думайте. Удачи.

Seric29
Seric29 | 30 мар. 2020 в 20:04
а для mql4 будет работать на сегодняшний день?
Igor Makanu
Igor Makanu | 30 мар. 2020 в 20:13
Seric29:
а для mql4 будет работать на сегодняшний день?

языки (MQL4 / MQL5)  сейчас полностью одинаковы - отличие в 2-3  функциях отсутствующих в MQL4 (ArrayPrint и что то еще по мелочи) и в "новых плюшках" для MQL5 - БД, DirectX и OpenCL


но статья написана 8 лет назад, сейчас MQL стал строго типизированным языком, и чтобы присвоить 2 структуры (пример в статье) нужно писать самостоятельно конструктор копирования или сериализовать структуру в массив байт и затем обратно

Dmitri Custurov
Dmitri Custurov | 4 сент. 2022 в 20:37
Интересно, можно ли получить реальный указатель на функцию. Указатели, полученные с помощью typedef, отлично работают внутри mql программы. Но к сожалению передать в dll мне их не удалось.
Vladimir Simakov
Vladimir Simakov | 8 сент. 2022 в 17:51
Dmitri Custurov #:
Интересно, можно ли получить реальный указатель на функцию. Указатели, полученные с помощью typedef, отлично работают внутри mql программы. Но к сожалению передать в dll мне их не удалось.

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

MQL5 Cloud Network ускоряет расчеты MQL5 Cloud Network ускоряет расчеты
Сколько ядер на вашем домашнем компьютере? И сколько компьютеров вы можете задействовать для оптимизации торговой стратегии? Мы покажем как с помощью MQL5 Cloud Network ускорить расчеты и получить для этого вычислительные мощности по всему миру одним щелчком мыши. Выражение "Время - деньги" становится актуальнее с каждым годом, и не всегда мы можем позволить себе ждать окончания важных расчетов в течение десятков часов или даже дней.
Интервью с Александром Арашкевичем (ATC 2011) Интервью с Александром Арашкевичем (ATC 2011)
Наконец улеглись страсти, мы можем перевести дух и начинать переосмысливать еще раз его результаты. И у нас есть еще один победитель Александр Арашкевич (AAA777) из Белоруссии, который получил специальный приз от Главного спонсора Automated Trading Championship 2011 - бесплатную поездку на соревнование Формулы-1 в 2012 году. Мы не могли упустить такой возможности пообщаться с ним.
Защита MQL5-программ: пароли, ключи, ограничение по времени, удаленная проверка лицензий Защита MQL5-программ: пароли, ключи, ограничение по времени, удаленная проверка лицензий
Большинство разработчиков нуждаются в защите своих кодов. В этой статье представлены несколько различных способов защиты MQL5-программ - методы обеспечения лицензирования скриптов, советников и индикаторов. Рассмотрена парольная защита, генераторы ключей, привязка к торговым счетам, ограничение по времени и удаленная проверка лицензий при помощи MQL5-RPC.
Трейдминатор 3: восстание торговых роботов Трейдминатор 3: восстание торговых роботов
В статье "Доктор Трейдлав..." мы остановились на том, что создали эксперт, оптимизирующий самостоятельно параметры заранее выбранной торговой системы. Было предложено создать эксперт, который не только оптимизирует параметры одной торговой системы, заложенной в основу эксперта, но делает выбор из нескольких торговых систем. Посмотрим же, что из этого может получится...