Mikle писал(а):Честно говоря, аргумент так себе - есть куча других способов получить неконтролируемую рекурсию, программист должен сам за этим следить.
Других способов нет — все другие способы на самом деле именно
другие. Одно дело, когда одна часть кода, написанная программистом, вызывает другую часть кода, написанную программистом. Здесь может возникнуть неконтролируемая рекурсия, и тогда, да, программист должен следить. И совсем другое дело (как здесь), когда кто-то вызывает твою функцию, а не ты сам. Когда часть кода, написанная программистом, вызывается не из другой части его же кода, а из чужого кода, над которым у программиста нет контроля и чёткого понимания.
Во всех таких случаях должны быть определены жёсткие и простые правила игры: в каких случаях твоей код может быть вызван сторонним кодом, и какие твои действия могут спровоцировать сторонний код на вызов твоего кода. И если эти правила будут довольно широкими и мягкими, может получиться, что очень широкий диапазон твоих действий может привести к вызову сторонним кодом твоего кода, который эти действия и совершает. Это путь к неконтролируемой, и, что страшнее, неизбежной рекурсии, причём там, где она никому не нужна.
Mikle писал(а):А диалоги попросту не выполняют DoEvents, некрасиво это, когда даже таймеры во время диалогов останавливаются.
Это не некрасиво, а это очень правильно, и если хорошо подумать, то поймёшь, что по другому нельзя/не стоит делать.
Во-первых, фраза про невызов
DoEvents в корне неверная.
DoEvents это обёртка над
GetMessage+
DispatchMessage. Именно эти два вызова она делает (возможно несколько раз, чтобы обработать все сообщения, скопившиеся в очереди оконных сообщений). Все диалоги именно это и делают в своём цикле, иначе бы UI просто заморозился, потому что оконные сообщения не доставлялись бы оконным процедурам, никакие контролы бы не реагировали на пользовательские действия и ничего бы даже не отрисовывалось и не перерисовывалось.
При этом цикл прокачки оконных сообщений (как внутри MsgBox, так и внутри CommonDialog) никак не фильтрует и не отсеивает сообщения по принципу «если это сообщения перерисовки или таймера родительского окна — мы не будем доставлять его». Ничего подобного там нет, доставляются абсолютно все сообщения.
Это не цикл прокачки сообщений отказывается доставлять «неуместные» сообщения, это оконная процедура самой VB-шной формы (которая получает
все сообщения, из тех, что для посланы соотв. окну) отказывается по поводу полученных сообщений WM_PAINT/WM_TIMER вызывать обработчики событий, если в данный момент идёт показ MsgBox'а/InputBox'а/CommonDialog'а.
И это правильно; и помимо риска неконтролируемой рекурсии есть вещь пострашнее — не по деструктивности, а по неразрешимости.
Представим себе, что на время показа чужеродных модальных диалогов вызов обработчиков событий Paint/Timer не блокировался бы. Представим себе, что в обработчики события есть нечто такое:
If Rnd < 0.13 Then EndТо есть с некоторой вероятностью (13%) при возникновении события вызывается
End.
Вопрос:
как в таком сценарии End должна смочь (чисто технически) разрулить возникшую необходимость в остановке работы проекта? Звучит, наверное, не очень убедительно и не очень проблемо-центрично, потому что у кого-то может возникнуть соблазн сказать: «А в чём вообще проблема? Пусть End сделает это так же, как всегда делает, как во всех остальных случаях».
А теперь я поясню. Что вообще делает End с технической точки зрения? Когда наш VB-проект выполняется, это выполнение является продолжением выполнения самого процесса VB IDE. В нашем коде одни наши процедуры могут вызывать другие наши же процедуры, другие наши же процедуры могут вызывать третьи наши же процедуры — во всех таких случаях растёт стек вызовов, растёт просто стек текущего потока, на нём размещаются какие-то переменные, аргументы вызовов, в этих переменных лежат какие-то данные, указатели на объекты/массивы/строки. А наши процедуры (нами написанные) вызываются из кода VB IDE.
И, к примеру, если в какой-то момент пользователь кликнул на кнопочку на форме, обработчик Command1_Click вызвал Foo, Foo вызвала Bar, Bar вызвала Baaaz, а Baaaz вызвала End, на момент вызова End мы имеем такую цепочку вызовов (это трассировка стека вызовов, прямая, а не обратная):
vb6.exe!WinMain ➥
... ➥
vb6.exe!ThunderMsgLoop (в этой процедуре крутится оконный цикл)
➥
user32.dll!DispatchMessage ➥
vb6.exe!StdCtlWndProc (общая для всех VB-шных оконных классов оконная процедура-обёртка)
➥
vb6.exe!CommonGizWndProc (общая для всех VB-шных контролов оконная процедура-обёртка)
➥
vb6.exe!PushCtlProc (оконная процедура VB-шных кнопкок)
➥
vb6.exe!_DoClick ➥
vb6.exe!EvtErrFire ➥
vb6.exe!EvtErrFireWorker ➥
vb6.exe!InvokeEvent ➥
vb6.exe!InvokeVtblEvent ➥
vb6.exe!CallProcWithArgs ➥
Project1!Form1::Command1_Click ➥
Project1!Module1::Foo ➥
Project1!Module2::Bar ➥
Project1!Module1::BaaazЗдесь синие процедуры — это процедуры написанные на С/С++, скомпилированные в Native-код и выполняемые непосредственно процессором, а красное — это процедуры, написанные на VB, преобразованные в P-код и выполняемые P-кодной виртуальной машиной.
Сама по себе P-кодная виртуальная машина, выполняющая P-код, состоит из Native-кода, выполняемого процессором. Поэтому формально, если рассматривать с точки зрения Native-кода, трассировка стека вызовов выглядела бы так:
vb6.exe!WinMain ➥
... ➥
vb6.exe!ThunderMsgLoop (в этой процедуре крутится оконный цикл)
➥
user32.dll!DispatchMessage ➥
vb6.exe!StdCtlWndProc (общая для всех VB-шных оконных классов оконная процедура-обёртка)
➥
vb6.exe!CommonGizWndProc (общая для всех VB-шных контролов оконная процедура-обёртка)
➥
vb6.exe!PushCtlProc (оконная процедура VB-шных кнопкок)
➥
vb6.exe!_DoClick ➥
vb6.exe!EvtErrFire ➥
vb6.exe!EvtErrFireWorker ➥
vb6.exe!InvokeEvent ➥
vb6.exe!InvokeVtblEvent ➥
vb6.exe!CallProcWithArgs ➥
анонимный_переходничок (сгенерированный VBA6.DLL переходничок-делегат с Native-кода на P-код) ➥
vba6.dll!MethCallEngine (в этой ф-ции работает цикл, исполняющий P-кодные инструкции одну за другой)Именно так бы показали трассировку обычные Native-кодный отладчики, именно таким способом мы бы думали о происходящем, если бы ничего не знали о P-коде, но с логической (смысловой), а не формальной точки зрения, трассировка выглядит (повторю её один в один) именно так:
vb6.exe!WinMain ➥
... ➥
vb6.exe!ThunderMsgLoop (в этой процедуре крутится оконный цикл)
➥
user32.dll!DispatchMessage ➥
vb6.exe!StdCtlWndProc (общая для всех VB-шных оконных классов оконная процедура-обёртка)
➥
vb6.exe!CommonGizWndProc (общая для всех VB-шных контролов оконная процедура-обёртка)
➥
vb6.exe!PushCtlProc (оконная процедура VB-шных кнопкок)
➥
vb6.exe!_DoClick ➥
vb6.exe!EvtErrFire ➥
vb6.exe!EvtErrFireWorker ➥
vb6.exe!InvokeEvent ➥
vb6.exe!InvokeVtblEvent ➥
vb6.exe!CallProcWithArgs ➥
Project1!Form1::Command1_Click ➥
Project1!Module1::Foo ➥
Project1!Module2::Bar ➥
Project1!Module1::BaaazВ норме, без всякого End,
Baaaz выполнится до конца и произойдёт возврат из
Baaaz в
Bar.
Затем
Bar выполнится до конца и произойдёт возврат в
Foo.
Затем
Foo выполнится до конца и произойдёт возврат в
Command1_Click.
Затем
Command1_Click выполнится до конца и произойдёт возврат в
vb6.exe!CallProcWithArgs.
И так далее, и до тех пор, пока мы не вернёмся обратно в цикл прокачки оконных сообщений (внутри
ThunderMsgLoop), следующая итерация которого возьмёт из очереди оконных сообщений следующее сообщение, вызовет для него DispatchMessage и оно будет обработано соответствующим образом.
Цикл прокачки сообщений прекращается только при закрытии IDE, а пока IDE не закрыта, абсолютно всё вызывается из него. И код самой IDE вызывается из него, и код VB-программы тоже вызывается из него (и Sub Main, и обработчики событий, цепочку вызовов для которых я показал выше).
Важно другое: каждая процедура из этой цепочки вложенных вызовов резервирует себе какое-то количество байтов из стека под локальные переменные, создаёт какие-то объекты или выделяет какие-то блоки памяти (под разные нужды), может быть создаёт какие-то окна, может быть захватывает какие-то ресурсы (например входит в критическую секцию с помощью EnterCriticalSection) и указатели на созданные объекты или выделенные блоки памяти, хендлы созданных окон сохраняет в свои локальные переменные (ну или в глобальные), затем делает какую-то работу (в том числе вызов других процедур), а в конце (перед выходом) созданные объекты уничтожаются, блоки памяти высвобождаются, окна уничтожаются, ресурсы отпускаются, и в конце концов область стека, отведённая под локальные переменные освобождается. При выходе из процедуры из стека выкидываются помещённые туда перед её вызовом аргументы (если они были). И это
САМЫЙ ВАЖНЫЙ момент, и о нём речь пойдёт в дальнейшем.
Так вот, в ситуации, когда у нес нет никакого End, выполнение доходит до своего пика (в плане вложенности вызовов), а потом происходит череда возвратов чуть ни не до самого верха, и каждая функция подчищает за собой.
И у нас, если смотреть с высокоуровневой точки зрения, не происходит утечки памяти и других ресурсов. А если смотреть с низкоуровневой точки, то у нас не происходит нарушения структуры стека, всё что было в стек помещено, из него вовремя удаляется, все регистры, значение которых не должны меняться на время вызова вложенной процедуры, восстанавливали при выходе из неё свои прежние значения.
Теперь вернёмся к изначальному вопросу: внутри
Baaaz в каком-то месте стоит вызов
End.
В этом случае естественный ход вещей (череда возвратов из процедуры в процедуру в порядке, обратном череде вызовов) произойти не должен! Весь код, стоящий внутри
Baaaz после вызова
End выполниться не должен. Возврата из
Baaaz в
Bar произойти вообще не должно. Остаток
Bar, идущий после вызова
Baaaz тоже не должен выполниться. Словом, после обращения к
End никакой VB-шный код не должен получить шанса выполниться.
Выполнение VB-шного кода должно прекратиться, но выполнение кода вообще (в рамках процесса VB IDE), понятное дело, должно продолжиться с некоторой точки. Ведь End не означает, что и IDE должна остановиться и закрыться — среда должна продолжить работу, поэтому очевидно, что нам нужно как-то вернуться обратно в оконный цикл (внутри
ThunderMsgLoop). Но возвращение туда как итог череды возвратов из процедуры в процедуру (как это происходит обычно) здесь не подходит.
Вопрос: как вернуться из
Project1!Module1::Baaaz в
vb6.exe!ThunderMsgLoop, но так, чтобы минуя все те промежуточные процедуры, которые нам не нужны?
Как сделать своеобразный goto, но не в рамках одной процедуры, а из вызываемой процедуры в вызывающую, причём далеко не через одну, а сразу через несколько? Возможно ли это вообще, либо же это невозможная задача?
Во-первых, это вполне себе решимая задача, она является рядовой задачей в любом механизме обработки исключений, когда из места, где исключение выброшено, мы должны мигом переместиться на один или несколько уровней выше, где исключение будет обработано. И даже в Си, где в отличие от Си++ нет механизма исключений, всегда была пара из функций
setjmp/
longjmp, одна и которых позволяла запомнить контекст (то есть значение всех регистров процессора + EIP (указатель на «текущую» инструкцию), а вторая меняла контекст потока, заставляя его продолжить выполнение с запомненного места.
Во-вторых, главная дилемма таких возвратов: это, когда мы прыгаем «через голову», а точнее возвращаемся из 10-й (по вложенности) процедуры сразу в 3-ю, минуя уровни 4...9 — это как быть с ресурсами, которые выделили/захватили функции 4—9? Если отмотать указатель на вершину стека (ESP) на ту величину, которая была в момент запоминания контекста, вернуть все регистры, начать выполнение с запомненного места, то код будет выполняться правильно и стек не пострадает, но все те ресурсы, которые были выделены/захвачены, и правильное освобождение которых предполагалось при естественном ходе исполнения, так и останутся в подвешенном состоянии.
Эта проблема тоже имеет классическое решение — процесс называется раскруткой стека (stack unwinding), а всем функциям, которые могут оказаться в раскручиваемой цепочке, предлагается зарегистрировать либо сами ресурсы (которые должны быть освобождены), либо некий хендлер, который будет вызван механизмом исключений для того, чтобы дать промежуточной (в длинной цепочке вызов) функции подчистить за собой — так называемый finally-блок (если говорить в терминах try...catch...finally-подхода).
Так что обе проблемы в какой-то мере решаемы.
Теперь вернёмся к VB. А нужно ли нам, чтобы при обращении к
End, выполнение резко перепрыгивало сразу в
ThunderMsgLoop?
На самом деле — нет! И так не делается. При срабатывании End выполняется раскрутка только той части стека, которая была выделена
красным. И VB делает это с лёгкостью, потому что VB ведёт учёт всех «пользовательских» объектов (объектов тех классов, которые написаны на VB в рамках текущего проекта), всех внешних объектов, всех строк, всех своих локальных переменных, а равно как и знает структуру стека P-кодной виртуальной машины, где в стеке этой виртуальной машины какие переменные, у какой переменной какой тип и как, в зависимости от типа переменной, нужно эту переменную зачистить, чтобы не возникло утечки памяти.
Поэтому раскрутка
красной части стека вызовов для VB решается относительно легко: цикл, идущий по цепочке P-кодных инструкция просто прекращается, стековые фреймы, соответствующие красной части стека вызовов, уничтожаются, все ресурсы освобождаются, свои объекты уничтожаются, внешние объекты IUnknown::Release-ятся.
Так что в случае, если в коде обработчика события сидел End (не важно, в самом обработчике, или в другой процедуре, вызованной из обработчика события), с точки зрения
vb6.exe!CallProcWithArgs происходит нормальный обыкновенный возврат из вызванной процедуры (кстати CallProcWithArgs даже не знает, что скрывается за обработчиком события, на который у неё имеется адрес: либо это native-кодный обработчик из какой-то скомпилированной DLL, либо же это Native-кодный переходничок на P-кодный код текущего проекта — ей безразлично).
Из
vb6.exe!CallProcWithArgs происходит нормальный возврат в
vb6.exe!InvokeVtblEvent.
А из неё — в
vb6.exe!InvokeEvent.
И так далее, пока мы не вернёмся обратно в цикл прокачки сообщений.
В общем,
End делает принудительный многоуровневый перескок только в отношении кода, написанного на VB (и существующего в виде P-кода), и делает раскрутку только тех стековых фреймов, которые порождены работой P-кодной виртуальной машины. То есть только
красной части.
Синяя часть раскрутке не подвергается, никаких перескоков, нарушающих естественный ход выполнения, там не происходит.
Пока всё хорошо.
А теперь, после затянувшейся вводной части, начинаем приближаться к проблеме.
Пусть внутри обработчика события
Form_Paint (или
Timer1_Timer) вызвана функция
MsgBox.
MsgBox — модальная функция. И нам важно даже не то, что при показе сообщения другие окна блокируются, нам важно то, что внутри
MsgBox стартует и крутится свой собственный цикл обработки сообщений, и этот цикл не прекращается, пока диалог с сообщением не закроют, а это означает, что и возврата из MsgBox не произойдёт до тех пор, пока пользователь не разделается с диалогом и пока цикл не прервётся из-за этого.
Посмотрим, как выглядит стек вызовов после того, как из
Form1_Paint был сделан вызов
MsgBox:
vb6.exe!WinMain ➥
... ➥
vb6.exe!ThunderMsgLoop (в этой процедуре крутится оконный цикл)
➥
user32.dll!DispatchMessage ➥
vb6.exe!StdCtlWndProc (общая для всех VB-шных оконных классов оконная процедура-обёртка)
➥
vb6.exe!CommonGizWndProc (общая для всех VB-шных контролов оконная процедура-обёртка)
➥
vb6.exe!GrafPaintPicture ➥
vb6.exe!EvtErrFire ➥
vb6.exe!EvtErrFireWorker ➥
vb6.exe!InvokeEvent ➥
vb6.exe!InvokeVtblEvent ➥
vb6.exe!CallProcWithArgs ➥
Project1!Form1::Form_Paint ➥
vba6.dll!rtcMsgBox ➥
vb6.exe!HostDisplayMsgBox2 ➥
vb6.exe!VBMessageBox2 ➥
vb6.exe!VBDialogCover2 ➥
vb6.exe!MessageBoxPVoid ➥
user32.dll!MessageBoxIndirect ➥
user32.dll!MessageBoxWorker ➥
user32.dll!SoftModalMessageBox ➥
user32.dll!InternalDialogBox ➥
user32.dll!DialogBox2 (здесь крутится новый оконный цикл)
Наиболее важный вывод из этой картины — это то, что у нас в стеке вызовов (то есть в цепочке вызываемых друг из друга процедур) появилось чередование
синих и
красных участков. И вершина стека вызовов (то есть конец цепочки) у нас синяя, а я напомню, что VB умеет безусловно и насильно прерывать и прекращать работу только «красного» кода и только для красной части стека VB умеет делать unwinding и корректно подчищать за функциями, работа которых была абортирована и которые не получат шанса подчистить за собой.
Но это не страшно, потому что мы точно знаем, что из недр user32.dll, где сейчас крутится и будет крутиться цикл прокачки оконных сообщений (пока диалог не закроют), никто не обратится к
End и не поставит VB6 перед лицом наразрешимой задачи.
Этот оконный цикл внутри
user32.dll!DialogBox2 — это почти такой же оконный цикл, как и в
vb6.exe!ThunderMsgLoop, основная суть которого состоит в итеративном получении сообщений из очереди с помощью
GetMessage/
PeekMessage и направлении их в соответствующую оконную процедуру с помощью
DispatchMessage.
И этот оконный цикл совершенно никак не фильтрует оконные сообщения и не дискриминирует окна VB-форм в плане доставки/недоставки
WM_PAINT и др. сообщения (вопреки мнениям, что «MsgBox не делает DoEvents»). Он перелопачивает абсолютно всё, и если мы проведём по окну нашей VB-формы (путём перетаскивания) диалоговым окном с сообщением и затрём таким образом старый растр, который там был, то система как обычно отправит окну нашей VB-формы сообщение
WM_PAINT.
И оно будет, в общем случае, незамедлительно подхвачено оконным циклом внутри
user32.dll!DialogBox2, будет вызвана ф-ция
DispatchMessage, которая найдёт и вызовет оконную процедуру VB-формы.
Цепочка вызовов при этом будет такой:
vb6.exe!WinMain ➥
... ➥
vb6.exe!ThunderMsgLoop (в этой процедуре крутится оконный цикл)
➥
user32.dll!DispatchMessage ➥
vb6.exe!StdCtlWndProc (общая для всех VB-шных оконных классов оконная процедура-обёртка)
➥
vb6.exe!CommonGizWndProc (общая для всех VB-шных контролов оконная процедура-обёртка)
➥
vb6.exe!FormCtlProc (оконная процедура для окон самих VB-форм)
➥
vb6.exe!GrafPaintPicture ➥
vb6.exe!EvtErrFire ➥
vb6.exe!EvtErrFireWorker ➥
vb6.exe!InvokeEvent ➥
vb6.exe!InvokeVtblEvent ➥
vb6.exe!CallProcWithArgs ➥
Project1!Form1::Form_Paint ➥
vba6.dll!rtcMsgBox ➥
vb6.exe!HostDisplayMsgBox2 ➥
vb6.exe!VBMessageBox2 ➥
vb6.exe!VBDialogCover2 ➥
vb6.exe!MessageBoxPVoid ➥
user32.dll!MessageBoxIndirect ➥
user32.dll!MessageBoxWorker ➥
user32.dll!SoftModalMessageBox ➥
user32.dll!InternalDialogBox ➥
user32.dll!DialogBox2 (здесь крутится новый оконный цикл)
➥
user32.dll!DispatchMessage ➥
vb6.exe!StdCtlWndProc ➥
vb6.exe!CommonGizWndProc ➥
vb6.exe!FormCtlProc ➥
vb6.exe!GrafPaintPicture ➥
vb6.exe!EvtErrFire ➥
vb6.exe!EvtErrFireWorker ▐ ➥
vb6.exe!InvokeEvent ▐ ➥
vb6.exe!InvokeVtblEvent ▐ ➥
vb6.exe!CallProcWithArgs ▐ ➥
Project1!Form1::Form_PaintЧасть цепочки, помеченна
зелёной линией, в реальной жизни никогда не выполняется, потому что
EvtErrFireWorker вызывает
_WillFire, чтобы проверить, должно ли событие сработать, и если сейчас отображается диалог, то
_WillFire даёт понять, что обработчик вызван быть не должен — это именно тот момент распутья, наличие которого не нравится Майклу. Если
_WillFire вернёт соответствующий код, то
InvokeEvent не вызывается и обработчик события не срабатывает. Именно
_WillFire отвечает за то, чтобы таймеры не срабатывали, когда проект поставлен на паузу, к примеру. Но представим, что VB был бы устроен не так, как он сейчас устроен, а так, как хотелось бы Майлку, чтобы он был устроен — то есть чтобы во время показа диалогов события Paint/Timer не блокировались.
Тогда у нас в цепочке вызовов было бы
два вхождения в
Form_Paint. Не настолько важно, что именно это за события и что за обработчики, в верхнем может быть
Command1_Click, а в нижнем —
Timer1_Timer.
Важно то, что у нас в цепочке есть чередование синих и красных участков, и что второй обработчик события вызван из недр
user32!DialogBox2, то есть из чужеродного кода. И вот теперь у нас из второго (нижнего) обработчика вызывается
End, а это означает, что нормальной (естественной) череды возвратов наверх из глубины этой длинной цепочки вызовов произойти не должно, а попросту выполнение должно перескочить сразу наверх.
Да только как это сделать, если VB понятия не имеет, как обойтись с синими участками и с частью стека, сформированной чужеродным кодом?
Это серьёзная проблема. Перед тем, как зайти во второй (по вложенности) обработчик события, где-то в недрах
user32!MessageBox был вызов
CreateWindow, породивший собственно то самое окно (и вернуший его HWND). Код user32.dll написан с абсолютной уверенностью, что если выполнение зашло в
MessageBox, то оно рано или поздно вернётся из него, а значит, если по пути выполнения что-то было создано/выделено, то оно обязательно будет освобождено/удалено.
Но когда из
MessageBox может быть вызван некий VB-шный код, а из VB-шного кода может быть сделан
End, сама суть
End предполагает, что выполнение из вызванного VB-шного кода обратно в вызывающую процедуру уже не вернётся. А значит, в частном случае, код user32 не получит шанса довыполнить оставшуюся часть MessageBox и уничтожить окно с сообщением (и тогда кто его уничтожит? Сам VB? Он понятия не имеет, что сторонний код породил какое-то окно, которое теперь уничтожать нужно нам, и где взять хендл), а в общем случае — просто нет способа сделать раскрутку той части стека, которая подкрашена жёлтой линией:
vb6.exe!WinMain ➥
... ➥
vb6.exe!ThunderMsgLoop (в этой процедуре крутится оконный цикл)
➥
user32.dll!DispatchMessage ➥
vb6.exe!StdCtlWndProc (общая для всех VB-шных оконных классов оконная процедура-обёртка)
➥
vb6.exe!CommonGizWndProc (общая для всех VB-шных контролов оконная процедура-обёртка)
➥
vb6.exe!FormCtlProc (оконная процедура для окон самих VB-форм)
➥
vb6.exe!GrafPaintPicture ➥
vb6.exe!EvtErrFire ➥
vb6.exe!EvtErrFireWorker ➥
vb6.exe!InvokeEvent ➥
vb6.exe!InvokeVtblEvent ➥
vb6.exe!CallProcWithArgs ←
на этот уровень с самого низа должно перепрыгнуть выполнение после End ▐ ➥
Project1!Form1::Form_Paint ▐ ➥
vba6.dll!rtcMsgBox ▐ ➥
vb6.exe!HostDisplayMsgBox2 ▐ ➥
vb6.exe!VBMessageBox2 ▐ ➥
vb6.exe!VBDialogCover2 ▐ ➥
vb6.exe!MessageBoxPVoid ▐ ➥
user32.dll!MessageBoxIndirect ▐ ➥
user32.dll!MessageBoxWorker ▐ ➥
user32.dll!SoftModalMessageBox ▐ ➥
user32.dll!InternalDialogBox ▐ ➥
user32.dll!DialogBox2 (здесь крутится новый оконный цикл)
▐ ➥
user32.dll!DispatchMessage ▐ ➥
vb6.exe!StdCtlWndProc ▐ ➥
vb6.exe!CommonGizWndProc ▐ ➥
vb6.exe!FormCtlProc ▐ ➥
vb6.exe!GrafPaintPicture ▐ ➥
vb6.exe!EvtErrFire ▐ ➥
vb6.exe!EvtErrFireWorker ▐ ➥
vb6.exe!InvokeEvent ▐ ➥
vb6.exe!InvokeVtblEvent ▐ ➥
vb6.exe!CallProcWithArgs ▐ ➥
Project1!Form1::Form_PaintКак
End может удалить/откатить всю эту отмеченную жёлтой линией часть стека и насильно перевести выполнение сразу не насколько уровней вложенности вверх, минуя поочерёдные возвраты из функции в функцию?
Да никак!
End понятия не имеет, где в стеке, построенном чужеродным кодом, локальные переменные, что они обозначают и как их «подчищать». При насильной смене EIP выделенные блоки памяти останутся не освобождёнными, созданные окна останутся не уничтоженными, временно скрытый указатель мыши так и останется скрытым, и так далее.
Чисто гипотетически,
End мог бы решить
эту проблему, выбросив некое специальное SEH-исключение, которое привело бы к тому, что произошла бы раскрутка стека до его примерно середины силами и средствами SEH, а все функции, сидящие в цепочке, получили бы шанс подчистить за собой через вызов зарегистрированных SEH-хендлеров (finally-обработчиков). Проблема в том, что такого специального SEH-исключения а-ля «отлаживаемая программа обратилась к End» не предусмотрено в Windows, а код user32 не использует SEH и не имеет finally-блоков в принципе.
Поэтому нет способа из некоей процедуры XXX, вызванной в конечном счёте из недр MsgBox, выкинуть исключение так, чтобы магическим образом сразу из XXX переместиться туда, откуда была была вызвана MsgBox (или даже выше уровнем). Не потому, что End так не умеет, не потому, что VB не додуман, а потому что сама архитектура Windows в плане устройства пользовательской подсистемы (user32) этого не предполагает.
Вот именно по этой причине на время, пока показывается какой-то диалог и пока в недрах user32 крутится собственный ассоциированный с показом диалога оконный цикл, срабатывание обработчиков событий в VB блокируется, хотя сами оконные сообщения, предвещающие эти события, исправно доставляются (как раз тем самым ассоциированным с показом диалога оконным циклом).
В ином случае из обработчика события может быть сделан либо
End, что поставит перед VB неразрешимую задачу, либо цененаправленно (Err.Raise) или совершенно случайно для программиста в коде обработчика события может быть выкинута ошибка, а ошибка требует точно такой же раскрутки стека и перепрыгивания выполнения на несколько уровней вверх до ближайшего
On Error ... (если он вообще есть).
End-то может и редкий случай в обработчике событий, особенно в событии Paint, но вот обычная ошибка (Out of memroy, Subscript out of bounds и т.п) запросто может произойти в обработчике события
Paint или
Timer, и если это произошло во время показа диалога,
НЕ СУЩЕСТВУЕТ способа закрыть диалог (или вынудить диалог закрыться) и переместиться на обработчик ошибки. Так что ошибки и их обработка точно так же страдают от проблемы, как и End.
И не остаётся ничего, кроме как блокировать срабатывание обработчиков событий на время пока диалога. Это звучит как-то не очень привлекательно, пока мы не разобрались с тонкостями, но понимание, что у среды теряется контроль за ходом исполнения кода, если обработчик события вызван из «диалоговой» функции вроде MessageBox, заставляет принять неминуемость такого подхода.
Тогда возникает вопрос: почему при модальном показе экземпляра формы поверх других экзмпляров форм обработчики событий продолжают срабатывать? В чём принципиальное отличие модального показа экземпляра формы (с помощью
Show vbModal) от показа модального диалога, если и в том, и в другом случаях инициируется и начинает работать новый цикл прокачки сообщений, который реализован в рамках Native-кодного кода vb6.exe/user32.dll. То есть имеется то же чередование синих и красных зон, но проблемы с раскруткой стека нет и обработчики событий не блокируются на время показа модального окна. Почему?
Рассмотрим такую ситуацию: на форме Form1 кликнули по кнопке и в ответ на это показывается экземпляр Form2 модально к экземпляру Form1, при этом на Form1 лежит SuperTimer, у которого в обработчике
SuperTimer_Timer с некоторой вероятностью вызывается End.
Трассировка стека вызовов на момент обращения к End будет такой:
vb6.exe!WinMain ➥
... ➥
vb6.exe!ThunderMsgLoop (в этой процедуре крутится оконный цикл)
➥
user32.dll!DispatchMessage ➥
vb6.exe!StdCtlWndProc (общая для всех VB-шных оконных классов оконная процедура-обёртка)
➥
vb6.exe!CommonGizWndProc (общая для всех VB-шных контролов оконная процедура-обёртка)
➥
vb6.exe!PushCtlProc (оконная процедура VB-шных кнопкок)
➥
vb6.exe!_DoClick ➥
vb6.exe!EvtErrFire ➥
vb6.exe!EvtErrFireWorker ➥
vb6.exe!InvokeEvent ➥
vb6.exe!InvokeVtblEvent ➥
vb6.exe!CallProcWithArgs ➥
Project1!Form1::Command1_Click ➥
vb6.exe!_CFORM_vtbl::meth__imethSHOW ➥
vb6.exe!RefCallMethFromVtable ➥
vb6.exe!MethCallMethod ➥
vb6.exe!CommonGizWndProc ➥
vb6.exe!FormCtlProc ➥
vb6.exe!ShowMethShow ➥
vb6.exe!FormShowModal ➥
vb6.exe!CMsoComponent::PushMsgLoop ➥
vb6.exe!CMsoCMHandler::FPushMsgLoop ➥
vb6.exe!ThunderMsgLoop ➥
user32.dll!DispatchMessage ➥
vb6.exe!StdCtlWndProc ➥
vb6.exe!CommonGizWndProc ➥
vb6.exe!TimerCtlProc ➥
vb6.exe!EvtErrFire ➥
vb6.exe!EvtErrFireWorker ➥
vb6.exe!InvokeEvent ➥
vb6.exe!InvokeVtblEvent ➥
vb6.exe!CallProcWithArgs ➥
Project1!SuperTimer_Timer (здесь обращение к End)Что мы видим? У нас опять есть чередование «синих» и «красных» функций в стеке вызовов, вершина стека (конец цепочки) «красная», но ей предшествует большой синий регион, который VB не умеет раскручивать, и, что самое плохое, в этом регионе есть участок стека, построенный чужеродным кодом из user32 — здесь это сокращённо отмечено как вызов
DispatchMessage, хотя на самом деле
DispatchMessage это цепочка вызовов:
user32.dll!DispatchMessage ➥
user32.dll!DispatchMessageWorker ➥
user32.dll!UserCallWinProcCheckWow ➥
user32.dll!InternalCallWinProcКак мы знаем, VB умеет делать unwinding (раскрутку) только красных участков стека, а здесь между двумя красными имеется синий участок, но, тем не менее, модальный показ экземпляра VB-формы не блокирует таймер на родительском окне (допуская появление двух красных участков, разделённых синим), а обращение к End из этого таймера корректно завершает проект (несмотря на наличие синего участка между двумя красными). Так как же VB удаётся разделаться с синим участком стека вызовов, лежащим между двумя красными? Может он всё-таки умеет обходиться с синими участками? А если нет, то в чём принципиальная разница с модальным MsgBox, где события перекрытого окна приходилось блокировать?
Ответ прежний: VB умеет делать раскрутку
только красных участков. Раскручивая красный участок, лежащий на вершине стека (т.е. в конце цепочки вызовов), VB разрушает все красные стековые фреймы, доходя до ближайшего синего. Встретив синий стековый фрейм, VB прекращает раскрутку и позволяет синему коду продолжить свой естественный ход выполнения.
То есть, когда внутри
SuperTimer_Timer сделано обращение к
End, уничтожается только красный фрейм вызова
Project1!SuperTimer_Timer и выполнение возвращается в
vb6.exe!CallProcWithArgs.
vb6.exe!CallProcWithArgs после того, как выполнение вернулась в неё, отрабатывает
ДО КОНЦА (это важно!), как если бы внутри таймерного события не было End. После возврата из
vb6.exe!CallProcWithArgs выполнение попадает в
vb6.exe!InvokeVtblEvent.
Остаток
vb6.exe!InvokeVtblEvent тоже выполняется до конца, и выполнение возвращается в
vb6.exe!InvokeEvent — и вот так по цепочке выполнение двигается из глубин цепочки вызовов в сторону её начала. И в скором времени выполнение возвращается из
DispatchMessage обратно в
ThunderMsgLoop.
Там до обращения к End крутился вторичный цикл обработки сообщений, который должен закончиться только с закрытием модально показанного экземпляра формы Form2. Когда выполнение вернётся из
DispatchMessage в
ThunderMsgLoop, по неслучайному совпадению этот экземпляр как раз будет закрыт (ибо
End уничтожает все VB-шные объекты, точнее зомбифицирует их, помечая как «мёртвые», но оставляя их пока в памяти на случай, если откуда-то будут поступать вызовы IUnknown::Release на этапе раскрутки/терминации работы проекта) и больше ни одной итерации оконного цикла выполнено не будет.
vb6.exe!ThunderMsgLoop в нормальном естественном порядке доработает до своего конца и произойдёт возврат в
vb6.exe!CMsoCMHandler::FPushMsgLoop.
И таким образом все функции из синего участка стека вызовов будут в нормальном порядке дорабатывать до своего финала и делать нормальный возврат в вызывающую процедуру.
И так до тех пор, пока в результате череды возвратов мы не попадём в
vb6.exe!_CFORM_vtbl::meth__imethSHOW.
vb6.exe!_CFORM_vtbl::meth__imethSHOW — это именно то, указатель на что содержится в ячейке vtable, соответствующей методу
Show. По своей сути это обёртка, которая вызов метода COM-объекта превращает в чисто-процедурный вызов
RefCallMethFromVtable. Все методы и свойства форм и встроенных в VB контроллов, доступные через их vtable, на деле являются такими фиктивными обёртками, превращающими объектный вызов (в духе COM) в чисто-процедурный. Но это просто информация для справки, на дело это не влияет.
Итак,
vb6.exe!_CFORM_vtbl::meth__imethSHOW тоже нормально отработает до конца и сделает возврат выполнения в вызывающую процедуру,
и вот тут происходит кое-что интересное, потому что из синей процедуры мы попадаем в красную! Вот участок стека вызовов, о котором сейчас идёт речь:
...
➥
vb6.exe!CallProcWithArgs ➥
Project1!Form1::Command1_Click ➥
vb6.exe!_CFORM_vtbl::meth__imethSHOW ➥
vb6.exe!RefCallMethFromVtable ...
Я уже писал, что красные участки стека вызовов — это в какой-то мере наша условность, потому что они не эквивалентны в полной мере синим участкам, так как красные участки соответствуют P-коду, который исполняется P-кодной виртуальной машиной, которая сама реализована в Native-коде, а синие участки соответствуют Native-кодным процедурам, которые исполняет сам процессор. И за красными участками стека вызовов на самом деле скрывается синий стековый фрейм виртуальной машины (являющейся частью vba6.dll (или msvbvm60.dll в случае скомпилированных проектов)), так как виртуальная машина это просто native-код, который гонит цикл обхода P-кодных инструкций, обходит их одну за другой и для каждой инструкции делает какое-то действие.
Так вот, всякий раз, когда происходит возврат выполнения из синей области в красную (то есть когда чужеродный native-код, вызванный из P-кода, возвращает управление обратно в P-кодную часть цепочки вызовов), в виртуальной машине выполняется проверка поля «режим» структуры EBTHREAD. Если режим «нормальный», виртуальная машина продолжает выполнять P-код дальше как ни в чём не бывало. Обращение же к
End меняет это поле в структуре EBTHREAD на режим «выполнение прекращено», поэтому когда происходит возврат из чужерожного native-кода в проектный P-код, выполнение напарывается на эту ловушку. Это ловушка на границе между синей и красной зоной — везде, где возможен возврат из синей в красную.
В случае вызова метода
Show формы (строка
Form2.Show vbModal, Me), на уровне P-кода за таким вызовом стоит P-кодная инструкция
VCallHresult. Возврат из
vb6.exe!_CFORM_vtbl::meth__imethSHOW происходит в то место кода виртуальной машины, которое отвечает за P-кодную инструкцию
VCallHresult (а она отвечает за вызовы методов COM-интерфейсов через vtable, причём методы должны возвращать HRESULT, что и отражено в названии).
После возврата в виртуальную машину из чужеродного кода проверяется, жив ли ещё текущий поток (поле в структуре EBTHREAD), и если нет (т.е. если было обращение к End), то вместо продолжения нормального исполнения P-кода происходит генерация ошибки (с кодом 0x9C68). Генерация ошибки приводит к раскрутке уже очередного красного участка стека вызовов (на данный момент он опять находится на вершине стека, потому что лежавшая вслед за ним синяя часть самораскрутилась в ходе нормального выполнения).
Этим обеспечивается, что код, стоявший вслед за вызовом метода
Show, выполнен не будет, возврат из процедуры, где располагался вызов метода
Show в вызывающую процедуру (если вызывающая процедура — тоже часть кода VB-проекта) тоже не произойдёт. Весь VB-код, который соответствует текущему красному участку стека вызовов, после того, как выполнение вернулось из синей части обратно в виртуальную машину, выполняться больше не будет, а вместо этого будут уничтожены красные стековые фреймы (в освобождением ресурсов) и выполняется вернётся в предшествующую красному участку стека вызовов синюю процедуру.
Подытожим:- VB обязан уметь делать раскрутку стека и принудительный возврат выполнения не просто в непосредственно вызывающую процедуру (по отношению к текущей), а сразу на несколько уровней вложенности, минуя промежуточные и не допуская выполнения кода промежуточных процедур. Необходимость уметь это делать обусловлена тремя потребностями:
- Обращение к End из любого места в коде
- Программист может остановить работу кода кнопкой «Стоп» из VB IDE в любой момент.
- Возникновение ошибки в любом месте в коде должно раскрутить стек и перенести выполнение к обработчику ошибок, если он есть.
- Но VB умеет абортировать выполнение и делать раскрутку стека только для проектного кода, который представлен P-кодом (и который в трассировке стека вызовов мы условились изображать красным цветом). Чужеродный код (обычно native-код, но вовсе не обязательно) по первом требованию VB абортировать не умеет, и ему ничего не остаётся делать, кроме как давать ему возможность отработать и завершиться «своим ходом».
- Когда VB-код исполняется, пока выполнение кода «гуляет» непосредственно по самому VB-коду, стек вызовов выглядит так:
чужеродный код
➥ код проекта
В этом случае обращение к End или кнопка «Стоп» в IDE раскручивает красную часть и возобновляет выполнение с того места, где был переход из синей в красную. Синяя часть продолжает выполняться как обычно — точно так же, как если бы красная отработала штатно.
- Когда VB-код исполняется и из VB-кода делается вызов к внешнему (чужеродному) коду, например, к коду функций рантайма, API-функциям, или к методу сторонних объектов, или же к методам своих объектов, не являющихся (слово относится к методам) частью VB-кода, стек вызовов выглядит так:
чужеродный код
➥ код проекта
➥ чужеродный код
В этом случае VB не может в любой момент остановить выполнение работы кода, да, собственного, у него и нет контроля над выполняемым чуждеродным кодом. VB вынужден дождаться, когда «нижний» чужеродный код закончит работу и вернёт выполнение на «красный уровень», и только тогда VB сможет остановить работу кода и сделать раскрутку стека, но лишь до верхнего чужеродного кода.
- Бывают ситуации, когда код проекта передаёт управление в чужеродный код, а из этого чужеродного кода выполнение опять переходит в код проекта (но не в результате возврата, а в результате обратного вызова). В простейшем случае это выглядит так:
чужеродный код
➥ код проекта
➥ чужеродный код
➥ код проекта
но в более сложном случае чередование может быть и таким:
чужеродный код
➥ код проекта
➥ чужеродный код
➥ код проекта
➥ чужеродный код
➥ код проекта
➥ чужеродный код
➥ код проекта
Во всех подобных случаях при возникновении необходимости раскрутки стеки и переноса выполнения на несколько уровней выше (минуя естественный ход исполнения), раскрутка может быть выполнена только для самого последнего в цепочке красного уровня. После раскрутки и абортирования красного кода (проектного кода) выполнение перейдёт в чужеродный код, и у VB нет рычагов воздействия на чужеродный код — чужеродный код может сколько угодно долго продолжать выполняться или крутить циклы или даже делать попытки из себя опять вызвать какой-то проектный (красный) код. Поскольку рычагов воздействия нет, VB ничего не остаётся делать, кроме как ждать, пока синий код в ходе естественного выполнения дойдёт до своего логического конца и вернёт выполнение обратно в красный код (проектный код).
По этой причине на время показа диалогов вызов обработчиков событий подавляется, чтобы было так:
#1 код Windows и VB IDE
#2 ➥ код проекта, вызвавший показ диалога
#3 ➥ код, отобразивший диалог и крутящий оконный цикл
а не так:
#1 код Windows и VB IDE
#2 ➥ код проекта, вызвавший показ диалога
#3 ➥ код, отобразивший диалог и крутящий оконный цикл
#4 ➥ код проекта — обработчик события
потому что в последнем случае обращение к End / кнопка «Стоп» / возникновении ошибки лишь заставит выполнение переместиться с четвёртого уровня на третий, а там выполнение продолжит «крутиться» сколь угодно долго, и никакими способами VB не может форсировать переход с третьего уровня на второй.
- В общем и целом, когда в стеке вызовов чередуются синие и красные участки, раскрутка идёт по принципу «красное абортируется сразу и бескомпроммисно и выполнение возвращается в синюю часть, а синей мы даём полную свободу выполняться до конца так, как она считает нужным и столько, сколько она считает нужным».
- При модальном показе экземпляров форм с помощью Show vbModal вызов обработчиков событий у окна-владельца не блокируется по той причине, что в этом случае хоть и имеется чередование красных и синих участков на стеке вызовов, относительно синего участка, лежащего между двумя красными, имеется гарантия, что после возврата из более удалённого от начала цепочки вызовов красного участка в промежуточный синий участок, выполнение в этом промежуточном (лежащем между двумя красными) синем участке не задержится, не зациклится, не застрянет, а очень скоро перейдёт в менее удалённый от начала цепочки вызовов красный участок. Напомню, что в отношении функций, показывающих диалоги и имеющих реалиацию оконного цикла где-нибудь в user32.dll или comdlg32.dll такой гарантии нет.
Бонус:Подавление срабатывания обработчиков событий во время отображения модальных диалогов устроено так, что оно не отслеживает сам факт появления модальных диалогов (каким путём бы они ни были при этом созданы), а полагается на флаг, устанавливаемый функциями MsgBox/InputBox/пр.
Поэтому если показывать окно сообщения не через
MsgBox, а через
MessageBox, подавления срабатывания событий не будет. Это даёт нам возможность посмотреть, как работал бы VB если бы желание Майкла было воплощено:
- Код: Выделить всё
Option Explicit
Private Declare Function MessageBox Lib "user32" Alias "MessageBoxA" (ByVal hwnd As Long, ByVal lpText As String, ByVal lpCaption As String, ByVal wType As Long) As Long
Private Sub Form_Activate()
Timer1.Interval = 250: Timer1.Enabled = True
MessageBox Me.hwnd, "Do you like it?", "Lan", vbYesNo
End Sub
Private Sub Timer1_Timer()
Static clr As Long
clr = clr + 1
If clr <= 15 Then
Me.BackColor = QBColor(clr)
Else
' Цвета кончились
Do Until Me.CurrentY > Me.ScaleHeight
Me.Print "ЦВЕТА КОНЧИЛИСЬ!!! Сейчас будет обращение к End!"
Loop
End ' проект должен завершиться, но не тут-то было
End If
End Sub
В этом случае, если сразу закрыть окно сообщения, то после перебора всех 15 цветов, проект завершится, экземпляр формы закроется, IDE перейдёт в состояние как до запуска проекта. Если же окно сообщения (показанное ф-цией MessageBox) не закрывать, то обработчик таймера будет срабатывать даже при открытом сообщение, однако 16-ое по счёту срабатывание хоть и сделает обращение к
End — проект от этого остановлен не будет, экземпляр формы останется существовать, диалоговое окно с сообщением тоже никуда не исчезнет, среда (VB IDE) останется в состоянии «проект выполняется», редактирование кода по прежнему будет заблокированным, однако кнопки «Пауза» и «Стоп» перестанут работать:
Лишь только после того, как пользователь вручную закроет окно, показанное ф-цией MessageBox, проект завершится. И хорошо, если от окна можно отделаться, а если окно криво создалось, или имеет нулевой размер, или ему по ошибке установили пустой clip-регион, и закрыть его не представляется никакой возможности? У нас, пока проект в состоянии «запущен», даже меню сохранения кода проекта заблокировано.