Файловые операции через WinAPI

MetaQuotes | 3 июля, 2008

Введение

Язык MQL4 спроектирован таким образом, чтобы даже неправильно написанные программы не могли случайно удалить данные с жесткого диска компьютера. Функции операций чтения и записи в файлы могут работать только в следующих директориях(цитирую):

  • /HISTORY/<текущий брокер> - специально для функции FileOpenHistory;
  • /EXPERTS/FILES - общий случай;
  • /TESTER/FILES - специально для тестирования.

Работа с файлами из других каталогов пресекается.

Если все же вам необходимо работать вне заданных (из соображений безопасности) папок, то вы можете обратиться к функциям операционной системы Windows. Для этого широко используются функции API, представленные в библиотеке kernel32.dll.


Файловые функции библиотеки kernel32.dll

За основу был взят скрипт, выложенный в ветке Файловые операции в MQL4 без ограничений. Это хороший пример импорта функций в программу на MQL4.

// константы для функции _lopen
#define OF_READ               0
#define OF_WRITE              1
#define OF_READWRITE          2
#define OF_SHARE_COMPAT       3
#define OF_SHARE_DENY_NONE    4
#define OF_SHARE_DENY_READ    5
#define OF_SHARE_DENY_WRITE   6
#define OF_SHARE_EXCLUSIVE    7
 
 
#import "kernel32.dll"
   int _lopen  (string path, int of);
   int _lcreat (string path, int attrib);
   int _llseek (int handle, int offset, int origin);
   int _lread  (int handle, string buffer, int bytes);
   int _lwrite (int handle, string buffer, int bytes);
   int _lclose (int handle);
#import

Эти функции объявлены в msdn как устаревшие, но использовать их можно, см. Obsolete Windows Programming Elements. Описание функций и параметров я приведу прямо из той ветки, как это сделал автор скрипта - mandor:

// _lopen  : Откpывает указанный файл. Возвpащает: описатель файла.
// _lcreat : Создает указанный файл.   Возвpащает: описатель файла.
// _llseek : Устанавливает указатель в откpытом файле. Возвpащает: 
// новое смещение указателя.
// _lread  : Считывает из откpытого файла указанное число байт. 
// Возвpащает: число считанных байт; 0 - если конец файла.
// _lwrite : Записывает данные из буфеpа в указанный файл. Возвpащает: 
// число записанных байт.
// _lclose : Закpывает указанный файл. Возвpащает: 0.
// В случае неуспешного завеpшения все функции возвращают значение 
// HFILE_ERROR=-1.
 
// path   : Стpока, опpеделяющая путь и имя файла.
// of     : Способ открытия.
// attrib : 0 - чтение или запись; 1 - только чтение; 2 - невидимый или 
// 3 - системный.
// handle : Файловый описатель.
// offset : Число байт, на котоpое пеpемещается указатель.
// origin : Указывает начальную точку и напpавление пеpемещения: 0 - 
// впеpед от начала; 1 - с текущей позиции; 2 - назад от конца файла.
// buffer : Пpинимающий/записываемый буфеp.
// bytes  : Число считываемых байт.
 
// Способы открытия (параметр of):
// int OF_READ            =0; // Открыть файл только для чтения
// int OF_WRITE           =1; // Открыть файл только для записи
// int OF_READWRITE       =2; // Открыть файл в режиме запись/чтение
// int OF_SHARE_COMPAT    =3; // Открывает файл в режиме общего 
// совместного доступа. В этом режиме любой процесс может открыть данный 
// файл любое количество раз. При попытке открыть этот файл в любом другом
// режиме, функция возвращает HFILE_ERROR.
// int OF_SHARE_DENY_NONE =4; // Открывает файл в режиме общего доступа 
// без запрета на чтение/запись другим процессам. При попытке открытия 
// данного файла в режиме OF_SHARE_COMPAT, функция возвращает HFILE_ERROR.
// int OF_SHARE_DENY_READ =5; // Открывает файл в режиме общего доступа с 
// запретом на чтение другим процессам. При попытке открытия данного файла 
// с флагами OF_SHARE_COMPAT и/или OF_READ или OF_READWRITE, функция 
// возвращает HFILE_ERROR.
// int OF_SHARE_DENY_WRITE=6; // Тоже самое, только с запретом на запись.
// int OF_SHARE_EXCLUSIVE =7; // Запрет текущему и другим процессам на 
// доступ к этому файлу в режимах чтения/записи. Файл в этом режиме можно 
// открыть только один раз (текущим процессом). Все остальные попытки 
// открытия файла будут провалены.

