среда, 14 октября 2015 г.

Об использовании функции FormatMessage из WinAPI для расшифровки своих кодов ошибок

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


Обозначенный механизм получения строкового сообщения об ошибках можно использовать в любых приложениях или программных библиотеках. Каждый EXE и DLL может содержать свой собственный набор ресурсов, определяющий локализованные варианты сообщений об ошибках. В своём программном коде можно использовать системные коды ошибок, уже определённые в составе WinAPI, в дополнение к своим кодам (дабы повторно не определять то, что уже и так имеется в системе). Для этого при вызове функции FormatMessage используется комбинация флагов FORMAT_MESSAGE_FROM_SYSTEM и FORMAT_MESSAGE_FROM_HMODULE (см. ниже комментарии в коде). Эта комбинация сообщает, что если информация об указанном коде ошибки не будет найдена в нашем модуле (DLL или EXE), то следует выполнить повторный поиск, но уже в системных ресурсах операционной системы. Т.о. в своих ресурсах можно ограничиться определением лишь того, что отсутствует в системных ресурсах. Коды системных ошибок определены в заголовке WinError.h.

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

Формат описания текстовых файлов, содержащих локализованные сообщения об ошибках опубликован в MSDN здесь. Пример такого файла так же присутствует в MSDN. На основе сформированного текстового файла, при помощи утилиты MC.EXE генерируется набор файлов, необходимых для встраивания (в наш EXE или DLL файл) ресурсов, содержащих локализованные сообщения об ошибках.

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

Примечание
Порядок поиска локализованных ресурсов достаточно ясно изложен в описании параметра dwLanguageId функции FormatMessage.

В каталоге проекта создаём новый текстовый файл в кодировке ANSI или Unicode (т.е. UTF-16, не путать с UTF-8). Если используется Notepad++, то кодировка Unicode там обозначена как UCS-2 LE BOM.

Заполняем файл содержимым, например:

; // ***** Sample.mc *****
; // This is the header section.

MessageIdTypedef=DWORD

SeverityNames=(Success=0x0:STATUS_SEVERITY_SUCCESS
Informational=0x1:STATUS_SEVERITY_INFORMATIONAL
Warning=0x2:STATUS_SEVERITY_WARNING
Error=0x3:STATUS_SEVERITY_ERROR
)

FacilityNames=(System=0x0:FACILITY_SYSTEM
Runtime=0x2:FACILITY_RUNTIME
Stubs=0x3:FACILITY_STUBS
Io=0x4:FACILITY_IO_ERROR_CODE
)

LanguageNames=(English=0x409:MSG00409)
LanguageNames=(Russian=0x419:MSG00419)

; // The following are message definitions.

MessageId=0x1
Severity=Error
Facility=Runtime
SymbolicName=MSG_BAD_COMMAND
Language=English
You have chosen an incorrect command.
.

Language=Russian
Вы выбрали неправильную команду.
.

MessageId=0x2
Severity=Warning
Facility=Io
SymbolicName=MSG_BAD_PARM1
Language=English
Cannot reconnect to the server.
.

Language=Russian
Не удаётся подключиться к серверу.
.

MessageId=0x3
Severity=Success
Facility=System
SymbolicName=MSG_STRIKE_ANY_KEY
Language=English
Press any key to continue . . . %0
.

Language=Russian
Нажмите любую клавишу для продолжения . . . %0
.

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

В меню Все Программы -> Visual Studio 2013 -> Visual Studio Tools открываем консоль Visual Studio: VS2013 x64 Cross Tools Command Prompt. Переходим в каталог проекта и запускаем программу MC.EXE с соответствующим набором опций:

  • Если содержимое текстового файла использует кодировку ANSI:
    mc.exe -a sample.mc
  • Если содержимое текстового файла использует кодировку UTF-16:
    mc.exe -u sample.mc
В результате работы обозначенной выше команды, в каталоге проекта появятся новые файлы:
  • MSG00409.bin
  • MSG00419.bin
  • Sample.h
  • Sample.rc
Добавляем в наш проект заголовочный файл Sample.h и ресурсный файл Sample.rc. Сгенерированный утилитой MC.EXE заголовочный файл выглядит следующим образом:

 // ***** Sample.mc *****
 // This is the header section.
 // The following are message definitions.
//
//  Values are 32 bit values laid out as follows:
//
//   3 3 2 2 2 2 2 2 2 2 2 2 1 1 1 1 1 1 1 1 1 1
//   1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0
//  +---+-+-+-----------------------+-------------------------------+
//  |Sev|C|R|     Facility          |               Code            |
//  +---+-+-+-----------------------+-------------------------------+
//
//  where
//
//      Sev - is the severity code
//
//          00 - Success
//          01 - Informational
//          10 - Warning
//          11 - Error
//
//      C - is the Customer code flag
//
//      R - is a reserved bit
//
//      Facility - is the facility code
//
//      Code - is the facility's status code
//
//
// Define the facility codes
//
#define FACILITY_SYSTEM                  0x0
#define FACILITY_STUBS                   0x3
#define FACILITY_RUNTIME                 0x2
#define FACILITY_IO_ERROR_CODE           0x4
 
 
//
// Define the severity codes
//
#define STATUS_SEVERITY_WARNING          0x2
#define STATUS_SEVERITY_SUCCESS          0x0
#define STATUS_SEVERITY_INFORMATIONAL    0x1
#define STATUS_SEVERITY_ERROR            0x3
 
 
//
// MessageId: MSG_BAD_COMMAND
//
// MessageText:
//
// You have chosen an incorrect command.
//
#define MSG_BAD_COMMAND                  ((DWORD)0xC0020001L)
 
