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

--- | 31 января, 2012


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

Введение

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

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

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


Содержание

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

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

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

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 байт.

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

Пример 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 является небезопасной, если для буфера-приемника не выделен достаточный объем данных. Поэтому будьте внимательны к размеру выделяемых объемов для приема данных.