Почему на разных компьютерах MessageBox, вызванный через WinAPI, выдает разные ошибки?

Для проекта есть необходимость использовать WinAPI. В процессе изучения столкнулся с методом получения ошибок Marshal.GetLastWin32Error().
Скопировал пример с сайта docs.microsoft.com и добавил в консольное приложение на .Net Core 5.0.

using System;
using System.Runtime.InteropServices;

internal class Win32
{
    // Use DllImportAttribute to inport the Win32 MessageBox
    // function.  Set the SetLastError flag to true to allow
    // the function to set the Win32 error.
    [DllImportAttribute("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern int MessageBox(IntPtr hwnd, String text, String caption, uint type);
}

class Program
{

    static void Run()
    {
        // Call the MessageBox with normal parameters.

        Console.WriteLine("Calling Win32 MessageBox without error...");

        Win32.MessageBox(new IntPtr(0), "Press OK...", "Press OK Dialog", 0);

        // Get the last error and display it.
        int error = Marshal.GetLastWin32Error();

        Console.WriteLine("The last Win32 Error was: " + error);

        // Call the MessageBox with an invalid window handle to
        // produce a Win32 error.

        Console.WriteLine("Calling Win32 MessageBox with error...");

        Win32.MessageBox(new IntPtr(123132), "Press OK...", "Press OK Dialog", 0);

        // Get the last error and display it.

        error = Marshal.GetLastWin32Error();

        Console.WriteLine("The last Win32 Error was: " + error);
    }

    static void Main(string[] args)
    {
        Run();
    }
}
// This code example displays the following to the console:
//
// Calling Win32 MessageBox without error...
// The last Win32 Error was: 0
// Calling Win32 MessageBox with error...
// The last Win32 Error was: 1400

На первом компьютере все работало как должно (Ноутбук одолжил пока не купил собственный).
После покупки своего ноутбука перенес туда проект. Тот же самый код выполняется с ошибкой: всегда после вызова messagebox возвращается ошибка 1400, но само окно появляется и возвращает 1 (IDOK).

Ошибка с WinAPI


  • Первый компьютер:
    • Производитель: MSI GF75 Thin 10UEK;
    • Windows: Windows10 Home x64;
    • Обновление: 20H2 (KB504237);
    • BIOS: E17FSIMS.102
    • Процессор: Intel i7-10750H;
  • Второй компьютер:
    • Производитель: HP ProBook 440 G8;
    • Windows: Windows10 Pro x64;
    • Обновление: 20H2 (KB504237)
    • BIOS: T70 Ver. 01.03.01
    • Процессор: Intel i7-1165G7

Если вызвать GetLastWin32Error до вызова messagebox - возвращается ошибка 0.
Может кто-то сталкивался с подобным? Проблема не особо серьезная, т.к. можно проверять что возвращает окно, но хотелось бы понять что происходит.


Ответы (1 шт):

Автор решения: aepot

TL;DR

Может кто-то сталкивался с подобным? Проблема не особо серьезная, т.к. можно проверять что возвращает окно, но хотелось бы понять что происходит.

Нужно сначала проверять, что вернул метод, и только потом если результат согласно документации содержит значение соответствующее признаку ошибки, вызывать GetLastError. Именно в таком порядке обрабатываются ошибки Win32 API. Во всех остальных случаях результат вызова GetLastError нельзя считать валидным.


Смысл GetLastError в том, что он возвращает код последней ошибки, возникшей в текущем потоке. Почему у вас после вызова метода статус ошибки сохраняется - одному только майкрософту известно. Вообще должен быть 0, и мне не удалось воспроизвести вашу проблему.

Но ответ мой в другом. Обрабатывать ошибку Win32 стоит только тогда, когда метод, вызванный с атрибутом DllImportAttribute.SetLastError == true вернул статус, соответствующий ошибке.

Если взять конкретный MessageBox, то согласно документации.

If the function fails, the return value is zero. To get extended error information, call GetLastError.

Ошибку стоит обрабатывать только тогда, когда он вернул 0.

Как можно правильно обработать ошибку Win32

Вся сложность этой истории заключается в том, что Win32 API писалась разными людьми в разных версиях Windows в разное время. И стандарты написания кода, несмотря на очень серьезный к ним подход у Microsoft, были разные. К тому же, разные методы возвращают разные типы данных.

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

Решение разделено на 2 части.

Часть 1. Атрибут, хранящий статус - индикатор ошибки

[AttributeUsage(AttributeTargets.Method, Inherited = false, AllowMultiple = false)]
sealed class Win32ErrorStatusAttribute : Attribute
{
    public object ErrorStatus { get; }

    public Win32ErrorStatusAttribute(object errorStatus)
       => ErrorStatus = errorStatus;
}

Здесь все просто, берем что-то переданное в конструктор и кладем в свойство ErrorStatus.

Использовать атрибут можно вот так

internal class Win32
{
    [Win32ErrorStatus(0)]
    [DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
    public static extern int MessageBox(IntPtr hwnd, string text, string caption, uint type);
}

То есть будем бросать исключение, если метод вернул 0.

Часть 2. Обработка статуса и выброс исключения

Метод, который принимает выражение, преобразуемое в делегат, читает значение из атрибута, сравнивает возвращаемый статус с тем, что лежит в атрибуте. Если совпадает, то бросает Win32Exception.

public static T ThrowIfWin32Error<T>(Expression<Func<T>> exp) where T : unmanaged
{
    Win32ErrorStatusAttribute att = (exp.Body as MethodCallExpression).Method.GetCustomAttribute<Win32ErrorStatusAttribute>();
    Func<T> func = exp.Compile();
    T result = func();
    if (att == null || typeof(T) != att.ErrorStatus.GetType() || !result.Equals(att.ErrorStatus))
        return result;
    throw new Win32Exception(Marshal.GetLastWin32Error());
}

Использование решения

enum MessageBoxResult // https://docs.microsoft.com/en-us/windows/win32/api/winuser/nf-winuser-messagebox#return-value
{
    IDABORT = 3,     // The Abort button was selected.
    IDCANCEL = 2,    // The Cancel button was selected.
    IDCONTINUE = 11, // The Continue button was selected.
    IDIGNORE = 5,    // The Ignore button was selected.
    IDNO = 7,        // The No button was selected.
    IDOK = 1,        // The OK button was selected.
    IDRETRY = 4,     // The Retry button was selected.
    IDTRYAGAIN = 10, // The Try Again button was selected.
    IDYES = 6        // The Yes button was selected. 
}

static void Main(string[] args)
{
    try
    {
        Console.WriteLine("Calling Win32 MessageBox without error...");
        int result = ThrowIfWin32Error(() => Win32.MessageBox(new IntPtr(0), "Press OK...", "Press OK Dialog", 0));
        Console.WriteLine((MessageBoxResult)result);
        Console.WriteLine("Calling Win32 MessageBox with error...");
        result = ThrowIfWin32Error(() => Win32.MessageBox(new IntPtr(123132), "Press OK...", "Press OK Dialog", 0));
        Console.WriteLine((MessageBoxResult)result);
    }
    catch (Win32Exception ex)
    {
        Console.WriteLine($"[{ex.NativeErrorCode}] {ex.Message}");
    }

    Console.ReadKey();
}

Вывод в консоль

Calling Win32 MessageBox without error...
IDOK
Calling Win32 MessageBox with error...
[1400] Invalid window handle.

То есть в результате получаем поведение, похожее на родное для .NET - выброс исключения в случае ошибки.

Само собой здесь могут быть недоработки или я что-то не учёл. Сильно вдоль и поперек я код не тестировал. Просто хотел объяснить, как можно обрабатывать ошибки Windows не загромождая код.

→ Ссылка