¿Cómo debo usar FormatMessage () correctamente en C ++?

90

Sin :

  • MFC
  • ATL

¿Cómo puedo usar FormatMessage()para obtener el texto de error de un HRESULT?

 HRESULT hresult = application.CreateInstance("Excel.Application");

 if (FAILED(hresult))
 {
     // what should i put here to obtain a human-readable
     // description of the error?
     exit (hresult);
 }
Aaron
fuente

Respuestas:

134

Esta es la forma correcta de obtener un mensaje de error del sistema para un HRESULT(llamado hresult en este caso, o puede reemplazarlo por GetLastError()):

LPTSTR errorText = NULL;

FormatMessage(
   // use system message tables to retrieve error text
   FORMAT_MESSAGE_FROM_SYSTEM
   // allocate buffer on local heap for error text
   |FORMAT_MESSAGE_ALLOCATE_BUFFER
   // Important! will fail otherwise, since we're not 
   // (and CANNOT) pass insertion parameters
   |FORMAT_MESSAGE_IGNORE_INSERTS,  
   NULL,    // unused with FORMAT_MESSAGE_FROM_SYSTEM
   hresult,
   MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT),
   (LPTSTR)&errorText,  // output 
   0, // minimum size for output buffer
   NULL);   // arguments - see note 
   
if ( NULL != errorText )
{
   // ... do something with the string `errorText` - log it, display it to the user, etc.

   // release memory allocated by FormatMessage()
   LocalFree(errorText);
   errorText = NULL;
}

La diferencia clave entre esto y la respuesta de David Hanak es el uso de la FORMAT_MESSAGE_IGNORE_INSERTSbandera. MSDN no tiene claro cómo se deben usar las inserciones, pero Raymond Chen señala que nunca debe usarlas al recuperar un mensaje del sistema, ya que no tiene forma de saber qué inserciones espera el sistema.

FWIW, si está usando Visual C ++, puede hacer su vida un poco más fácil usando la _com_errorclase:

{
   _com_error error(hresult);
   LPCTSTR errorText = error.ErrorMessage();
   
   // do something with the error...

   //automatic cleanup when error goes out of scope
}

No es parte de MFC o ATL directamente que yo sepa.

Shog9
fuente
8
Cuidado: este código usa hResult en lugar de un código de error de Win32: ¡son cosas diferentes! Es posible que obtenga el texto de un error completamente diferente al que realmente ocurrió.
Andrei Belogortseff
1
Excelente punto, @Andrei, y de hecho, incluso si el error es un error de Win32, esta rutina solo tendrá éxito si se trata de un error del sistema : un mecanismo robusto de manejo de errores necesitaría ser consciente de la fuente del error, examinar el código antes de llamar a FormatMessage y quizás consultar otras fuentes en su lugar.
Shog9
1
@AndreiBelogortseff ¿Cómo puedo saber qué usar en cada caso? Por ejemplo, RegCreateKeyExdevuelve a LONG. Sus documentos dicen que puedo usar FormatMessagepara recuperar el error, pero tengo que convertirlo LONGen un archivo HRESULT.
CSL
FormatMessage () toma un DWORD, @csl, un entero sin signo que se supone que es un código de error válido. No todos los valores devueltos, o HRESULTS para el caso, serán códigos de error válidos; el sistema asume que ha verificado que es antes de llamar a la función. Los documentos para RegCreateKeyEx deben especificar cuándo el valor de retorno puede interpretarse como un error ... Realice esa verificación primero y solo luego llame a FormatMessage.
Shog9
1
MSDN en realidad ahora proporciona su versión del mismo código.
ahmd0
14

Tenga en cuenta que no puede hacer lo siguiente:

{
   LPCTSTR errorText = _com_error(hresult).ErrorMessage();

   // do something with the error...

   //automatic cleanup when error goes out of scope
}

A medida que la clase se crea y se destruye en la pila, se deja errorText para apuntar a una ubicación no válida. En la mayoría de los casos, esta ubicación aún contendrá la cadena de error, pero esa probabilidad desaparece rápidamente al escribir aplicaciones con subprocesos.

Por lo tanto, siempre hágalo de la siguiente manera, como respondió Shog9 arriba:

{
   _com_error error(hresult);
   LPCTSTR errorText = error.ErrorMessage();

   // do something with the error...

   //automatic cleanup when error goes out of scope
}
Marius
fuente
7
El _com_errorobjeto se crea en la pila en ambos ejemplos. El término que estás buscando es temporal . En el ejemplo anterior, el objeto es un temporal que se destruye al final de la declaración.
Rob Kennedy
Sí, lo decía en serio. Pero espero que la mayoría de la gente al menos pueda averiguarlo a partir del código. Técnicamente, los temporales no se destruyen al final de la declaración, sino al final del punto de secuencia. (que es lo mismo en este ejemplo, así que esto es solo dividir los pelos.)
Marius
1
Si desea que sea seguro (tal vez no muy eficiente ), puede hacerlo en C ++:std::wstring strErrorText = _com_error(hresult).ErrorMessage();
ahmd0
11

Prueba esto:

void PrintLastError (const char *msg /* = "Error occurred" */) {
        DWORD errCode = GetLastError();
        char *err;
        if (!FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                           NULL,
                           errCode,
                           MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // default language
                           (LPTSTR) &err,
                           0,
                           NULL))
            return;

        static char buffer[1024];
        _snprintf(buffer, sizeof(buffer), "ERROR: %s: %s\n", msg, err);
        OutputDebugString(buffer); // or otherwise log it
        LocalFree(err);
}
David Hanak
fuente
void HandleLastError (hresult)?
Aaron
1
Seguro que tú mismo puedes hacer estas adaptaciones.
oefe
@Atklin: Si desea usar hresult de un parámetro, obviamente no necesita la primera línea (GetLastError ()).
David Hanak
4
GetLastError no devuelve un HResult. Devuelve un código de error Win32. Podría preferir el nombre PrintLastError ya que en realidad no maneja nada. Y asegúrese de utilizar FORMAT_MESSAGE_IGNORE_INSERTS.
Rob Kennedy
Gracias por su ayuda chicos :) - muy apreciado
Aaron
5

Esto es más una adición a la mayoría de las respuestas, pero en lugar de usar LocalFree(errorText)use la HeapFreefunción:

::HeapFree(::GetProcessHeap(), NULL, errorText);

Desde el sitio de MSDN :

Windows 10 :
LocalFree no está en el SDK moderno, por lo que no se puede usar para liberar el búfer de resultados. En su lugar, use HeapFree (GetProcessHeap () ,astedMessage). En este caso, esto es lo mismo que llamar a LocalFree en memoria.

Actualización
que encontré LocalFreeen la versión 10.0.10240.0 del SDK (línea 1108 en WinBase.h). Sin embargo, la advertencia todavía existe en el enlace anterior.

#pragma region Desktop Family or OneCore Family
#if WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP | WINAPI_PARTITION_SYSTEM)

WINBASEAPI
_Success_(return==0)
_Ret_maybenull_
HLOCAL
WINAPI
LocalFree(
    _Frees_ptr_opt_ HLOCAL hMem
    );

#endif /* WINAPI_FAMILY_PARTITION(WINAPI_PARTITION_DESKTOP | WINAPI_PARTITION_SYSTEM) */
#pragma endregion

Actualización 2
También sugeriría usar la FORMAT_MESSAGE_MAX_WIDTH_MASKbandera para arreglar los saltos de línea en los mensajes del sistema.

Desde el sitio de MSDN :

FORMAT_MESSAGE_MAX_WIDTH_MASK
La función ignora los saltos de línea regulares en el texto de definición del mensaje. La función almacena saltos de línea codificados en el texto de definición del mensaje en el búfer de salida. La función no genera nuevos saltos de línea.

Actualización 3
Parece haber 2 códigos de error del sistema en particular que no devuelven el mensaje completo utilizando el enfoque recomendado:

¿Por qué FormatMessage solo crea mensajes parciales para los errores del sistema ERROR_SYSTEM_PROCESS_TERMINATED y ERROR_UNHANDLED_EXCEPTION?

Esqueleto de clase
fuente
4

Aquí hay una versión de la función de David que maneja Unicode

void HandleLastError(const TCHAR *msg /* = "Error occured" */) {
    DWORD errCode = GetLastError();
    TCHAR *err;
    if (!FormatMessage(FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM,
                       NULL,
                       errCode,
                       MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), // default language
                       (LPTSTR) &err,
                       0,
                       NULL))
        return;

    //TRACE("ERROR: %s: %s", msg, err);
    TCHAR buffer[1024];
    _sntprintf_s(buffer, sizeof(buffer), _T("ERROR: %s: %s\n"), msg, err);
    OutputDebugString(buffer);
    LocalFree(err);

}

Oleg Zhylin
fuente
1
Tenga en cuenta que no está pasando el tamaño de búfer correcto _sntprintf_sen el caso de UNICODE. La función toma el número de caracteres, por lo que desea _countofo ARRAYSIZEtambién conocido como en sizeof(buffer) / sizeof(buffer[0])lugar de sizeof.
ThFabba
4

Desde c ++ 11, puede usar la biblioteca estándar en lugar de FormatMessage:

#include <system_error>

std::string message = std::system_category().message(hr)
Crónico
fuente
2

Como se señaló en otras respuestas:

  • FormatMessagetoma un DWORDresultado no un HRESULT(típicamente GetLastError()).
  • LocalFree es necesario para liberar la memoria asignada por FormatMessage

Tomé los puntos anteriores y agregué algunos más para mi respuesta:

  • Envuelva el FormatMessageen una clase para asignar y liberar memoria según sea necesario
  • Use la sobrecarga del operador (por ejemplo, operator LPTSTR() const { return ...; }para que su clase se pueda usar como una cadena
class CFormatMessage
{
public:
    CFormatMessage(DWORD dwMessageId,
                   DWORD dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT)) :
        m_text(NULL)
    {
        Assign(dwMessageId, dwLanguageId);
    }

    ~CFormatMessage()
    {
        Clear();
    }

    void Clear()
    {
        if (m_text)
        {
            LocalFree(m_text);
            m_text = NULL;
        }
    }

    void Assign(DWORD dwMessageId,
                DWORD dwLanguageId = MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT))
    {
        Clear();
        DWORD dwFlags = FORMAT_MESSAGE_FROM_SYSTEM
            | FORMAT_MESSAGE_ALLOCATE_BUFFER
            | FORMAT_MESSAGE_IGNORE_INSERTS,
        FormatMessage(
            dwFlags,
            NULL,
            dwMessageId,
            dwLanguageId,
            (LPTSTR) &m_text,
            0,
            NULL);
    }

    LPTSTR text() const { return m_text; }
    operator LPTSTR() const { return text(); }

protected:
    LPTSTR m_text;

};

Encuentre una versión más completa del código anterior aquí: https://github.com/stephenquan/FormatMessage

Con la clase anterior, el uso es simplemente:

    std::wcout << (LPTSTR) CFormatMessage(GetLastError()) << L"\n";
Stephen Quan
fuente
0

El siguiente código es el equivalente en C ++ que escribí en contraste con ErrorSalir () de Microsoft, pero ligeramente alterado para evitar todas las macros y usar Unicode. La idea aquí es evitar moldes y mallocs innecesarios. No pude escapar de todos los elencos de C, pero esto es lo mejor que pude reunir. Perteneciente a FormatMessageW (), que requiere que la función de formato asigne un puntero y el ID de error de GetLastError (). El puntero después de static_cast se puede utilizar como un puntero wchar_t normal.

#include <string>
#include <windows.h>

void __declspec(noreturn) error_exit(const std::wstring FunctionName)
{
    // Retrieve the system error message for the last-error code
    const DWORD ERROR_ID = GetLastError();
    void* MsgBuffer = nullptr;
    LCID lcid;
    GetLocaleInfoEx(L"en-US", LOCALE_RETURN_NUMBER | LOCALE_ILANGUAGE, (wchar_t*)&lcid, sizeof(lcid));

    //get error message and attach it to Msgbuffer
    FormatMessageW(
        FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM | FORMAT_MESSAGE_IGNORE_INSERTS,
        NULL, ERROR_ID, lcid, (wchar_t*)&MsgBuffer, 0, NULL);
    //concatonate string to DisplayBuffer
    const std::wstring DisplayBuffer = FunctionName + L" failed with error " + std::to_wstring(ERROR_ID) + L": " + static_cast<wchar_t*>(MsgBuffer);

    // Display the error message and exit the process
    MessageBoxExW(NULL, DisplayBuffer.c_str(), L"Error", MB_ICONERROR | MB_OK, static_cast<WORD>(lcid));

    ExitProcess(ERROR_ID);
}

fuente