Skip to content

Instantly share code, notes, and snippets.

@zamabuvaraeu
Created June 18, 2026 16:16
Show Gist options
  • Select an option

  • Save zamabuvaraeu/616d72785edb80895297a8c7020871d8 to your computer and use it in GitHub Desktop.

Select an option

Save zamabuvaraeu/616d72785edb80895297a8c7020871d8 to your computer and use it in GitHub Desktop.
Реентерабельность

Реентерабельность

Оконная процедура должна быть реентерабельной. Разберу вопрос подробно: что такое реентерабельность WndProc, как её нарушить и как этого избежать.

Что такое реентерабельность 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)

Почему этот код нарушает реентерабельность

Глобальная переменная g_currentHwnd

Если рекурсивный вызов WndProc произойдёт для другого окна, g_currentHwnd будет перезаписан, и после возврата из рекурсии исходное окно потеряет корректную ссылку.

Рекурсивный вызов через SendMessage

SendMessage синхронно вызывает WndProc для того же окна. Пока первый вызов не завершится, второй будет «зависшим» на проверке g_isProcessing. Это создаёт риск:

  • взаимоблокировки (deadlock), если второй вызов тоже пытается что‐то сделать;
  • потери состояния, если логика зависит от глобальных переменных.

Долгая операция (Sleep)

Задержка в 2 секунды блокирует обработку сообщений. Если за это время придут другие сообщения (например, от системы), они будут либо потеряны, либо обработаны некорректно.

Отсутствие защиты от вложенных вызовов

Код предполагает, что WndProc выполняется только один раз за раз. Реальная Windows не гарантирует этого.

Как это проявляется на практике

Сценарий сбоя:

Пользователь кликает мышью → WM_LBUTTONDOWN.

WndProc вызывает SendMessage(WM_USER).

SendMessage немедленно вызывает тот же WndProc рекурсивно.

Рекурсивный вызов видит g_isProcessing == TRUE, показывает MessageBox и возвращает 0.

Исходный вызов продолжает выполнение, сбрасывает g_isProcessing = FALSE, но операция WM_USER+1 фактически не выполнена.

Состояние программы становится несогласованным.

Правильный подход: реентерабельный WndProc

Чтобы сделать 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).

Обрабатывайте все сообщения, даже если они пришли в неожиданном порядке.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment