PDA

Просмотр полной версии : [Урок] Перехват функций, часть 1: основы



Daniel_Cortez
25.09.2014, 18:25
Оглавление:
Часть 1: основы
Часть 2: практика - пишем античит на HP (http://pro-pawn.ru/showthread.php?10611-%D0%9F%D0%B5%D1%80%D0%B5%D1%85%D0%B2%D0%B0%D1%82-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B9-%D1%87%D0%B0%D1%81%D1%82%D1%8C-2-%D0%BF%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B0-%D0%BF%D0%B8%D1%88%D0%B5%D0%BC-%D0%B0%D0%BD%D1%82%D0%B8%D1%87%D0%B8%D1%82-%D0%BD%D0%B0-HP)

Внимание! Эта тема закрыта от комментариев. Если у вас есть вопросы по поводу перехватов - внимательно прочтите вторую часть урока, и если что-то останется для вас непонятным, задавайте вопросы в той теме.


Всем привет.

Сегодня я расскажу вам о перехвате функций в Pawn.

Для начала разберёмся, что такое перехваты и с чем их едят.
Перехваты в SA:MP обычно используется для того, чтобы изменить поведение каких-либо функций.
Классическим примером может послужить написание античита на HP.
Предположим, у нас есть глобальный массив, в котором мы будем записывать HP игроков после его выдачи с помощью SetPlayerHealth.
Без перехватов придётся выискивать в моде все вызовы SetPlayerHealth и под каждым из них вручную записывать выдаваемое количество HP в ячейку массива.

В то же время, используя технику перехвата, достаточно всего лишь написать 1 перехватчик, в котором происходит та же запись в массив, и поместить его в самое начало мода, перед подключением a_samp.inc. В дальнейшем перехваты вместе с остальными переменными и функциями античита можно вынести в отдельный инклуд (например, "ac_hp.inc"), который при необходимости можно будет отключить, всего лишь закомментировав строку #include "ac_hp.inc".
Таким образом, техника перехватов даёт возможность "прицепить" свой код к стандартным функциям и коллбэкам без вмешательства в сам мод, в разы упростив дальнейшую разработку.

В этом уроке будет описан только один метод, который был открыт благодаря ipsBruno и Y_Less.
Вся суть данного метода состоит в том, что макропроцессор обрабатывает выражения "#if defined X" в 2 прохода, поэтому если "X" - название ещё не известной компилятору функции/переменной/константы, во время 1-го прохода макропроцессор пропустит такой макрос и сначала соберёт информацию о функциях, переменных и константах в скрипте, завершив 1-й проход.
После этого на 2-м проходе компилятор уже будет знать обо всех переменных, функциях и константах, расположенных не только перед строкой с "#if defined", но и после неё. В результате перехватчик подменит собой перехватываемую функцию, а затем вызовет её, если она существует. Если же перехватываемой функции не существует, будет вызван только перехватчик.


Преимущества такого способа перед другими методами:
Не требуется вызывать CallLocalFunction, благодаря чему не происходит увеличения нагрузки из-за использования нативных функций.
Не используется модификация кода во время его выполнения (из-за таких модификаций могла возникнуть несовместимость кода с JIT).


Рассматриваемый нами метод перехвата работает примерно так:

<перехватчик>
<код перехвата>
<вызов перехватываемой функции>
<конец перехватчика>
<функция уже была перехвачена?>
<удалить старый перехват>
<иначе>
<дать знать, что теперь функция перехвачена>
<конец ветвления>
<вставить перехватчик в цепочку вызовов>
<предварительно объявить функцию>
В результате при умелом использовании одна или несколько функций-перехватчиков могут быть выстроены в цепочку: сначала вызывается первый объявленный перехватчик, а за ним и все остальные, пока очередь не дойдёт до оригинальной перехватываемой функции:

<перехватчик1>
<выполнить набор действий перехватчика>
<вызвать перехватчик2>
<конец перехватчика>

<перехватчик2>
<выполнить набор действий перехватчика>
<вызвать перехватчик3>
<конец перехватчика>

<...>

<перехватчикN>
<выполнить набор действий перехватчика>
<вызвать перехватываемую функцию>
<конец перехватчика>

<перехватываемая функция>
<...>
<конец функции>

В Pawn существуют 2 способа реализации этого метода: один для перехвата коллбэков (функций, автоматически вызываемых сервером; пример: OnPlayerConnect) и один для нативных функций (функций, реализованных в самом сервере; пример: printf, SendClientMessage).

Перехват коллбэков реализуется следующим образом:

// CallbackFunc - название перехватываемой функции
// param1, param2 - параметры перехватываемой функции
public CallbackFunc(param1, param2)
{
// здесь место для Вашего кода, ради которого и осуществляется перехват
// ...

// если перехватываемая функция существует - нужно вызвать и её,
// после чего вернуть значение, которая вернёт перехватываемая функция
// (LibName - название Вашей библиотеки (в случае с Pawn - инклуда),
// в котором перехватывается функция CallbackFunc)
#if defined LibName__CallbackFunc
return Libname__CallbackFunc(param1, param2);
#endif
}

// если объявлен макрос с префиксом "_ALS_" и именем функции после него -
// это значит, что такая функция уже была где-то перехвачена

// если функция CallbackFunc уже была перехвачена - уберём макрос, сделанный
// в предыдущем перехвате (назначение этого макроса будет объяснено далее)
#if defined _ALS_CallbackFunc
#undef CallbackFunc

// если же функция ещё не была перехвачена,
// дадим знать о её перехвате, объявив макрос "_ALS_CallbackFunc"
#else
#define _ALS_CallbackFunc
#endif

// сделаем макрос "CallbackFunc", чтобы заменить название перехватываемой
// функции CallbackFunc на Libname__CallbackFunc во время её объявления в основном скрипте
#define CallbackFunc LibName__CallbackFunc

// отныне перехватываемая функция будет называться не "CallbackFunc", а "LibName__CallbackFunc",
// а название "CallbackFunc" достанется функции-перехватчику;
// остаётся лишь сделать для перехваченной функции опережающее объявление
// с её новым названием, чтобы избежать лишних предупреждений от компилятора
#if defined LibName__CallbackFunc
forward LibName__CallbackFunc(param1, param2);
#endif


Перехват нативных функций производится похожим образом, но, в отличие от первого способа, перехватываемая функция не переименовывается, поэтому не требуется делать для неё новое предварительное объявление (forward).

// функция-перехватчик
stock LibName__NativeFunc(param1, param2)
{
// место для кода перехвата
// ...

// вызов перехватываемой функции, на этот раз без проверки существования функции
return NativeFunc(param1, param2);
}
#if defined _ALS_NativeFunc
#undef NativeFunc
#else
#define _ALS_NativeFunc
#endif
#define NativeFunc LibName__NativeFunc


Теперь разберёмся, как всё это выглядит в деле.
Методы для коллбэков и нативных функций разные, поэтому рассмотрены они будут отдельно.

Перехват коллбэков на примере OnDialogResponse:

// массив, в котором записаны ID диалогов ,показанных другим игрокам
static player_dialog_ids[MAX_PLAYERS] = {-1, ...};

// перехватчик
public OnDialogResponse(playerid, dialogid, response, listitem, inputtext[])
{
// если игрок посылает сигнал об ответе на диалог,
// который ему не показывали - это признак использования читов
// (и да, я знаю, что сейчас этой проблемы в SA:MP нет, это всего лишь пример!)
if(player_dialog_ids[playerid] != dialogid)
Kiсk(playerid);
player_dialog_ids[playerid] = -1;
#if defined spd__OnDialogResponse
return spd__OnDialogResponse(playerid, dialogid, response, listitem, inputtext);
#endif
}
#if defined _ALS_OnDialogResponse
#undef OnDialogResponse
#else
#define _ALS_OnDialogResponse
#endif
#define OnDialogResponse spd__OnDialogResponse
#if defined spd__OnDialogResponse
forward spd__OnDialogResponse(playerid, dialogid, response, listitem, inputtext[]);
#endif

Перехват нативной функции на примере ShowPlayerDialog:

static player_dialog_ids[MAX_PLAYERS] = {-1, ...};

// перехватчик
stock spd__ShowPlayerDialog(playerid, dialogid, type, header[], text[], button1[], button2[])
{
player_dialog_ids[playerid] = dialogid;
return ShowPlayerDialog(playerid, dialogid, type, header, text, button1, button2);
}
#if defined _ALS_ShowPlayerDialog
#undef ShowPlayerDialog
#else
#define _ALS_ShowPlayerDialog
#endif
#define ShowPlayerDialog spd__ShowPlayerDialog



На сегодня всё, удачного скриптинга!

Для тех, кто любит стрелять себе в ногу:


Если вы только начинаете практиковаться с перехватами, сверяйте свой код с примерами из этого урока.

Не выдумывайте своих префиксов вместо "_ALS_".
Директива "#if defined _ALS_<название функции>" используется специально, чтобы ваш перехват правильно работал с другими перехватами той же функции.
Если вместо "_ALS_" использовать другой префикс, потеряется совместимость с другими перехватчиками.
Пример того, как не нужно делать:


public OnGameModeInit()
{
// ...
#if defined my_lib_OnGameModeInit
my_lib_OnGameModeInit();
#endif
}
#if defined _my_prefix_OnGameModeInit
#undef OnGameModeInit
#else
#define _my_prefix_OnGameModeInit
#endif
#if defined my_lib_OnGameModeInit
forward my_lib_OnGameModeInit();
#endif
#define OnGameModeInit my_lib_OnGameModeInit

Правильный пример:


public OnGameModeInit()
{
// ...
#if defined my_lib_OnGameModeInit
my_lib_OnGameModeInit();
#endif
}
#if defined _ALS_OnGameModeInit
#undef OnGameModeInit
#else
#define _ALS_OnGameModeInit
#endif
#if defined my_lib_OnGameModeInit
forward my_lib_OnGameModeInit();
#endif
#define OnGameModeInit my_lib_OnGameModeInit


Как-то давно один "профессионал" спросил меня, мол зачем вся эта чепуха с перехватами, когда всё можно уместить в один #define?

stock my_AddStaticVehicle(modelid, Float:spawn_x, Float:spawn_y, Float:spawn_z, Float:angle, color1, color2)
{
print('Функция AddStaticVehicle перехвачена');
return AddStaticVehicle(modelid, spawn_x, spawn_y, spawn_z, angle, color1, color2);
}
#define AddStaticVehicle my_AddStaticVehicle

Так вот, этот метод в корне неправильный.

Почему? Очень просто, им нельзя навесить больше одного перехвата на одну и ту же функцию.
Не верите? Тогда попробуйте скомпилировать такой код:

// 2-й перехватчик для AddStaticVehicle
stock my2_AddStaticVehicle(modelid, Float:spawn_x, Float:spawn_y, Float:spawn_z, Float:angle, color1, color2)
{
print('Функция AddStaticVehicle перехвачена ещё раз');
return AddStaticVehicle(modelid, spawn_x, spawn_y, spawn_z, angle, color1, color2);
}
#define AddStaticVehicle my2_AddStaticVehicle

// 1-й перехватчик для AddStaticVehicle
// Здесь будет "warning 201: redefinition of constant/macro (symbol "AddStaticVehicle")".
stock my1_AddStaticVehicle(modelid, Float:spawn_x, Float:spawn_y, Float:spawn_z, Float:angle, color1, color2)
{
print('Функция AddStaticVehicle перехвачена');
return AddStaticVehicle(modelid, spawn_x, spawn_y, spawn_z, angle, color1, color2);
}
#define AddStaticVehicle my1_AddStaticVehicle

Получилось скомпилировать код без варнингов и ошибок?

Такой способ подойдёт только если вы используете его в своём моде и только если вы перехватываете функцию один раз (но даже в этом случае не рекомендуется, т.к. вам может понадобиться перехватить функцию более 1 раза).
Но если вы используете этот ленивый способ в выкладываемых на Pro-Pawn работах (да, я смотрю на вас, авторы уроков и мануалов), даже не надейтесь, что ваша работа будет одобрена.
Вы явно ошиблись порталом, если думаете, что можно одобрить полурабочий говнокод, который может нарушить совместимость с другими работами.


Но ведь и так сойдёт! А кому не нравится, пусть сами переделывают!!
Отправляйтесь обратно н***й на govno-info и постите говнокод там.



P.S.: Скоро будет готова 2-я часть урока, в которой будет рассматриваться использование перехватов на практике.
Часть 2. (http://pro-pawn.ru/showthread.php?10611-%D0%9F%D0%B5%D1%80%D0%B5%D1%85%D0%B2%D0%B0%D1%82-%D1%84%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D0%B9-%D1%87%D0%B0%D1%81%D1%82%D1%8C-2-%D0%BF%D1%80%D0%B0%D0%BA%D1%82%D0%B8%D0%BA%D0%B0-%D0%BF%D0%B8%D1%88%D0%B5%D0%BC-%D0%B0%D0%BD%D1%82%D0%B8%D1%87%D0%B8%D1%82-%D0%BD%D0%B0-HP)


Автор: Daniel_Cortez (http://pro-pawn.ru/member.php?100-Daniel_Cortez)

Специально для Pro-Pawn.ru (http://www.pro-pawn.ru)
Копирование данной статьи на других ресурсах без разрешения автора запрещено!

Nurick
25.09.2014, 19:12
Хотелось бы увидеть на практике. Подожду 2-ю часть.

Daniel_Cortez
26.09.2014, 20:25
Добавил в начало статьи объяснение сути перехватов, дабы больше не было вопросов.
Вторую часть выложу на следующей неделе, как только доделаю пару-тройку примеров.

H_Less
03.07.2015, 20:54
А что, если нет необходимости выискивать все вызовы SetPlayerHealth и сделать просто так?


#define h_SetPlayerHealth(%0,%1) (SetPlayerHealth(%0,%1);ac_health[%0]=%1)

Не воспринимайте мой ответ всерьез. Просто сложилось такое впечатление, что автору лень выискивать все вызовы и из-за этого пишет перехватчик.

По поводу перехватов. А что если нативная функция имеет всего один параметр? К примеру GetVehicleModel. То как ее перехватить?

$continue$
03.07.2015, 21:15
А что, если нет необходимости выискивать все вызовы SetPlayerHealth и сделать просто так?


#define h_SetPlayerHealth(%0,%1) (SetPlayerHealth(%0,%1);ac_health[%0]=%1)

Не воспринимайте мой ответ всерьез. Просто сложилось такое впечатление, что автору лень выискивать все вызовы и из-за этого пишет перехватчик.

По поводу перехватов. А что если нативная функция имеет всего один параметр? К примеру GetVehicleModel. То как ее перехватить?

Руками и перехватываем :rofl:
И кстати, Вам рассказали в первом посте почему лучше всего использовать перехваты.


stock _GetVehicleModel_(vehicleid)
{
// Код
return GetVehicleModel(vehicleid);
}
#if defined _ALS_GetVehicleModel
#undef GetVehicleModel
#else
#define _ALS_GetVehicleModel
#endif
#define GetVehicleModel _GetVehicleModel_

H_Less
03.07.2015, 21:58
Руками и перехватываем :rofl:
И кстати, Вам рассказали в первом посте почему лучше всего использовать перехваты.


stock _GetVehicleModel_(vehicleid)
{
// Код
return GetVehicleModel(vehicleid);
}
#if defined _ALS_GetVehicleModel
#undef GetVehicleModel
#else
#define _ALS_GetVehicleModel
#endif
#define GetVehicleModel _GetVehicleModel_


Перехват руками? Это BDSM уже какой то :D

Daniel_Cortez
21.07.2015, 20:22
Почему то не отчищает строку до =


stock fread_ex(File: handle, string[], size = sizeof string, bool: pack = false)
{
GetStringDataAfterChar(string);
return fread(handle, string, size, pack);
}
#if defined _ALS_fread
#undef fread
#else
#define _ALS_fread
#endif
#define fread fread_ex

Могу я поинтересоваться, зачем здесь вообще нужен перехват?
И что будет, если вам вдруг понадобится использовать оригинальную функцию fread, без считывания данных после знака "=" ?
Или у вас так и задумано в корне поменять принцип работы функции и сделать недоступным оригинал? Если да, то перехваты созданы совсем не для этого.
Кстати, влияние на логику функций таки обсуждается во 2-й части урока, среди остальных правил.


Перехватчик не должен менять логику работы перехватываемой функции.
Изменение возвращаемых значений допускается только в том случае, если это не нарушает совместимости с оригинальной функцией.
Например, в античите на HP перехватчик GetPlayerHealth может возвращать не то значение, которое возвратит оригинальная функция, а кол-во HP игрока, хранящееся в античите. Но в то же время перехватчик не должен всегда возвращать, например, сумму HP и брони, это уже совсем другая логика.
Иными словами, работа перехватчика должна быть незаметной для того кода, который использует перехватываемую функцию, как будто того перехватчика и нет.
Если же вам нужна функция, работающая по-другому - сделайте отдельную функцию, но не нужно путать её с оригиналом с помощью перехвата. Вмешательство в стандартную логику обычно приводит только к проблемам.


Я думал, это должно быть очевидно, что не нужно наступать на грабли, но раз так...
Ок, закрою эту тему и оставлю в 1-м посте примечание, чтобы, прежде чем задавать какие-то вопросы, читали 2-ю часть урока и оставляли комментарии там.

Daniel_Cortez
25.08.2015, 15:21
Обновил 1-й пост. Добавлен новый пункт "Для тех, кто любит стрелять себе в ногу".
Отдельное спасибо Untonyst (http://pro-pawn.ru/member.php?4123-Untonyst) за то, что указал на ошибки в примере перехвата OnDialogResponse.



Для тех, кто любит стрелять себе в ногу:

Если вы только начинаете практиковаться с перехватами, сверяйте свой код с примерами из этого урока.

Не выдумывайте своих префиксов вместо "_ALS_".
Директива "#if defined _ALS_<название функции>" используется специально, чтобы ваш перехват правильно работал с другими перехватами той же функции.
Если вместо "_ALS_" использовать другой префикс, потеряется совместимость с другими перехватчиками.
Пример того, как не нужно делать:


public OnGameModeInit()
{
// ...
#if defined my_lib_OnGameModeInit
my_lib_OnGameModeInit();
#endif
}
#if defined _my_prefix_OnGameModeInit
#undef OnGameModeInit
#else
#define _my_prefix_OnGameModeInit
#endif
#if defined my_lib_OnGameModeInit
forward my_lib_OnGameModeInit();
#endif
#define OnGameModeInit my_lib_OnGameModeInit

Правильный пример:


public OnGameModeInit()
{
// ...
#if defined my_lib_OnGameModeInit
my_lib_OnGameModeInit();
#endif
}
#if defined _ALS_OnGameModeInit
#undef OnGameModeInit
#else
#define _ALS_OnGameModeInit
#endif
#if defined my_lib_OnGameModeInit
forward my_lib_OnGameModeInit();
#endif
#define OnGameModeInit my_lib_OnGameModeInit


Как-то давно один "профессионал" спросил меня, мол зачем вся эта чепуха с перехватами, когда всё можно уместить в один #define?


stock my_AddStaticVehicle(modelid, Float:spawn_x, Float:spawn_y, Float:spawn_z, Float:angle, color1, color2)
{
print('Функция AddStaticVehicle перехвачена');
return AddStaticVehicle(modelid, spawn_x, spawn_y, spawn_z, angle, color1, color2);
}
#define AddStaticVehicle my_AddStaticVehicle

Так вот, этот метод в корне неправильный.

Почему? Очень просто, им нельзя сделать больше одного перехвата на одну и ту же функцию.
Не верите? Тогда попробуйте скомпилировать такой код:


// 2-й перехватчик для AddStaticVehicle
stock my2_AddStaticVehicle(modelid, Float:spawn_x, Float:spawn_y, Float:spawn_z, Float:angle, color1, color2)
{
print('Функция AddStaticVehicle перехвачена ещё раз');
return AddStaticVehicle(modelid, spawn_x, spawn_y, spawn_z, angle, color1, color2);
}
#define AddStaticVehicle my2_AddStaticVehicle

// 1-й перехватчик для AddStaticVehicle
// Здесь будет "warning 201: redefinition of constant/macro (symbol "AddStaticVehicle")".
stock my1_AddStaticVehicle(modelid, Float:spawn_x, Float:spawn_y, Float:spawn_z, Float:angle, color1, color2)
{
print('Функция AddStaticVehicle перехвачена');
return AddStaticVehicle(modelid, spawn_x, spawn_y, spawn_z, angle, color1, color2);
}
#define AddStaticVehicle my1_AddStaticVehicle

Получилось скомпилировать код без варнингов и ошибок? Я предупреждал.

Такой способ подойдёт только если вы используете его в своём моде и только если вы перехватываете функцию один раз (хоть я этого и не рекомендую).
Но если вы используете этот ленивый способ в выкладываемых на Pro-Pawn работах (да, я смотрю на вас, авторы уроков и мануалов), даже не надейтесь, что ваша работа будет одобрена.
Вы явно ошиблись порталом, если думаете, что можно одобрить полурабочий говнокод, который может нарушить совместимость с другими работами.


Отправляйтесь обратно н***й на govno-info и постите говнокод там.