Начнём с затравки. Это действительно просто:
procedure TForm1.Button1Click(Sender: TObject); var Wnd: HWND; function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall; begin if AWnd = Wnd then Caption := 'OK'; Result := True; end; begin Wnd := Handle; EnumWindows(@EnumWindowsProc, 0); end;Вопрос: каким образом
EnumWindowsProc
получает доступ к локальной переменной Wnd
и свойству Caption
?Если грубо и просто, то для обращения к переменной
Wnd
функция EnumWindowsProc
говорит: "возьми первую переменную у моего вызывающего". Иными словами, EnumWindowsProc
производит доступ к переменной на стеке относительно своего вызова.Это будет работать нормально, если
EnumWindowsProc
вызвать напрямую из Button1Click
: конструкция "возьми первую переменную у моего вызывающего" обратится к Wnd
(первая переменная) из Button1Click
(который является вызывающим для EnumWindowsProc
) - что нам и нужно.Но что произойдёт, если
EnumWindowsProc
вызовет EnumWindows
? В этом случае вызывающим для EnumWindowsProc
будет не Button1Click
, а EnumWindows
. Иными словами, конструкция "возьми первую переменную у моего вызывающего" обратится не к Wnd
из Button1Click
, а к некоторой локальной переменной из EnumWindows
- что приведёт к чтению мусора.Аналогичное справедливо и для поля
Caption
.Почему компилятор такое допускает? Ну, вообще-то, он этого не допускает. Код вида
EnumWindows(EnumWindowsProc, 0);не скомпилируется, он сгенерирует ошибку "E2094 Local procedure/function 'EnumWindowsProc' assigned to procedure variable". Компилятор не даст вам передать локальную подпрограмму в качестве callback-а и не даст сохранить её во временную переменную. Однако, указывая оператор
@
, вы отбрасываете информацию о типе (указатель на подпрограмму) и используете нетипизированный указатель. Разумеется, нетипизированный указатель совместим с чем угодно - вот почему компилятор пропускает фрагмент вида
EnumWindows(@EnumWindowsProc, 0);
К сожалению, на самом деле, ошибка "Local procedure/function 'EnumWindowsProc' assigned to procedure variable" будет сгенерирована для функций типаBeginThread
, но неCreateThread
и неEnumWindows
. Дело в том, чтоCreateThread
иEnumWindows
используют крайне неудачный дизайн прототипа: вместо настоящего процедурного параметра эти подпрограммы объявляют параметр как простой нетипизированный указатель. Поэтому для этих функций нет иного выхода как передавать callback через взятие указателя.
Окей, теперь переходим к более сложному, часть два:
procedure TForm1.Button1Click(Sender: TObject); function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall; var Wnd: HWND; begin Wnd := HWND(AParam); if AWnd = Wnd then Result := True else Result := False; end; var Wnd: HWND; begin Wnd := Handle; EnumWindows(@EnumWindowsProc, LPARAM(Wnd)); end;Кажется, что в этом коде нет проблем? Мы не используем поля объекта и локальные переменные выше по стеку вызовов? И, вроде бы, этот код работает?
Да, это задачка с подвохом. На самом деле, проблема всё ещё есть.
Попробуйте скомпилировать этот код для x64. Запустите его. Ничего не замечаете? Почему-то
AWnd
становится равен AParam
, AParam
становится случайным числом, а настоящее окно AWnd
и вовсе пропадает. Что происходит?В отличие от кода x32, который для доступа к переменным просто смещается по стеку на заранее известное смещение от собственных локальных переменных, код x64 иначе производит доступ к локальным переменным: для всех локальных подпрограмм передаётся новый (скрытый) параметр - указатель на базу стека вызывающей подпрограммы. Иными словами, наша
EnumWindowsProc
, на самом деле, выглядит так:
function EnumWindowsProc(const ARBP: Pointer; const AWnd: HWND; const AParam: LPARAM): BOOL; stdcall;Разумеется, это происходит для всех локальных подпрограмм - вне зависимости от того, обращаются ли они к локальным переменным вне их личной зоны видимости или нет (в противном случае получилось бы несовпадение сигнатур функций в зависимости от того, кто кого вызывает).
В целом, ситуация крайне похожа на то, как методы объекта принимают скрытый первый параметр
Self
.Вот вам и проблема. "Настоящий"
AWnd
сходит за (скрытый) ARBP
- который никак не используется функцией и потому игнорируется. "Настоящий" AParam
располагается на месте AWnd
, ну а на место AParam
приходится какой-то мусор из регистра R8
. Напомню, что единое соглашение вызова x64 (на x64 игнорируются модификаторы модели вызова register
, stdcall
и т.д.) передаёт первые параметры в регистрах RCX
, RDX
, R8
и R9
, а пятый и последующие параметры передаются через стек).Примечание: поскольку все параметры функции укладываются в регистры, то стек не используется. Следовательно, возврат управления из EnumWindowsProc
происходит без проблем.
Мораль истории: не используйте @ при передачи параметров-функций. Не используйте локальные подпрограммы в качестве callback-ов. Что также подтверждается документацией:
Nested procedures and functions (routines declared within other routines) cannot be used as procedural values, nor can predefined procedures and functions.Задачка была на знание справки Delphi ;)
См. также:
- Новое ключевое слово static в Delphi
- Исправляем плохой дизайн API: функции обратного вызова без user-аргумента
Далее была бонус-задачка, которая изначально не планировалась, но была оперативно написана по горячему обсуждению решения части два:
function EnumWindowsProc(const AWnd: HWND; const AParam: LPARAM): Boolean; stdcall; var Wnd: HWND; begin Wnd := HWND(AParam); if AWnd = Wnd then Result := True else Result := False; end; procedure TForm1.Button1Click(Sender: TObject); var Wnd: HWND; begin Wnd := Handle; EnumWindows(@EnumWindowsProc, LPARAM(Wnd)); end;Кажется, что уж в этом-то коде нет проблем? Функция теперь глобальная, не обращается к чужим локальным данным, т.е. сигнатура должна быть корректной?
Заметьте, что прототип функции
EnumWindowsProc
не совпадает с тем, что требует EnumWindows
. Дело в том, что функция EnumWindowsProc
объявлена как возвращающая значение Boolean
. В то время как оригинальная функция требует BOOL
.Чем
Boolean
отличается от BOOL
? Вспомним: размером. Boolean
занимает один байт, а BOOL
- четыре байта.В частности, это означает, что присваивание
Result := False
(для случая когда Result
имеет тип Boolean
) обнулит только младший байт выходного регистра (EAX
/RAX
), а остальные три/семь байтов останутся без изменения! Почти наверняка в EAX
/RAX
- не ноль. Например, этот регистр используется как буфер при работе с параметрами подпрограммы, которые (параметры) - не ноль, более того - обычно больше 255. Таким образом, установка младшего байта в ноль не обнулит регистр. Иными словами (настоящий) Result
(который BOOL
) никогда не станет False
- даже хотя Result
(в виде Boolean
) принимает значение False
.Примечание: увидеть это поведение под отладчиком можно скомпилировав программу без оптимизации.
Упражнение: почему модификаторы
const
не являются проблемой в прототипе?Для тех, кому недостаточно "грубо и просто" - можно почитать детальный анализ задачи в блоге _Rouse.
Комментариев нет:
Отправить комментарий
Можно использовать некоторые HTML-теги, например:
<b>Жирный</b>
<i>Курсив</i>
<a href="http://www.example.com/">Ссылка</a>
Вам необязательно регистрироваться для комментирования - для этого просто выберите из списка "Анонимный" (для анонимного комментария) или "Имя/URL" (для указания вашего имени и (опционально) ссылки на сайт). Все прочие варианты потребуют от вас входа в вашу учётку.
Пожалуйста, по возможности используйте "Имя/URL" вместо "Анонимный". URL можно просто не указывать.
Ваше сообщение может быть помечено как спам спам-фильтром - не волнуйтесь, оно появится после проверки администратором.
Примечание. Отправлять комментарии могут только участники этого блога.