Функции обратного вызова
Другое очень важное понятие в Windows-программировании — это понятие функции обратного вызова (callback-функции). Функцией обратного вызова называется функция приложения, которая никогда не вызывается напрямую другими функциями или процедурами этого приложения (хотя ничто не запрещает это делать), а вызывается операционной системой Windows. Это позволяет Windows общаться с приложением напрямую посредством различных параметров, определенных как функции обратного вызова. К этим функциям выдвигаются требования: во-первых, эти функции должны быть именно функциями, а не методами класса (хотя это иногда можно обойти); во-вторых, эти функции должны быть написаны в соответствии с моделью вызова stdcall.
В качестве примера такой функции может послужить функция EnumWindows. В справочной системе она описана следующим образом:
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);
В Windows.pas она имеет вид:
function EnumWindows (lpEnumFunc: TFNWndEnumProc; lParam: LPARAM : BOOL;
stdcall
В качестве первого параметра должен быть указатель на функцию обратного вызова. Синтаксис прототипа этой функции описан так (поскольку это только прототип, то реальное имя может быть любым):
BOOL CALLBACK EnumWindowsProc (HWND hwnd LPARAM lParam);
Любым может быть и тип самой функции тип второго параметра, который разработчик может использовать по своему усмотрению, лишь бы его длина не превышала 32 бит.
Пример функции обратного вызова для случая, когда второй параметр имеет тип Longint, будет в Delphi выглядеть так:
function MyCallbackFunction (Wnd: Hwnd; P: Longint) :Bool; stdcall; begin
{ что-то делается } end;
var
MyVar: Longint;
EnumWindows(@MyCallbackFunction, Longint(MyVar));
Callback-функция (англ. call — вызов, англ. back — обратный) или функция обратного вызова в программировании — передача исполняемого кода в качестве одного из параметров другого кода.
См. также Разработка API (контракта) для своей DLL.
Введение в callback-функции
К примеру, если вы хотите установить таймер с использованием Windows API, вы можете вызвать функцию SetTimer
, передав в неё указатель на свою функцию, которая и будет callback-функцией. Система будет вызывать вашу функцию каждый раз, когда срабатывает таймер:
procedure MyTimerHandler(Wnd: HWND; uMsg: UINT; idEvent: UINT_PTR; dwTime: DWORD); stdcall; begin // Будет вызвана через 100 мс. end; procedure TForm1.Button1Click(Sender: TObject); begin SetTimer(Handle, 1, 100, @MyTimerHandler); end;
Вот ещё пример: если вы хотите найти все окна на рабочем столе, вы можете использовать функцию EnumWindows
:
function MyEnumFunc(Wnd: HWND; lpData: LPARAM): Bool; stdcall; begin // Вызывается для каждого найденного окна в системе end; procedure TForm1.Button1Click(Sender: TObject); begin EnumWindows(@MyEnumFunc, 0); end;
Поскольку функция обратного вызова обычно выполняет ту же задачу, что и код, который её устанавливает, то получается, что обоим кускам кода нужно работать с одними и теми же данными. Следовательно, данные от устанавливающего кода необходимо как-то передать в функцию обратного вызова. Для этой цели в функциях обратного вызова предусматриваются т.н. user-параметры: это либо указатель, либо целое число (обязательно типа Native(U)Int, но не (U)Int), который никак не используются самим API и прозрачно передаются в callback-функцию. Либо (в редких случаях) это может быть какое-то значение, уникально идентифицирующее вызов функции.
К примеру, в SetTimer
есть idEvent
, а в EnumWindows
есть lpData
. Мы можем использовать эти параметры, чтобы передать произвольные данные. Вот, к примеру, как можно найти все окна заданного класса:
type PEnumArgs = ^TEnumArgs; TEnumArgs = record ClassName: String; Windows: TStrings; end; function FindWindowsOfClass(Wnd: HWND; lpData: LPARAM): Bool; stdcall; var Args: PEnumArgs; WndClassName, WndText: String; begin Args := Pointer(lpData); SetLength(WndClassName, Length(Args.ClassName) + 2); SetLength(WndClassName, GetClassName(Wnd, PChar(WndClassName), Length(WndClassName))); if WndClassName = Args.ClassName then begin SetLength(WndText, GetWindowTextLength(Wnd) + 1); SetLength(WndText, GetWindowText(Wnd, PChar(WndText), Length(WndText))); Args.Windows.Add(Format('%8x : %s', [Wnd, WndText])); end; Result := True; end; procedure TForm1.Button1Click(Sender: TObject); var Args: TEnumArgs; begin // В Edit можно вводить значения типа // 'TForm1', 'IME', 'MSTaskListWClass', 'Shell_TrayWnd', 'TTOTAL_CMD', 'Chrome_WidgetWin_1' Args.ClassName := Edit1.Text; Args.Windows := Memo1.Lines; Memo1.Lines.BeginUpdate; try Memo1.Lines.Clear; EnumWindows(@FindWindowsOfClass, LPARAM(@Args)); finally Memo1.Lines.EndUpdate; end; end;
Примечание: неким аналогом user-параметров являются свойства Tag
и Data
, хотя их использование не всегда бывает идеологически верным (правильно: создать класс-наследник).
Static и callback-методы вместо callback-функций
Поскольку обычно современные приложения строятся как совокупность классов — было бы неплохо изолировать функцию обратного вызова: сделать её не глобальной, а членом класса. Это легко сделать следующим образом, используя статические классовые методы:
type TForm1 = class(TForm) Edit1: TEdit; Memo1: TMemo; Button1: TButton; // Переходник strict private type PInternalEnumArgs = ^TInternalEnumArgs; TInternalEnumArgs = record Self: TForm1; Data: Pointer; end; class function InternalEnumWindowsCallback(Wnd: HWND; lpData: LPARAM): Bool; stdcall; static; // Hi-level интерфейс protected function EnumWindowsCallback(const AWnd: HWND; const lpData: Pointer): Boolean; virtual; function EnumWindows(const lpData: Pointer = nil): Boolean; end; // ... function TForm1.EnumWindows(const lpData: Pointer): Boolean; var Args: TInternalEnumArgs; begin Args.Self := Self; Args.Data := lpData; Result := WinAPI.Windows.EnumWindows(@InternalEnumWindowsCallback, @Args); end; class function TForm1.InternalEnumWindowsCallback(Wnd: HWND; lpData: LPARAM): Bool; var Args: PInternalEnumArgs; begin Args := Pointer(lpData); Result := Args.Self.EnumWindowsCallback(Wnd, Args.Data); end; function TForm1.EnumWindowsCallback(const AWnd: HWND; const lpData: Pointer): Boolean; var WndClassName, WndText: String; begin // Ваш код - он может работать как с lpData, так и с членами класса. // К примеру: SetLength(WndClassName, Length(Edit1.Text) + 2); SetLength(WndClassName, GetClassName(Wnd, PChar(WndClassName), Length(WndClassName))); if WndClassName = Edit1.Text then begin SetLength(WndText, GetWindowTextLength(Wnd) + 1); SetLength(WndText, GetWindowText(Wnd, PChar(WndText), Length(WndText))); Memo1.Lines.Add(Format('%8x : %s', [Wnd, WndText])); end; Result := True; end;
(подробнее про ключевое слово static
можно почитать тут)
Здесь InternalEnumWindowsCallback
служит в качестве переходника-адаптера, который конвертирует функцию stdcall в метод класса. Настоящий же callback (который и выполняет всю работу) содержится в обычном методе класса EnumWindowsCallback
. Обратите внимание, что поскольку callback является методом, то он имеет доступ к свойствам и методам класса. Поэтому, как правило, callback-методы не используют user-параметры, а сами параметры читают напрямую из класса. Само собой, такой подход тем проще по сравнению с callback-функциями, чем больше параметров нужно передать и чем больше результата надо вывести — поскольку с callback-методами нам не нужно передавать каждое значение вручную через промежуточную запись. Тем не менее, в примере выше я сохранил параметр lpData
для свободного использования: не исключена ситуация, когда какие-либо данные будет проще передать через параметр, а не свойства класса — как правило, это бывают локальные данные, которые рассчитываются внутри метода класса, но не сохраняются в полях класса. А если бы нам не нужно было сохранять функциональность lpData
, то мы могли бы убрать тип TInternalEnumArgs
, передавая Self
напрямую как lpData
.
Если дизайн API разрабатывает не слишком опытный программист, он может просто не подумать про необходимость наличия user-параметров для callback-функций (а равно как и множество других вещей). В результате у вас может быть на руках такой код:
type TCallback = function(FoundData: TData): BOOL; cdecl; function RegisterCallback(Callback: TCallback): Integer; cdecl;
Как видите, здесь нет user-параметра, а единственный параметр для callback-функции представляет собственно данные для функции.
Решение в лоб: глобальные переменные
Само собой, поскольку разработчик API — не вы, то и изменить прототип функции обратного вызова вы не можете. Что же делать? Поскольку мы не можем передавать данные через локальный параметр, то остаётся только вариант с глобальным параметром:
var GMemo: TStrings; function MyCallback(FoundData: TData): BOOL; cdecl; begin GMemo.Lines.Add(DataToString(FoundData)); end; function TForm1.Button1Click(Sender: TObject); begin GMemo := Memo1; RegisterCallback(MyCallback); end;
Конечно, мы можем сделать callback-функцию членом класса, как мы делали это выше (с помощью статических классовых методов), но надо понимать, что в этом варианте API callback-метод должен быть классовым, а не обычным, а переменная тоже должна быть классовой, а не полем класса — поскольку мы не можем передать Self
через user-параметр. А следовательно, это будут всё те же глобальные функция и переменная, но немного замаскированные. Соответственно, обращаться к свойствам и методам класса из callback-а мы не сможем.
Данное решение можно рассматривать как «удовлетворительное», поскольку оно в принципе работает, но использует глобальные переменные — что плохо. Иногда это может быть допустимо, но часто нам необходимо вызывать callback-функции многопоточно или даже просто в несколько разных вызовов в рамках одного потока. В этом случае кажется, что надёжного способа идентификации нет?
Правильное решение: динамический код
Тем не менее, опытный программист может предложить гарантировано рабочий вариант, «добавляющий» user-параметр к существующему «плохому» API. Это вовсе не невыполнимая задача, как может показаться, но её решение достаточно нетривиально. Суть идеи в том, что user-параметр можно заменить на саму функцию обратного вызова, которая будет уникальна для каждого использования. Таким образом, вызов функции будет идентифицироваться не по параметру, а по тому, какая из функций вызвана.
Постановка задачи
Само собой, в предварительно скомпилированном файле невозможно иметь произвольное число функций для произвольных user-параметров. И именно поэтому это решение требует генерации кода на лету. К счастью, это не слишком сложная задача, поскольку вы можете воспользоваться услугами компилятора Delphi для генерации шаблона. Более того — вы можете даже не знать ассемблер. Но вам нужно иметь некоторое представление об устройстве памяти Windows.
Итак, пусть у нас есть следующее:
type TData = Integer; // просто для примера; это может быть что угодно: указатель, запись и т.п. TCallback = function(FoundData: TData): BOOL; cdecl; TRegisterCallbackFunc = function(Callback: TCallback): Integer; cdecl; TUnregisterCallbackFunc = procedure(Callback: TCallback); cdecl; var RegisterCallback: TRegisterCallbackFunc; // импортируется из DLL UnregisterCallback: TUnregisterCallbackFunc; // импортируется из DLL
Это — наш «плохой» API. Поскольку сторонний API обычно располагается в отдельной DLL, то я сделал пример с переменной-функцией, а не обычной функцией. В любом случае, если у вас есть обычная функция (API лежит в отдельном модуле, есть dcu, но не pas), то этот случай легко сводится к примеру выше. Итак, наша задача: добавить в этот API поддержку user-параметра.
Создание мастер-шаблона
Шаг первый: пишем следующий код:
function RealCallback(FoundData: TData; Data: Pointer): BOOL; cdecl; begin Result := True; end; type TRealCallbackFunc = function(FoundData: TData; Data: Pointer): BOOL; cdecl; var GRealCallback: TRealCallbackFunc = RealCallback; function InternalCallback(FoundData: TData): BOOL; cdecl; begin Result := GRealCallback(FoundData, Pointer($12345678)); end;
Здесь: InternalCallback
— это callback-функция, прототип которой полностью соответствует API. Именно её мы будем устанавливать в качестве callback-а. RealCallback
— это модифицированная callback-функция, которая отличается от API лишь наличием дополнительного параметра: это и есть наш user-параметр. Хотя прототип RealCallback
может быть произвольным, но для упрощения нашей жизни желательно, чтобы он был бы максимально похожим на InternalCallback
. Сама InternalCallback
должна просто вызывать RealCallback
, передавая фиксированный указатель в качестве параметра. Значение $12345678 выбрано по той простой причине, что его легко будет увидеть. Вы можете использовать любое другое «волшебное» значение.
Функция RealCallback
из InternalCallback
вызывается не напрямую, а опосредованно — через глобальную переменную GRealCallback
. Я поясню ниже зачем это сделано.
Итак, добавьте прямой вызов InternalCallback
в ваш код:
procedure TForm1.Button1Click(Sender: TObject); begin InternalCallback(0); // здесь: TData = Integer (для этого примера) end;
и установите на него точку останова. Запустите проект, нажмите кнопку, встаньте на точку останова, зайдите в функцию InternalCallback
(F7) и откройте CPU-отладчик (Ctrl + Alt + C или View / Debug Windows / CPU View). Вы увидите такой код:
Unit1.pas.38: begin 005B5C7C 55 push ebp 005B5C7D 8BEC mov ebp,esp 005B5C7F 51 push ecx Unit1.pas.39: Result := GRealCallback(FoundData, Pointer($12345678)); 005B5C80 6878563412 push $12345678 005B5C85 8B4508 mov eax,[ebp+$08] 005B5C88 50 push eax 005B5C89 FF15A0395C00 call dword ptr [$005c39a0] 005B5C8F 83C408 add esp,$08 005B5C92 8945FC mov [ebp-$04],eax Unit1.pas.40: end; 005B5C95 8B45FC mov eax,[ebp-$04] 005B5C98 59 pop ecx 005B5C99 5D pop ebp 005B5C9A C3 ret
Выделите этот код и скопируйте куда-нибудь (не нужно его запускать!). Вы можете увидеть, что весь код состоит из трёх строк: begin (он же — пролог), вызов функции, end (он же — эпилог). Каждая строка помечена комментарием. Если вы не знаете ассемблер, то всё, что вам нужно знать: первый столбец это адреса инструкций. Эти адреса принадлежат вашему exe (ведь именно код exe мы вызвали по Button1Click
). Второй столбец: hex-коды байтов кода. Т.е. это машинный код в чистом виде. Третий столбец — это ассемблерный код, соответствующий машинному коду. Самое замечательное в этом листинге — нам не нужен ассемблерный код, нам нужен лишь машинный код. Сейчас я поясню почему…
Теперь смотрим: в этой функции есть всего два переменных значения:
- Адрес вызываемой функции (
RealCallback
) - Значение user-параметра
Весь остальной текст статичен и не зависит ни он чего, т.е. он будет ровно тем же самым в любых случаях: для любых user-параметров, для любых callback-функций. Это означает, что если мы хотим сами генерировать функции, подобные InternalCallback
, то мы можем просто скопировать весь код целиком и просто подставить в него два числа: адрес функции и адрес параметра.
User-параметр легко увидеть, поскольку мы использовали волшебное значение $12345678. Адрес функции увидеть сложнее (если вы не знакомы с ассемблером), но можно догадаться, что он зашифрован в этой строке:
005B5C89 FF15A0395C00 call dword ptr [$005c39a0]
Почему?
- Слово call намекает на «вызов».
- Адрес $005C39A0 явно лежит недалеко от адресов $005B5C7C-$005B5C9A, т.е. это какой-то код в exe.
Поскольку наш Паскаль-код вызывает функцию не напрямую, а через глобальную переменную, то легко предположить, что $005C39A0 — это адрес не самой функции, а адрес указателя на функцию.
Примечание: вот почему я использовал конструкцию с опосредованным вызовом функции вместо прямого: потому что в этой конструкции вызов задаётся как «вызвать функцию по этому адресу». Здесь явно присутствует «этот адрес» — что означает, что его можно легко поменять. Если бы вызов был прямым, то машинный код говорил бы «вызвать функцию, которая лежит перед этой через N байтов». Задавать адрес функции в таком варианте было бы намного сложнее.
Вот и всё, что касается разбора шаблона. Обратите внимание, что мы не использовали никаких особенных знаний, кроме умения пользоваться отладчиком и здравого смысла.
Тогда приступаем к кодированию. Для начала надо скопировать наш шаблон в массив байтов для дальнейшего использования в коде программы. Для этого я сначала выписал весь машинный код (второй столбец), одновременно заменив адрес указателя на функцию на (новое) волшебное значение для упрощения отладки:
55 8BEC 51 6878563412 // $12345678 - user-параметр 8B4508 50 FF15FFEEDDCC // $CCDDEEFF - указатель на функцию 83C408 8945FC 8B45FC 59 5D C3
Напоминаю, что не нужно руками выписывать эти коды — выделите в CPU-отладчике мышью нужные строки и нажмите Ctrl + C — это скопирует выделенные строки в буфер обмена. Затем перейдите в редактор кода и вставьте текст из буфера, после чего удалите три строки с комментариями, а также первый и последний столбцы, оставив только машинный код.
Обратите внимание, что x86 является little-endian архитектурой — что означает, что все числа «записываются наоборот».
После этого я убрал разделители строк, объединив весь машинный код в длинный поток байтов:
558BEC5168785634128B450850FF15FFEEDDCC 83C4088945FC8B45FC595DC3
(я разбил на две строки для упрощения читабельности в блоге, в редакторе это одна строка)
После чего я вставил #$ через каждые два символа и оформил эту константу как строку:
const CallbackTemplate: RawByteString = #$55#$8B#$EC#$51#$68#$78#$56#$34#$12#$8B#$45#$08#$50#$FF#$15#$FF#$EE#$DD#$CC + #$83#$C4#$08#$89#$45#$FC#$8B#$45#$FC#$59#$5D#$C3;
Почему строку? Ну, это проще и короче, чем настоящий массив байтов: не надо вставлять пробелы, запятые, не надо указывать размерность массива. Кроме того, для строк есть готовая функция поиска и замены (мы увидим позднее почему это важно). Строка однобайтовая, без кодировки (RawByteString
— это AnsiString
в старых версиях Delphi) — поэтому эта строка является массивом байтов.
Теперь неплохо бы проверить, что мы нигде не ошиблись. Измените обработчик нажатия на кнопку следующим образом:
procedure TForm1.Button1Click(Sender: TObject); begin TCallback(Pointer(CallbackTemplate))(0); end;
Этой странной строкой мы говорим, что указатель CallbackTemplate
(а любая строка — это указатель; для статического массива вам потребовалось бы брать указатель явно через @) следует трактовать не как строку, а как функцию типа TCallback
. Ну и эту функцию, стало быть, надо вызвать. Вот более длинная версия того же кода:
procedure TForm1.Button1Click(Sender: TObject); var Template: AnsiString; Ptr: Pointer; CB: TCallback; begin Template := CallbackTemplate; Ptr := Pointer(Template); CB := TCallback(Ptr); CB(0); end;
Установите точку останова на вызов функции (в любом варианте — длинном или коротком), запустите программу, нажмите кнопку, встаньте на точке останова. Не нажимайте F7: поскольку для функции закодированной в CallbackTemplate
отсутствует исходный код, то компилятор выполнит всю функцию целиком за один проход — что приведёт к Access Violation, поскольку оба наших указателя ($12345678 и $CCDDEEFF) указывают в космос. Вместо этого вызовите CPU отладчик (Ctrl + Alt + C или View / Debug Windows / CPU View) и несколько раз нажмите F7 уже в нём — пока вас не перебросит к знакомому коду:
005B5C74 55 push ebp 005B5C75 8BEC mov ebp,esp 005B5C77 51 push ecx 005B5C78 6878563412 push $12345678 005B5C7D 8B4508 mov eax,[ebp+$08] 005B5C80 50 push eax 005B5C81 FF15FFEEDDCC call dword ptr [$ccddeeff] 005B5C87 83C408 add esp,$08 005B5C8A 8945FC mov [ebp-$04],eax 005B5C8D 8B45FC mov eax,[ebp-$04] 005B5C90 59 pop ecx 005B5C91 5D pop ebp 005B5C92 C3 ret
Убедитесь, что этот фрагмент точно совпадает с исходным кодом (разве что с изменённым адресом на $CCDDEEFF). Если совпадает, то вы всё сделали верно, шаблон готов. Если нет — исправьте и будьте в дальнейшем более внимательны.
Динамическая генерация кода
Следующий шаг — нам необходимо создавать реальные функции обратного вызова (с реальными адресами) по шаблону CallbackTemplate
. Собственно, сделать это очень просто — достаточно просто заменить адреса функций и код готов. Есть только небольшая особенность: в архитектуре x86 любой исполняемый код должен располагаться в странице памяти, имеющей атрибут выполнения (EXECUTE). Если мы просто выделим память (GetMem
/AllocMem
или просто используем строку, массив и другие данные), то это будут «данные»: у них будет доступ на чтение (READ), запись (WRITE), но не выполнение (EXECUTE). Поэтому попытка вызова этого кода приведёт к Access Violation.
Примечание: на ранних процессорах архитектуры x86 атрибуты «чтение» и «выполнение» были эквивалентными. Поэтому, хотя технически ставить равенство между ними никогда не было верным, некоторые воспользовались этой особенностью реализации и передавали управление на код в сегментах данных. Повторим: это никогда не было корректным, но это работало на старых процессорах. Теперь этот код будет вылетать. См. также: DEP.
Вспомним как работает менеджер памяти: он дробит страницы памяти на блоки, которые программа «выделяет» из памяти. Отсюда следует, что мы не можем просто взять память средствами языка Delphi и изменить ей атрибуты: эта память будет расположена в одной странице с какими-то другими данными, и, меняя доступ к странице, мы поменяем доступ к каким-то ещё данным. По этой причине нам необходимо выделять память напрямую у системы, минуя посредников.
Суммируя сказанное, вот подходящий код:
unit FixCallbacks; interface // Функция динамически генерирует код для вызова // функции ACallback с параметром AUserParam по известному шаблону машинного кода ATemplate // Шаблон ATemplate должен использовать значения $12345678 и $CCDDEEFF как заглушки // для передачи AUser и указателя на ACallback соответственно // Результат работы функции можно передавать в "плохой" API function AllocTemplate(const ATemplate: RawByteString; const ACallback, AUserParam: Pointer): Pointer; // Функция освобождает шаблон, созданный функцией AllocTemplate // В неё необходимо передать те же параметры, что и в AllocTemplate // Функция вернёт указатель, который необходимо передать в функцию дерегистрации "плохого" API function DisposeTemplate(const ACallback, AUser: Pointer): Pointer; implementation uses Winapi.Windows, System.SysUtils, System.Classes; var // Список всех динамически сгенерированных кусков кода KnownTemplates: TThreadList; function AllocTemplate(const ATemplate: RawByteString; const ACallback, AUserParam: Pointer): Pointer; procedure StrReplace(var ATemplate: RawByteString; const ASource, ADest: NativeUInt); var X: Integer; begin for X := 1 to Length(ATemplate) - SizeOf(ASource) do if PNativeUInt(@ATemplate[X])^ = ASource then begin PNativeUInt(@ATemplate[X])^ := ADest; Break; end; end; var OrgPtr: Pointer; OrgSize: Cardinal; Ptr: PPointer; ActualTemplate: RawByteString; Dummy: Cardinal; begin // Шаг первый: выделяем ресурсы // Взяли шаблон ActualTemplate := ATemplate; // Выделили память для динамической генерации кода // Добавили размер указателя, т.к. нам нужно куда-то сохранять указатель на ACallback // Второй указатель нужен для удобства: в него мы сохраним user-параметр (см. DisposeTemplate ниже) // Атрибуты страницы: чтение + запись - поскольку сначала нам нужно записать туда код OrgSize := Length(ATemplate) + SizeOf(Pointer) * 2; OrgPtr := VirtualAlloc(nil, OrgSize, MEM_COMMIT or MEM_RESERVE, PAGE_READWRITE); Win32Check(Assigned(OrgPtr)); Ptr := OrgPtr; // Шаг второй: готовим данные // Подменяем в шаблоне заглушки на реальные данные // Блок начнётся с самого указателя на функцию, а машинный код пойдёт за ним, // поэтому в качестве "указателя на указатель на функцию" будет выступать сам Ptr StrReplace(ActualTemplate, $12345678, NativeUInt(AUserParam)); StrReplace(ActualTemplate, $CCDDEEFF, NativeUInt(Ptr)); // Шаг третий: копируем данные // Раз блок начинается с указателя на функцию, то сначала сохраняем указатель Ptr^ := ACallback; Inc(Ptr); // Затем - user-параметр Ptr^ := AUserParam; Inc(Ptr); // Наконец, копируем готовый машинный код в место его выполнения (сразу за указателем) Move(Pointer(ActualTemplate)^, Ptr^, Length(ActualTemplate)); // Шаг четвёртый: делаем код кодом // Меняем атрибуты страницы на "чтение + выполнение" // "Чтение" необходимо по той причине, что мы храним в этом же блоке указатель на функцию - // а его (указатель) читает код. Соответственно, без доступа на чтение код вылетит с AV. Win32Check(VirtualProtect(OrgPtr, OrgSize, PAGE_EXECUTE_READ, Dummy)); // Не забываем про многоядерные процессоры: надо указать, что мы модифицировали исполняемый код Win32Check(FlushInstructionCache(GetCurrentProcess, OrgPtr, OrgSize)); // Шаг пятый: возвращаем результат Result := Ptr; // Сохраняем результат в списке сгенерированных callback-функций. // Это необходимо, чтобы DisposeTemplate могла найти динамический шаблон по статическому KnownTemplates.Add(Ptr); end; function DisposeTemplate(const ACallback, AUser: Pointer): Pointer; // AllocTemplate возвращает указатель на машинный код - // что на SizeOf(Pointer) * 2 ( = SizeOf(TSavedTemplate)) байт дальше начала блока памяти. // Соответственно, нам сначала нужно найти реальный указатель блока памяти. type PSavedTemplate = ^TSavedTemplate; TSavedTemplate = packed record Callback, User: Pointer; end; var List: TList; X: Integer; SavedTemplate: PSavedTemplate; begin Result := nil; if ACallback = nil then Exit; SavedTemplate := nil; // Ищем динамический шаблон по статическому // Необходимо заблокировать список на время работы, т.к. у нас не атомарная операция List := KnownTemplates.LockList; try for X := 0 to List.Count - 1 do begin SavedTemplate := List[X]; // Ещё раз: мы храним сдвинутые указатели, начало блока находится на два указателя ранее Dec(SavedTemplate); // Нашли шаблон? if (SavedTemplate.Callback = ACallback) and (SavedTemplate.User = AUser) then begin // Вернём указатель на динамический шаблон для его разрегистрации Result := List[X]; // Сначала удаляем его из списка... List.Delete(X); Break; end else SavedTemplate := nil; end; finally KnownTemplates.UnlockList; end; if SavedTemplate = nil then Exit; // ...а затем - освобождаем память Win32Check(VirtualFree(SavedTemplate, 0, MEM_RELEASE)); end; initialization KnownTemplates := TThreadList.Create; finalization FreeAndNil(KnownTemplates); end.
Данный модуль предлагает две универсальные функции для динамической генерации кода переходников callback-функций. Логика кода достаточно понятно расписана в комментариях, поэтому я не буду её описывать отдельно.
С этими функциями мы можем переделать интерфейсный модуль для «плохого» API следующим образом:
// ... interface // ... type TData = Integer; // осталось как было // Новый тип - как замена TCallback, но с добавленным lpUser TRealCallback = function(FoundData: TData; const lpUser: Pointer): BOOL; cdecl; // Новые функции - как замена старых, но с добавленным lpUser function RegisterCallback(const Callback: TRealCallback; const lpUser: Pointer): Integer; procedure UnregisterCallback(const Callback: TRealCallback; const lpUser: Pointer); implementation uses FixCallbacks; // подключаем "волшебные" функции // Старые декларации скрыли в implementation type TCallback = function(FoundData: TData): BOOL; cdecl; TRegisterCallbackFunc = function(Callback: TCallback): Integer; cdecl; TUnregisterCallbackFunc = procedure(Callback: TCallback); cdecl; var InternalRegisterCallback: TRegisterCallbackFunc; InternalUnregisterCallback: TUnregisterCallbackFunc; function RegisterCallback(const Callback: TRealCallback; const lpUser: Pointer): Integer; var UniqueCallback: TCallback; begin UniqueCallback := TCallback(AllocTemplate(CallbackTemplate, @Callback, lpUser)); Result := InternalRegisterCallback(UniqueCallback); end; procedure UnregisterCallback(const Callback: TRealCallback; const lpUser: Pointer); begin InternalUnregisterCallback(TCallback(DisposeTemplate(@Callback, lpUser))); end; // ...
И тогда мы можем написать такой код:
function MyCallback(FoundData: TData; const lpUser: Pointer): BOOL; cdecl; var Self: TForm1; begin Self := TForm1(lpUser); Self.Memo1.Lines.Add(IntToStr(FoundData)); Result := True; end; procedure TForm1.Button1Click(Sender: TObject); begin RegisterCallback(MyCallback, Pointer(Self)); end; procedure TForm1.Button2Click(Sender: TObject); begin UnregisterCallback(MyCallback, Pointer(Self)); end;
Вуаля! Магия!
Обратите внимание, что функции AllocTemplate
и DisposeTemplate
являются универсальными и никак не зависят от вашего кода. Чтобы проиллюстрировать эту универсальность — давайте перепишем наш пример поиска окон заданного класса через EnumWindows
так, чтобы он не использовал бы user-параметр функции EnumWindows
. Для этого нам нужно составить шаблон. Пишем код:
function RealFindWindowsOfClass(Wnd: HWND; lpData: LPARAM; User: Pointer): Bool; stdcall; begin Result := True; end; type TFindWindowsOfClassFunc = function(Wnd: HWND; lpData: LPARAM; User: Pointer): Bool; stdcall; var GFindWindowsOfClass: TFindWindowsOfClassFunc = RealFindWindowsOfClass; function FindWindowsOfClass(Wnd: HWND; lpData: LPARAM): Bool; stdcall; begin Result := GFindWindowsOfClass(Wnd, lpData, Pointer($12345678)); end; procedure TForm1.Button1Click(Sender: TObject); begin FindWindowsOfClass(0, 0); end;
Получаем такой ассемблерный листинг:
Unit44.pas.40: begin 005B5CE4 55 push ebp 005B5CE5 8BEC mov ebp,esp 005B5CE7 51 push ecx Unit44.pas.41: Result := GFindWindowsOfClass(Wnd, lpData, Pointer($12345678)); 005B5CE8 6878563412 push $12345678 005B5CED 8B450C mov eax,[ebp+$0c] 005B5CF0 50 push eax 005B5CF1 8B4508 mov eax,[ebp+$08] 005B5CF4 50 push eax 005B5CF5 FF15A0395C00 call dword ptr [$005c39a0] 005B5CFB 8945FC mov [ebp-$04],eax Unit44.pas.42: end; 005B5CFE 8B45FC mov eax,[ebp-$04] 005B5D01 59 pop ecx 005B5D02 5D pop ebp 005B5D03 C20800 ret $0008
Вырезаем машинный код и делаем его константой (не забыв заменить 005C39A0 на CCDDEEFF):
const EnumWndTemplate: RawByteString = #$55#$8B#$EC#$51#$68#$78#$56#$34#$12#$8B#$45#$0C#$50#$8B#$45#$08#$50#$FF#$15#$FF#$EE#$DD#$CC + #$89#$45#$FC#$8B#$45#$FC#$59#$5D#$C2#$08#$00;
Всё готово, можно вызывать:
function FindWindowsOfClass(Wnd: HWND; lpData: LPARAM; User: Pointer): Bool; stdcall; var Self: TForm1; WndClassName, WndText: String; begin Self := TForm1(User); SetLength(WndClassName, Length(Self.Edit1.Text) + 2); SetLength(WndClassName, GetClassName(Wnd, PChar(WndClassName), Length(WndClassName))); if WndClassName = Self.Edit1.Text then begin SetLength(WndText, GetWindowTextLength(Wnd) + 1); SetLength(WndText, GetWindowText(Wnd, PChar(WndText), Length(WndText))); Self.Memo1.Lines.Add(Format('%8x : %s', [Wnd, WndText])); end; Result := True; end; procedure TForm1.Button1Click(Sender: TObject); const EnumWndTemplate: RawByteString = #$55#$8B#$EC#$51#$68#$78#$56#$34#$12#$8B#$45#$0C#$50#$8B#$45#$08#$50#$FF#$15#$FF#$EE#$DD#$CC + #$89#$45#$FC#$8B#$45#$FC#$59#$5D#$C2#$08#$00; var CustomizedCallback: Pointer; begin CustomizedCallback := AllocTemplate(EnumWndTemplate, @FindWindowsOfClass, Pointer(Self)); try Memo1.Lines.BeginUpdate; try Memo1.Lines.Clear; EnumWindows(CustomizedCallback, 0); finally Memo1.Lines.EndUpdate; end; finally DisposeTemplate(@FindWindowsOfClass, Pointer(Self)); end; end;
Вот и всё, что я хотел сегодня сказать.
ВАЖНОЕ ПРИМЕЧАНИЕ
Подход, описанный в этой статье, является хаком. Это означает, что это — костыль, обходной путь, способ заставить код работать хоть как-то. Это не оправдание для написания «плохого» кода API. Если у вас есть контроль над прототипами callback-функций — измените их! Введите поддержку user-параметров.
См. также Разработка API (контракта) для своей DLL.
P.S. Глобальный список в KnownTemplates
не является обязательным. Вполне можно обойтись без него. Для этого вызывающий должен сохранять результат вызова AllocTemplate
, а затем передавать его в DisposeTemplate
(вместо тех же аргументов, как это сделано сейчас). Тогда список был бы не нужен, потому что DisposeTemplate
смогла бы извлечь указатель на освобождаемый блок из своего аргумента. Этот сценарий удобен в случае кода как в примере с EnumWindows
: нам достаточно передать CustomizedCallback
в DisposeTemplate
— и это совершенно не накладно. Почему же я сделал список? Потому что такой подход накладывает обязанность хранения указателя на вызывающего. Посмотрите сценарий с интерфейсным модулей для «плохого» API. Если мы хотим в точности сохранить интерфейс кода, то мы не можем передать вызывающему указатель. Вот почему нам и потребовался список. Если же вы готовы пойти на изменение интерфейса заголовочных модулей «плохого» API, то вы вполне можете отказаться от списка. Тогда DisposeTemplate
будет выглядеть так:
// ATemplate - указатель, возвращённый функцией AllocTemplate procedure DisposeTemplate(ATemplate: Pointer); begin if ATemplate = nil then Exit; ATemplate := Pointer(NativeUInt(ATemplate) - SizeOf(Pointer) * 2); if ATemplate = nil then Exit; Win32Check(VirtualFree(ATemplate, 0, MEM_RELEASE)); end;
А из функции AllocTemplate
надо будет убрать строку с добавлением в KnownTemplates
, после чего все упоминания KnownTemplates
можно также удалить (а заодно — удалить модуль Classes
из uses
).
45 4.2. Функция окна Все Windows-программы должны содержать специальную функцию, которая вызывается не самой программой, а операционной системой, когда Windows передает сообщение программе. Эту функцию назьшают функцией окна, или процедурой окна Согласно терминологии Windows, функции, вызываемые системой, называются функциями обратного вызова. Именно через нее осуществляется взаимодействие между программой и системой. Имя функции окна может быть любое, например, WindowsFunc, тип функции — LRESULT CALLBACK. Первое слово LRESULT означает, что функция должна вернуть в качестве результата длинное целое (long) — код завершения, а CALLBACK означает, что это функция обратного вызова. LRESULT CALLBACK WindowsFunс(HWND hWnd, UINT message, WPAFAM wParam, LPARAM IParam) Функция окна имеет 4 параметра, которые характеризуют передаваемое сообщение: дескриптор окна, тип сообщения, последние два зависят от типа сообщения. В этой функции должна быть реализована обработка всех сообщений Windows. Обычно она состоит из оператора switch, в котором на каждое сообщение предусмотрена соответствующая реакция. 4.3. Сообщения Windows Каждое сообщение характеризуется 4 параметрами: дескриптор окна, тип сообщения и два дополнительных параметра, которые зависят от типа сообщения. Первый параметр window handle — это дескриптор окна, которому адресовано сообщение. Он представляет собой уникальный номер, идентифицирующий окно. Второй параметр определяет тип сообщения — message type. Тип сообщения — это один из идентификаторов, определенных в заголовочных файлах Windows. Идентификаторы начинаются с префикса WM_ (Windows Message). Наиболее часто посылаемые сообщения: WM DESTROY (при закрытии окна), WMPAINT (когда окно требует обновления), WM COMMAND (при выборе команды меню), WM CHAR (при нажатии клавиши клавиатуры), WM LBUTTONDOWN (при нажатии левой кнопки мыши), WM SIZE (при изменении размеров окна) и др. Последние два параметра содержат дополнительную информацию, необходимую для интерпретации сообщения, например, для сообщений WM LBUTTONDOWN и WM RBUTTONDOWN, передаются координаты курсора мыши, для WM CHAR — код клавиши, WM COMMAND — идентификатор выбранного пункта меню. Когда сообщение посылается окну программы, все перечисленные параметры сообщения передаются функции окна.
Made with FlippingBook
RkJQdWJsaXNoZXIy MTY0OTYy
From Wikipedia, the free encyclopedia
In computer programming, a callback is a function that is stored as data (a reference) and designed to be called by another function – often back to the original abstraction layer.
A function that accepts a callback parameter may be designed to call back before returning to its caller which is known as synchronous or blocking. The function that accepts a callback may be designed to store the callback so that it can be called back after returning which is known as asynchronous, non-blocking or deferred.
Programming languages support callbacks in different ways such as function pointers, lambda expressions and blocks.
To aid understanding the concept, the following is an analogy from real life.
A customer visits a store to place an order. This is like the first call.
The customer gives to a clerk a list of items, a check to cover their cost and delivery instructions. These are the parameters of the first call including the callback which is the delivery instructions. It is understood that the check will be cashed and that the instructions will be followed.
When the staff are able, they deliver the items as instructed which is like calling the callback.
Notably, the delivery need not be made by the clerk who took the order. A callback need not be called by the function that accepted the callback as a parameter.
Also, the delivery need not be made directly to the customer. A callback need not be to the calling function. In fact, a function would generally not pass itself as a callback.
Some find the use of back to be misleading since the call is (generally) not back to the original caller as it is for a telephone call.
A blocking callback runs in the execution context of the function that passes the callback. A deferred callback can run in a different context such as during interrupt or from a thread. As such, a deferred callback can be used for synchronization and delegating work to another thread.
A callback can be used for event handling. Often, consuming code registers a callback for a particular type of event. When that event occurs, the callback is called.
Callbacks are often used to program the graphical user interface (GUI) of a program that runs in a windowing system. The application supplies a reference to a custom callback function for the windowing system to call. The windowing system calls this function to notify the application of events like mouse clicks and key presses.
Asynchronous action
[edit]
A callback can be used to implement asynchronous processing. A caller requests an action and provides a callback to be called when the action completes which might be long after the request is made.
A callback can be used to implement polymorphism. In the following pseudocode, say_hi
can take either write_status
or write_error
.
def write_status(string message): write(stdout, message) def write_error(string message): write(stderr, message) def say_hi(write): write("Hello world")
The callback technology is implemented differently by programming language.
In assembly, C, C++, Pascal, Modula2 and other languages, a callback function is stored internally as a function pointer. Using the same storage allows different languages to directly share callbacks without a design-time or runtime interoperability layer. For example, the Windows API is accessible via multiple languages, compilers and assemblers.
C++ also allows objects to provide an implementation of the function call operation. The Standard Template Library accepts these objects (called functors) as parameters.
Many dynamic languages, such as JavaScript, Lua, Python, Perl[1][2] and PHP, allow a function object to be passed.
CLI languages such as C# and VB.NET provide a type-safe encapsulating function reference known as delegate.
Events and event handlers, as used in .NET languages, provide for callbacks.
Functional languages generally support first-class functions, which can be passed as callbacks to other functions, stored as data or returned from functions.
Many languages, including Perl, Python, Ruby, Smalltalk, C++ (11+), C# and VB.NET (new versions) and most functional languages, support lambda expressions, unnamed functions with inline syntax, that generally acts as callbacks..
In some languages, including Scheme, ML, JavaScript, Perl, Python, Smalltalk, PHP (since 5.3.0),[3] C++ (11+), Java (since 8),[4] and many others, a lambda can be a closure, i.e. can access variables locally defined in the context in which the lambda is defined.
In an object-oriented programming language such as Java versions before function-valued arguments, the behavior of a callback can be achieved by passing an object that implements an interface. The methods of this object are callbacks.
In PL/I and ALGOL 60 a callback procedure may need to be able to access local variables in containing blocks, so it is called through an entry variable containing both the entry point and context information. [5]
Callbacks have a wide variety of uses, for example in error signaling: a Unix program might not want to terminate immediately when it receives SIGTERM, so to make sure that its termination is handled properly, it would register the cleanup function as a callback. Callbacks may also be used to control whether a function acts or not: Xlib allows custom predicates to be specified to determine whether a program wishes to handle an event.
In the following C code, function print_number
uses parameter get_number
as a blocking callback. print_number
is called with get_answer_to_most_important_question
which acts as a callback function. When run the output is: «Value: 42».
#include <stdio.h> #include <stdlib.h> void print_number(int (*get_number)(void)) { int val = get_number(); printf("Value: %d\n", val); } int get_answer_to_most_important_question(void) { return 42; } int main(void) { print_number(get_answer_to_most_important_question); return 0; }
In C++, functor can be used in addition to function pointer.
In the following C# code,
method Helper.Method
uses parameter callback
as a blocking callback. Helper.Method
is called with Log
which acts as a callback function. When run, the following is written to the console: «Callback was: Hello world».
public class MainClass { static void Main(string[] args) { Helper helper = new Helper(); helper.Method(Log); } static void Log(string str) { Console.WriteLine($"Callback was: {str}"); } } public class Helper { public void Method(Action<string> callback) { callback("Hello world"); } }
In the following Kotlin code, function askAndAnswer
uses parameter getAnswer
as a blocking callback. askAndAnswer
is called with getAnswerToMostImportantQuestion
which acts as a callback function. Running this will tell the user that the answer to their question is «42».
fun main() { print("Enter the most important question: ") val question = readLine() askAndAnswer(question, ::getAnswerToMostImportantQuestion) } fun getAnswerToMostImportantQuestion(): Int { return 42 } fun askAndAnswer(question: String?, getAnswer: () -> Int) { println("Question: $question") println("Answer: ${getAnswer()}") }
In the following JavaScript code, function calculate
uses parameter operate
as a blocking callback. calculate
is called with multiply
and then with sum
which act as callback functions.
function calculate(a, b, operate) { return operate(a, b); } function multiply(a, b) { return a * b; } function sum(a, b) { return a + b; } // outputs 20 alert(calculate(10, 2, multiply)); // outputs 12 alert(calculate(10, 2, sum));
The collection method .each()
of the jQuery library uses the function passed to it as a blocking callback. It calls the callback for each item of the collection. For example:
$("li").each(function(index) { console.log(index + ": " + $(this).text()); });
Deferred callbacks are commonly used for handling events from the user, the client and timers. Examples can be found in addEventListener
, Ajax and XMLHttpRequest
.
[6]
In addition to using callbacks in JavaScript source code, C functions that take a function are supported via js-ctypes.[7]
The following REBOL/Red code demonstrates callback use.
- As alert requires a string, form produces a string from the result of calculate
- The get-word! values (i.e., :calc-product and :calc-sum) trigger the interpreter to return the code of the function rather than evaluate with the function.
- The datatype! references in a block! [float! integer!] restrict the type of values passed as arguments.
Red [Title: "Callback example"] calculate: func [ num1 [number!] num2 [number!] callback-function [function!] ][ callback-function num1 num2 ] calc-product: func [ num1 [number!] num2 [number!] ][ num1 * num2 ] calc-sum: func [ num1 [number!] num2 [number!] ][ num1 + num2 ] ; alerts 75, the product of 5 and 15 alert form calculate 5 15 :calc-product ; alerts 20, the sum of 5 and 15 alert form calculate 5 15 :calc-sum
Rust have the Fn
, FnMut
and FnOnce
traits.[8]
fn call_with_one<F>(func: F) -> usize where F: Fn(usize) -> usize { func(1) } let double = |x| x * 2; assert_eq!(call_with_one(double), 2);
In this Lua code, function calculate
accepts the operation
parameter which is used as a blocking callback. calculate
is called with both add
and multiply
, and then uses an anonymous function to divide.
function calculate(a, b, operation) return operation(a, b) end function multiply(a, b) return a * b end function add(a, b) return a + b end print(calculate(10, 20, multiply)) -- outputs 200 print(calculate(10, 20, add)) -- outputs 30 -- an example of a callback using an anonymous function print(calculate(10, 20, function(a, b) return a / b -- outputs 0.5 end))
In the following Python code, function calculate
accepts a parameter operate
that is used as a blocking callback. calculate
is called with square
which acts as a callback function.
def square(val): return val ** 2 def calculate(operate, val): return operate(val) # outputs: 25 calculate(square, 5)
In the following Julia code, function calculate
accepts a parameter operate
that is used as a blocking callback. calculate
is called with square
which acts as a callback function.
julia> square(val) = val^2 square (generic function with 1 method) julia> calculate(operate, val) = operate(val) calculate (generic function with 1 method) julia> calculate(square, 5) 25
- Command pattern
- Continuation-passing style
- Event loop
- Event-driven programming
- Implicit invocation
- Inversion of control
- libsigc++, a callback library for C++
- Signals and slots
- User exit
- ^ «Perl Cookbook — 11.4. Taking References to Functions». 2 July 1999. Retrieved 2008-03-03.
- ^ «Advanced Perl Programming — 4.2 Using Subroutine References». 2 July 1999. Retrieved 2008-03-03.
- ^ «PHP Language Reference — Anonymous functions». Retrieved 2011-06-08.
- ^ «What’s New in JDK 8». oracle.com.
- ^ Belzer, Jack; Holzman, Albert G; Kent, Allen, eds. (1979). Encyclopedia of Computer Science and Technology: Volume 12. Marcel Dekker, inc. p. 164. ISBN 0-8247-2262-0. Retrieved January 28, 2024.
- ^ «Creating JavaScript callbacks in components». Archive. UDN Web Docs (Documentation page). sec. JavaScript functions as callbacks. Archived from the original on 2021-12-16. Retrieved 2021-12-16.
- ^ Holley, Bobby; Shepherd, Eric (eds.). «Declaring and Using Callbacks». Docs. Mozilla Developer Network (Documentation page). Archived from the original on 2019-01-17. Retrieved 2021-12-16.
- ^ «Fn in std::ops — Rust». doc.rust-lang.org. Retrieved 18 January 2025.
- Basic Instincts: Implementing Callback Notifications Using Delegates — MSDN Magazine, December 2002
- Implement callback routines in Java
- Implement Script Callback Framework in ASP.NET 1.x — Code Project, 2 August 2004
- Interfacing C++ member functions with C libraries (archived from the original on July 6, 2011)
- Style Case Study #2: Generic Callbacks
Что это за тип функции такой CALLBACK? Обычно такие функции называются функциями обратного вызова. Это просто. Если Вашу функцию должен вызывать Windows, то вы должны указать ей тип передачи параметров как CALLBACK. Этот тип вызова описан в WinDef.H как:
#define CALLBACK __stdcall
То есть тип передачи параметров PASCAL. Обычный вызов функций осуществляется в стиле WIN32 API. Как же Windows узнает, что эту функцию можно выполнить ? Вы сами, зная то или нет, передаете ее в параметрах. Если вы создаете окно в Win 32, то и передаете функцию окна. Windows эту функцию вызывает когда управляет окном. Все просто. Сказали системе, что если нужно обратиться к окну вот тебе функция. После этого Windows знает, что если нужно перерисовать окно, то он хвать эту функцию и передает ей в параметры WM_PAINT. Идея довольно простая. Операционная система должна уметь вызывать некоторые функции в приложении, чтобы освободить вас как программиста от слежения за программой. Кто писал для ДОС знает как это не удобно думать о том, какое окно видно на экране, а какое нет. Пусть операционная система заботится. Итак, в любой программе для Windows (кстати и не только в понимании графического интерфейса) есть функции, которые вызовет операционная система. Как пример главная функция окна. Эта функция должна быть в программе правильно оформлена, а именно CALLBACK. Обычно мы ее передаем в виде параметров при вызове фнукций WIN32 API.