Функция чтения из файла

Рассмотрим предложенную фукнцию для чтения из файла. Есдинственным ее параметром является переменная типа string, в которой содержится имя файла. Импортированная функция _lopen(path,0) возвращает указатель на открытый файл, и по назначению очень похожа на функцию FileOpen() из MQL4.

//+------------------------------------------------------------------+
//|   прочитать файл и вернуть строку с содержимым                   |
//+------------------------------------------------------------------+
string ReadFile (string path) 
  {
    int handle=_lopen (path,OF_READ);           
    if(handle<0) 
      {
        Print("Ошибка открытия файла ",path); 
        return ("");
      }
    int result=_llseek (handle,0,0);      
    if(result<0) 
      {
        Print("Ошибка установки указателя" ); 
        return ("");
      }
    string buffer="";
    string char1="x";
    int count=0;
    result=_lread (handle,char1,1);
    while(result>0) 
      {
        buffer=buffer+char1;
        char1="x";
        count++;
        result=_lread (handle,char1,1);
     }
    result=_lclose (handle);              
    if(result<0)  
      Print("Ошибка закрытия файла ",path);
    return (buffer);
  }

Функция _lseek() также имеет аналог в MQL4 - это FileSeek(). Для закрытия файла служит функция _lclose, которая используется также, как и FileClose(). Единственной новой функцией является _lread(handle, buffer, bytes), которая читает из указанного файла (указатель на который должен быть предварительно получен функцией _lopen() ) в переменную buffer указанное количество байт. В качестве переменой buffer необходимо использовать строковую константу необходимой длины. В данном пример мы видим:

    string char1="x";
    result=_lread (handle,char1,1);

-

указана строковая константа char, которая имеет единичную длину, то есть позволяет считать в нее только один байт. Причем значение этой константы не влияет, это может быть не "х", а "Z" или даже " "(знак пробела). Вы не сможете считать в нее большее количество байт, чем было распределено для этой константы изначально. В данном случае, попытки считать 2 или более байт успехом не увенчаются. Кроме того, результатом функции _lread() является количество реально считанных байтов. Если файл имеет длину в 20 байт, а вы попытаетесь считать в переменную длиной 30 байт более 20 байт, что функция вернет значение 20. Если применять эту функцию последовательно, то мы будем двигаться по файлу, читая один блок байтов за другим. Например, файл имеет размер 22 байта, мы начинаем его считывать блоками по 10 байт, тогда после двух вызовов функции __lread(handle, buff, 10) останутся непрочитанными два байта в конце файла.


При третьем вызове __lread(handle, buff, 10) вернет значение 2, то есть, прочитанными будут последние 2 байта. При четвертом вызове получим нулевое значение - ни один байт не прочитан, указатель находится в конце файла. Именно на этом простроена процедура считывания символов из файла в цикле:

    while(result>0) 
      {
        buffer=buffer+char1;
        char1="x";
        count++;
        result=_lread (handle,char1,1);
     }

До тех пор, пока result (количество прочитанных байтов) больше нуля, происходит циклический вызов функции _lread(handle, char1, 1). Как видите, ничего сложного в этих функциях нет. Значение считанного символа сохраняется в переменной char1 и на следующей итерации этот символ дописывается с строковой переменной buffer. По окончании работы пользовательская функция ReadFile() возвращает содержимое прочитанного файла в этой переменной. Как видите, ничего сложного в этом нет.


Функция записи в файл

Запись в некотором смысле даже проще, чем чтение. Необходимо открыть файл и записать в него байтовый массив функцией _lwrite (int handle, string buffer, int bytes). Здесь handle - файловый указатель, полученный функцией _lopen(), параметр buffer - строковая переменная, параметр bytes указывает сколько байт необходимо записать. После записи файл закрывается функцией _lclose(). Рассмотрим авторскую функцию WriteFile():