//
// MessageId: MSG_BAD_PARM1
//
// MessageText:
//
// Cannot reconnect to the server.
//
#define MSG_BAD_PARM1                    ((DWORD)0x80040002L)
 
//
// MessageId: MSG_STRIKE_ANY_KEY
//
// MessageText:
//
// Press any key to continue . . . %0
//
#define MSG_STRIKE_ANY_KEY               ((DWORD)0x00000003L)
 

Подключаем заголовочный файл Sample.h в коде исходников и используем его. В обозначенном ниже примере мы определяем функцию isValidCommandIndex, использующую "родной" механизм WinAPI для оповещения о возникновении ошибок:

#include<Windows.h>
#include <iostream>
#include "Sample.h"
using namespace std;
/*
MSDN resources:
 
FormatMessage function:
https://msdn.microsoft.com/en-us/library/windows/desktop/ms679351%28v=vs.85%29.aspx
 
Message Text Files:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd996906%28v=vs.85%29.aspx
 
Sample Message Text File:
https://msdn.microsoft.com/en-us/library/windows/desktop/dd996907%28v=vs.85%29.aspx
 
Message Compiler (MC.exe):
https://msdn.microsoft.com/en-us/library/windows/desktop/aa385638%28v=vs.85%29.aspx
 
*/
 
// Our function uses the WinAPI mechanism for notifying of error
BOOL isValidCommandIndex(int index){
  if (index < 0){
    SetLastError(MSG_BAD_COMMAND);  // the identifier from the Sample.h
    return FALSE;
  }
  else{
    SetLastError(ERROR_SUCCESS);
    return TRUE;
  }
}
 
int wmain(int argc, TCHAR* argv[])
{
  // Getting the readable Cyrillic chars in the console window for ANSI and 
  // Unicode encodings...
  setlocale(LC_ALL, "Russian");
  
  int command_index = -1;
  BOOL result = isValidCommandIndex(command_index);
  if (!result){    
    DWORD errCode = GetLastError(); // Get the last error at once
 
    LCID langId = MAKELANGID(LANG_NEUTRAL, SUBLANG_NEUTRAL);
    // Also you can try such variants, for getting the messages with other 
    // localizations:
    // LCID langId = MAKELANGID(LANG_ENGLISH, SUBLANG_ENGLISH_US);
    // LCID langId = MAKELANGID(LANG_RUSSIAN, SUBLANG_RUSSIAN_RUSSIA);
 
    PTCHAR message = NULL;
 
    // HANDLE of EXE or DLL which contains the resource with the error messages
    // which we are to get. In our case this is current EXE, therefore it is
    // possible to point the NULL value of HANDLE for the FormatMessage 
    // function.
    HANDLE handle = NULL;
 
    int tchars_count = FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
      FORMAT_MESSAGE_FROM_SYSTEM | /* Search in the system resources if the
                                   message will not be found in our module. It
                                   allows to use the system error codes in our
                                   code additionally to defined by us error
                                   codes. */
     FORMAT_MESSAGE_FROM_HMODULE |
     FORMAT_MESSAGE_IGNORE_INSERTS, handle, errCode, langId, (PTCHAR)&message,
     0, NULL);
    if (0 == tchars_count){
      // In the Watch[1-4] window it is possible to get the last error code 
      // through the '$err,hr' (without quotes) name. Add this name into the 
      // Watch[1-4] window if you need.
      DWORD lastErrorCode = GetLastError();
      if (lastErrorCode != ERROR_SUCCESS){
        FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER |
          FORMAT_MESSAGE_FROM_SYSTEM |
          FORMAT_MESSAGE_IGNORE_INSERTS, NULL, lastErrorCode, langId,
          (PTCHAR)&message, 0, NULL);
      }
    }
    wcout << message;
    LocalFree(message);
  }  
  // *******************************************
  wchar_t c;
  wcout << L"Нажмите любую клавишу для выхода..." << endl;
  wcin >> c;
  return 0;
}

Компилируем наш код и запускаем его. В консоли получаем следующий вывод:

Вы выбрали неправильную команду.
Нажмите любую клавишу для выхода...

Если указать английскую локализацию (см. комментарии в коде), то сообщение об ошибке будет на английском:

You have chosen an incorrect command.
Нажмите любую клавишу для выхода...

Как видим, текст сообщения об ошибке соответствует тому, который мы определили в составе ресурсов нашего EXE файла.

Где это может пригодиться?
Например, если созданные вами функции могут использоваться сторонними разработчиками, имеющими опыт работы сWinAPI, то задействование стандартного механизма оповещения об ошибках, предоставляемого операционной системой Windows, для них будет делом привычным. Вам достаточно будет лишь сообщить в документации о том, что для получения кода ошибки и его расшифровки следует использовать стандартный механизм WinAPI: функции GetLastError и FormatMessage.

2 комментария:

ЗлостныйГАД комментирует...

День добрый! Заинтересовала ваша статья Хабре (http://habrahabr.ru/post/94231/). А вот исходники недоступны. Можете помочь??

Andrey Bushman комментирует...

Публикация 5-ти летней давности. Исходники у меня не сохранились. Ссылка на них не открывается, видимо я занимался уборкой и в т.ч. удалил и исходники этого проекта.