Оконная процедура должна быть реентерабельной. Разберу вопрос подробно: что такое реентерабельность WndProc, как её нарушить и как этого избежать.
Реентерабельность (reentrancy) — способность функции безопасно выполняться одновременно из нескольких потоков и вложенных вызовов. Оконная процедура WndProc должна быть реентерабельной, потому что:
- Windows может отправить новое сообщение до завершения обработки предыдущего;
- вызовы
SendMessageилиPostMessageвнутриWndProcмогут привести к рекурсивному вызову той же процедуры; - системные диалоги или меню могут временно перехватывать фокус и генерировать сообщения.
Вот код, который нарушает реентерабельность:
#include once "windows.bi"
' Глобальные переменные — источник проблем
Dim Shared Variable As Integer
Dim Shared g_currentHwnd As HWND
Private Function MainFormWndProc( _
ByVal hWin As HWND, _
ByVal wMsg As UINT, _
ByVal wParam As WPARAM, _
ByVal lParam As LPARAM _
) As LRESULT
' Глобальная переменная для окна
g_currentHwnd = hwnd
Select Case wMsg
Case WM_LBUTTONDOWN
' устанавливаем нашу переменную как нам надо
Variable = 20
' Вызываем какую-нибудь функцию
SendMessage(hWin, WM_USER, 0, 0)
' В это время ядро Windows
' вызывает нашу оконную процедуру
' call WndProc(hWin, WM_USER, ...)
If Variable <> 20 Then
' Ой! наша переменная Variable
' изменилась между вызовами!
MessageBox( _
hWin, _
__TEXT("Переменная изменилась между вызовами"), _
NULL, _
MB_OK Or MB_ICONINFORMATION _
)
End If
Case WM_USER
' Имитация долгой операции
Sleep_(2000)
' Обрабатываем что‐то, используя глобальные данные
If g_currentHwnd Then
SetWindowText(g_currentHwnd, __TEXT("Обработано!"))
End If
Variable = 100
' После обработки этого сообщения
' ядро вернёт управление в кусок кода
' If Variable <> 20 Then
Case WM_DESTROY
PostQuitMessage(0)
Case Else
Return DefWindowProc(hWin, wMsg, wParam, lParam)
End Select
Return 0
End Function
Private Function wWinMain( _
Byval hInst As HINSTANCE, _
ByVal hPrevInstance As HINSTANCE, _
ByVal lpCmdLine As LPCWSTR, _
ByVal iCmdShow As Long _
)As Long
Const NineWindowTitle = __TEXT("Window")
Const MainWindowClassName = __TEXT("Window")
Dim wcls As WNDCLASSEX = Any
With wcls
.cbSize = SizeOf(WNDCLASSEX)
.style = CS_HREDRAW Or CS_VREDRAW
.lpfnWndProc = @MainFormWndProc
.cbClsExtra = 0
.cbWndExtra = 0
.hInstance = hInst
.hIcon = NULL
.hCursor = LoadCursor(NULL, IDC_ARROW)
.hbrBackground = Cast(HBRUSH, COLOR_WINDOW + 1)
.lpszMenuName = Cast(TCHAR Ptr, NULL)
.lpszClassName = @MainWindowClassName
.hIconSm = NULL
End With
Dim resRegister As ATOM = RegisterClassEx(@wcls)
If resRegister = 0 Then
Return 1
End If
Dim hWin As HWND = CreateWindowEx( _
WS_EX_OVERLAPPEDWINDOW, _
@MainWindowClassName, _
@NineWindowTitle, _
WS_OVERLAPPEDWINDOW Or WS_CLIPCHILDREN, _
CW_USEDEFAULT, CW_USEDEFAULT, 640, 480, _
NULL, _
NULL, _
hInst, _
NULL _
)
If hWin = NULL Then
Return 1
End If
ShowWindow(hWin, iCmdShow)
UpdateWindow(hWin)
Dim m As MSG = Any
Dim GetMessageResult As Integer = GetMessage(@m, NULL, 0, 0)
Do While GetMessageResult <> 0
If GetMessageResult = -1 Then
Return 1
End If
TranslateMessage(@m)
DispatchMessage(@m)
GetMessageResult = GetMessage(@m, NULL, 0, 0)
Loop
Return m.wParam
End Function
Dim lpCmdLine As WCHAR Ptr = NULL
Dim retCode As Long = wWinMain( _
GetModuleHandle(0), _
NULL, _
lpCmdLine, _
SW_SHOW _
)
End(retCode)Если рекурсивный вызов WndProc произойдёт для другого окна, g_currentHwnd будет перезаписан, и после возврата из рекурсии исходное окно потеряет корректную ссылку.
SendMessage синхронно вызывает WndProc для того же окна. Пока первый вызов не завершится, второй будет «зависшим» на проверке g_isProcessing. Это создаёт риск:
- взаимоблокировки (deadlock), если второй вызов тоже пытается что‐то сделать;
- потери состояния, если логика зависит от глобальных переменных.
Задержка в 2 секунды блокирует обработку сообщений. Если за это время придут другие сообщения (например, от системы), они будут либо потеряны, либо обработаны некорректно.
Код предполагает, что WndProc выполняется только один раз за раз. Реальная Windows не гарантирует этого.
Сценарий сбоя:
Пользователь кликает мышью → WM_LBUTTONDOWN.
WndProc вызывает SendMessage(WM_USER).
SendMessage немедленно вызывает тот же WndProc рекурсивно.
Рекурсивный вызов видит g_isProcessing == TRUE, показывает MessageBox и возвращает 0.
Исходный вызов продолжает выполнение, сбрасывает g_isProcessing = FALSE, но операция WM_USER+1 фактически не выполнена.
Состояние программы становится несогласованным.
Чтобы сделать WndProc реентерабельным, избегайте глобальных переменных и длинных операций. Используйте локальные данные и асинхронные механизмы:
Private Function MainFormWndProc( _
ByVal hWin As HWND, _
ByVal wMsg As UINT, _
ByVal wParam As WPARAM, _
ByVal lParam As LPARAM _
) As LRESULT
Select Case wMsg
Case WM_LBUTTONDOWN
' устанавливаем нашу переменную как нам надо
Variable = 20
' Запускаем асинхронную задачу вместо синхронного SendMessage
PostMessage(hWin, WM_USER, 0, 0)
' Переменная Variable сейчас не изменилась
Case WM_USER
' Выполняем работу без глобальных флагов
SetWindowText(hWin, __TEXT("Обработано!"))
Variable = 100
Case WM_TIMER
' Используем таймер вместо длительной операциии
KillTimer(hWin, 1)
Case WM_DESTROY
PostQuitMessage(0)
Case Else
Return DefWindowProc(hWin, wMsg, wParam, lParam)
End Select
Return 0
End FunctionНе используйте глобальные флаги для отслеживания состояния обработки.
Заменяйте SendMessage на PostMessage для асинхронных операций.
Избегайте долгих операций в WndProc. Переносите их в фоновые потоки или таймеры.
Храните состояние в локальных переменных или в данных окна (SetWindowLongPtr).
Обрабатывайте все сообщения, даже если они пришли в неожиданном порядке.