//+------------------------------------------------------------------+
//|  записать содержимое буфера по указанному пути                   |
//+------------------------------------------------------------------+
void WriteFile (string path, string buffer) 
  {
    int count=StringLen (buffer); 
    int result;
    int handle=_lopen (path,OF_WRITE);
    if(handle<0) 
      {
        handle=_lcreat (path,0);
        if(handle<0) 
          {
            Print ("Ошибка создания файла ",path);
            return;
          }
        result=_lclose (handle);
     }
    handle=_lopen (path,OF_WRITE);               
    if(handle<0) 
      {
        Print("Ошибка открытия файла ",path); 
        return;
      }
    result=_llseek (handle,0,0);          
    if(result<0) 
      {
        Print("Ошибка установки указателя"); 
        return;
      }
    result=_lwrite (handle,buffer,count); 
    if(result<0)  
        Print("Ошибка записи в файл ",path," ",count," байт");
    result=_lclose (handle);              
    if(result<0)  
        Print("Ошибка закрытия файла ",path);
  }

Правда, требуется проводить проверку на ошибки. Сначала делается попытка открыть файл на запись:

    int handle=_lopen (path,OF_WRITE);

(функция _lopen() вызывается с параметром OF_WRITE).

Если попытка оказалась неудачной (handle < 0), то делается попытка создать файл с заданным именем:

        handle=_lcreat (path,0);

Если же и эта функция вернет отрицательный указатель, то происходит досрочное завершение функции WriteFile(). Остальной код в этой функции понятен без объяснений. Простейшая функция start() позволяет проверить работу скрипта File_Read_Write.mq4.

//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
    string buffer=ReadFile("C:\\Text.txt");
    int count=StringLen(buffer);
    Print("Прочитано байт:",count);
    WriteFile("C:\\Text2.txt",buffer);   
//----
   return(0);
  }
//+------------------------------------------------------------------+

Обратите внимание, что символ обратной косой черты "\" (back slash) написан дважды, хотя на самом деле должен быть один. Дело в том, что некоторые спецсимволы, такие символ перевода строки ("\n") или символ табуляции ("\t") пишутся с использованием обратного слеша, . Если вы забудете об этом, то при написании пути в тестовой переменной у вас будут проблемы во время исполнения программы.

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


Все работает, только есть одна ложка дегтя - для больших файлов скрипт функция ReadFile() работает слишком долго.


Причина столь медленного считывания файла кроется в том, что мы считываем информацию по одному байту (символу). На рисунке видно, что файл размером 280 324 байта потребовал 103 секунды. Столько времени потребовалось чтобы 280 324 раза считать по одному символу. Вы можете самостоятельно проверить время работы скрипта File Read Write.mq4, который приложен к статье. Как мы можем ускорить время чтения из файла? Ответ напрашивается сам собой - нужно считывать не по одному символу, а, например, по 50 символов за раз. Тогда количество обращений к функции _lread() сократиться в 50 раз. Соответственно, и время чтения должно сократиться в 50 раз. Проверим это.

Новая функция чтения файла блоками по 50 байт.

Изменяем код, назовем новый вариант как xFiles,mq4. Компилируем и запускаем его на исполнение. Тут необходимо напомнить, что в настройках (Ctrl+O) необходимо разрешить импорт функций из DLL.



Итак, время выполнения модифицированного скрипта xFiles.mq4 составило 2047 миллисекунд, что составляет примерно 2 секунды. Делим 103 секунды (время исполнения первоначального скрипта) на 2 секунды и получаем 103 / 2 = 51.5 раза. Таким образом, время исполнения программы действительно изменилось примерно в 50 раз, как и ожидалось. Как же был модифицирован код для этого?

Изменения небольшие:

string ReadFile (string path) 
  {
    int handle=_lopen (path,OF_READ);
    int read_size = 50;           
    string char50="x                                                 ";
 
    if(handle<0) 
      {
        Print("Ошибка открытия файла ",path); 
        return ("");
      }
    int result=_llseek (handle,0,0);      
    if(result<0) 
      {
        Print("Ошибка установки указателя" ); 
        return ("");
      }
    string buffer="";
    int count=0;
    int last;
    
    result=_lread (handle,char50,read_size);
    int readen;
    while(result>0 && result == read_size) 
      {
        buffer=buffer + char50;
        count++;
        result=_lread (handle,char50,read_size);
        last = result;
     }
    Print("Последний прочитанный блок имеет размер в байтах:", last);
    char50 = StringSubstr(char50,0,last);
    buffer = buffer + char50;    
    result=_lclose (handle);              
    if(result<0)  
      Print("Ошибка закрытия файла ",path);
    return (buffer);
  }

Обратите внимание, что строковая переменная char50 теперь иницилизированна константой в 50 символов (символ "x" и 49 пробелов).


Теперь мы можем производить чтение из файла таким образом, чтобы считывать в эту переменную 50 байт(символов) за один раз:

result=_lread (handle,char50,read_size);

Здесь read_size = 50. Разумеется, что размер считываемого файла очень редко будет кратен 50 байтам, а значит в какой-то момент результатом выполнения этой функции будет значение, отличное от 50-ти. Это является сигналом для прекращения цикла и последний прочитанный в переменную блок символов усекается до размера реально прочитанного количества байтов.


Вы можете изменить размер буфера для считывания с помощью функции lread() до размера N, только не забудьте сделать две модификации:

  1. установите значение read_size = N;
  2. инициализируйте строковую переменную char50 константой длины N(N<256).

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

Функция создания папок

Функция для создания папок также доступна в библиотеке kernel32.dll - CreateDirectory Function. Необходимо только отметить, что эта функция делает попытку создания только папки самого нижнего уровня, она не создает все промежуточные папки в пути в случае их отсутствия. Например, если мы попробуем с помощью этой функции создать папку "С:\folder_A\folder_B", то попытка будет успешна в том случае, если путь "С:/folder_A" уже существует до вызова функции. В противном случае папка folder_B создана не будет. Добавим в секцию импорта новую функцию:

#import "kernel32.dll"
   int _lopen  (string path, int of);
   int _lcreat (string path, int attrib);
   int _llseek (int handle, int offset, int origin);
   int _lread  (int handle, string buffer, int bytes);
   int _lwrite (int handle, string buffer, int bytes);
   int _lclose (int handle);
   int CreateDirectoryA(string path, int atrr[]);
#import

Первый параметр содержит путь, по которому необходимо создать новую папку, а второй параметр atrr[] служит для указания прав для создаваемой папки и должен иметь тип _SECURITY_ATTRIBUTES. Мы не будем указывать никакой информации для второго параметра, а просто будем передавать пустой массив int. В этом случае создаваемая папка будет наследовать все права от родительской папки. Но прежде чем попытаться применить эту функцию, нам понадобиться произвести такую операцию, как -

Разбор пути на составляющие

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

"С:\folder_A\folder_B\folder_С\folder_D"

здесь папку folder_D назовем папкой четвертого уровня, потому что над ней есть еще три папки вышестоящих уровней. Диск "С:" содержит папку "folder_A", папка "С:\folder_A\" содержит папку "folder_B", папка "С:\folder_A\folder_B\" содержит папку "folder_С"; и так далее. Значит нам необходимо разбить весь путь к файлу на массив вложенных друг в друга файлов. Назовем нужную функцию как ParsePath():

//+------------------------------------------------------------------+
//| разбить путь на массив подпапок                                  |
//+------------------------------------------------------------------+
bool ParsePath(string & folder[], string path)
   {
   bool res = false;
   int k = StringLen(path);
   if (k==0) return(res);
   k--;
 
   Print("Распарсим путь=>", path);
   int folderNumber = 0;
//----
   int i = 0;
   while ( k >= 0 )
      {
      int char = StringGetChar(path, k);
      if ( char == 92) //  обратный слеш "\"
         {
         if (StringGetChar(path, k-1)!= 92)
            {
            folderNumber++;
            ArrayResize(folder,folderNumber);
            folder[folderNumber-1] = StringSubstr(path,0,k);
            Print(folderNumber,":",folder[folderNumber-1]);
            }
         else break;         
         }
      k--;   
      }
   if (folderNumber>0) res = true;   
//----
   return(res);   
   }   
//+------------------------------------------------------------------+

Разделителем между папками служит символ "\", который имеет в кодировке ANSI значение 92. Функция получает в качестве аргумента путь path, и заполняет переданный в нее по ссылке массив folder[] именами найденных путей, начиная с самого нижнего и заканчивая самым верхним. Для нашего примера массив folder будет содержать следующие значения:

folder[0] = "С:\folder_A\folder_B\folder_С\folder_D";
folder[1] = "С:\folder_A\folder_B\folder_С";
folder[2] = "С:\folder_A\folder_B";
folder[3] = "С:\folder_A";
folder[4] = "С:";

Если нам необходимо записать файл с именем С:\folder_A\folder_B\folder_С\folder_D\test.txt", то указанный путь мы можем разбить на имя файла test.txt и струтуру вложенных папок "С:\folder_A\folder_B\folder_С\folder_D", которая и содержит данный файл. При неудачной попытке создания файла по этому пути, в первую очередь необходимо попытаться создать папку самого нижнего уровня "С:\folder_A\folder_B\folder_С\folder_D".

Если попытка создания данной папки не увенчалась успехом, то вероятней всего отсутствует родительская папка "С:\folder_A\folder_B\folder_С". Поэтому мы будем делать создать папки все более высокого уровня, пока не получил сообщение об удачном заверешнии функции CreateDirectoryA(). Вот поэтому нам и понадобилась функция, которая заполняет строковый массив folder[] именами папок в возрастающем порядке. Самый первый нулевой индекс содержит папку самого нижнего уровня, последний индекс массива содержит корневой каталог.

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

//+------------------------------------------------------------------+
//|  создает все необходимые папки и подпапки                        |
//+------------------------------------------------------------------+
bool CreateFullPath(string path)
   {
   bool res = false;
   if (StringLen(path)==0) return(false);
   Print("Создаем путь=>",path);
//----
   string folders[];
   if (!ParsePath(folders, path)) return(false);
   Print("Всего вложенных папок:", ArraySize(folders));
   
   int empty[];
   int i = 0;
   while (CreateDirectoryA(folders[i],empty)==0) i++;
   Print("Создали папку:",folders[i]);
   i--;
   while (i>=0) 
      {
      CreateDirectoryA(folders[i],empty);
      Print("Создали папку:",folders[i]);
      i--;
      }
   if (i<0) res = true;   
//----
   return(res);
   }

Теперь осталось только изменить немного функцию WriteFile() с учетом возможности создать новую папку.

//+------------------------------------------------------------------+
//|  записать содержимое буфера по указанному пути                   |
//+------------------------------------------------------------------+
void WriteFile (string path, string buffer) 
  {
    int count=StringLen (buffer); 
    int result;
    int handle=_lopen (path,OF_WRITE);
    if(handle<0) 
      {
        handle=_lcreat (path,0);
        if(handle<0) 
          {
            Print ("Ошибка создания файла ",path);
            if (!CreateFullPath(path))
               {
               Print("Не удалось создать папку:",path);
               return;
               }
            else handle=_lcreat (path,0);   
          }
        result=_lclose (handle);
        handle = -1;
     }
    if (handle < 0) handle=_lopen (path,OF_WRITE);               
    if(handle<0) 
      {
        Print("Ошибка открытия файла ",path); 
        return;
      }
    result=_llseek (handle,0,0);          
    if(result<0) 
      {
        Print("Ошибка установки указателя"); 
        return;
      }
    result=_lwrite (handle,buffer,count); 
    if(result<0)  
        Print("Ошибка записи в файл ",path," ",count," байт");
    result=_lclose (handle);              
    if(result<0)  
        Print("Ошибка закрытия файла ",path);
    return;        
  }

Логика работы измененной функции представлена на рисунке.


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

        result=_lclose (handle);
        handle = -1;

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

    if (handle < 0) handle=_lopen (path,OF_WRITE);

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

Изменим функцию start() для проверки новых возможностей

//+------------------------------------------------------------------+
//| script program start function                                    |
//+------------------------------------------------------------------+
int start()
  {
//----
    int start = GetTickCount();
    string buffer=ReadFile("C:\\Text.txt");
 
    int middle = GetTickCount();
    int count=StringLen(buffer);
 
    Print("Прочитано байт:",count);
 
    WriteFile("C:\\folder_A\\folder_B\\folder_C\\folder_D\\Text2.txt",buffer);   
    int finish = GetTickCount();
    Print("Размер файла - ",count," bytes. Чтение:",(middle-start)," ms. Запись:",(finish-middle)," ms.");
//----
   return(0);
  }
//+------------------------------------------------------------------+

и запустим скрипт xFiles.mq4 на исполнение.



Заключение

Использовать функции WinAPI не так сложно, но не нужно забывать и об обратной стороне выхода за песочницу:

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


Ссылка, которая может быть полезной: Работа с функциями Windows API и DLL - http://www.compress.ru/article.aspx?id=11741&iid=457