Всем привет.
Начиная с версии 3.10.5 в компиляторе Pawn появился оператор __emit. В данной статье я постараюсь рассказать обо всех особенностях этого оператора. За основу будет взят материал из подготовленного мной ранее описания оператора __emit на GitHub (1, 2).
В отличие от директивы #emit, оператор __emit представляет собой более продуманную реализацию встроенного ассемблера для Pawn.
Основные преимущества оператора перед директивой:
- 2 варианта синтаксиса (одиночный и блоковый).
- Возможность применения в макросах и выражениях.
- Проверка типов операндов в инструкциях.
- Выражения в операндах инструкций.
- Возможность указать метку, объявленную после блока __emit.
- Возможность указать 2 и более операндов в инструкции.
- Автозамена макроинструкций на обычные инструкции.
- Универсальные псевдоинструкции.
Варианты синтаксиса
Оператор __emit имеет два варианта синтаксиса: одиночный и блоковый.
Пример одиночного синтаксиса:
__emit const.pri 1;
Здесь всё просто: разрешается только одна инструкция на предложение (под предложением имеется в виду всё то, что идёт до знака ";").
Для внедрения в код более одной инструкции AMX следует использовать либо блоковый синтаксис, либо несколько одиночных предложений __emit:
new a = 4, b = 5; // Сделаем так, чтобы переменные "a" и "b" обменялись значениями __emit load.s.pri a; __emit load.s.alt b; __emit stor.s alt a; __emit stor.s.pri b; // Теперь a = 5, b = 4
С помощью блокового синтаксиса возможно внедрение любого числа инструкций AMX, однако данный синтаксис разделяется на 2 подвида: с круглыми скобками и с фигурными.
В синтаксисе с фигурными скобками на одну строку должна приходиться только одна инструкция.
Примеры:
__emit { const.pri = 1 } new a = 4, b = 5; __emit { load.s.pri a load.s.alt b stor.s.pri b stor.s.alt a } __emit { // Да, под "любым числом" я имел в виду не только 1 и более инструкций, // но и их полное отсутствие тоже. } __emit {}
Данный вариант синтаксиса хорошо подходит для простых ассемблерных вставок, как с директивой #emit.
Также есть ещё один вариант блокового синтаксиса, с круглыми скобками.
__emit( push.alt, stack 0, move.pri, pop.alt );
Обратите внимание: в этом варианте не требуется использовать только одну инструкцию на строку, а сами инструкции разделяются между собой запятыми.
Применение в макросах и выражениях
Поскольку __emit является оператором, в отличие от директивы #emit, его можно использовать в макросах и выражениях.
Поскольку у каждого выражения есть результат, будучи в выражении, оператор __emit возвращает значение, которое было в регистре PRI после выполнения последней инструкции AMX в блоке.
new x = __emit( const.pri 1 ); // x = 1
Можно соединять несколько подвыражений __emit в цепочку, разделяя их запятыми, как с вызовами функций:
new x = (__emit stack 0, __emit move.pri);
Благодаря этому можно внедрить в выражение более чем 1 инструкцию AMX.
Но удобнее в таких случаях использовать блоковый синтаксис с круглыми скобками:
new x = __emit( stack 0, move.pri );
Кроме того, можно объединить в цепочку и __emit с круглыми скобками:
__emit nop, __emit( stack 0, move.pri ), __emit( nop, nop );
__emit с одиночным синтаксисом вполне подходит для использования в макросах:
#define DoSomething() __emit const.pri 1
Также в макросах можно указать несколько инструкций AMX с помощью блокового синтаксиса, однако рекомендуется именно вариант с круглыми скобками.
Причину такого выбора проще рассмотреть на примере:
#define DoSomething() __emit { nop }
Вариант с фигурными скобками требует ставить только одну инструкцию на строку. В то же время всё содержимое макроса понимается компилятором как одна строка, поэтому больше 1 инструкции в __emit с фигурными скобками не укажешь.
Мало того, такой вариант __emit нельзя использовать в выражениях, чего может крайне не хватать для макросов, маскирующихся под функции.
Другое дело - вариант с круглыми скобками:
#define GetFreeStackSpace() ( \ __emit( \ lctrl 4, \ heap 0, \ sub \ ) \ ) main() { // Здесь значение, возвращаемое последним блоком __emit, // используется как аргумент функции printf(). // И так тоже можно, возвращаемое значение останется неиспользованным. GetFreeStackSpace(); }
Данный вариант синтаксиса специально задумывался так, чтобы быть похожим на вызов функции, благодаря чему он идеально подходит для написания макросов.
Проверка операндов инструкций
В отличие от одноимённой директивы, оператор __emit проверяет тип и количество операндов у каждой указанной в нём инструкции. К примеру, операндом инструкции перехода (JUMP, JZER, JNZ и пр.) может быть только название метки, иначе компилятор выдаст ошибку.
new x = 1; __emit jump x; // error 019: not a label: "x" __emit jump 0; // error 001: expected token "-label-", but found "-integer value-" label1: __emit jump label1; // ok
Помимо меток для инструкций перехода, проверяются и другие типы значений:
main() { new x = 1; static y = 1; // Инструкция load.pri загружает значение из глобальной переменной (т.е. из секции данных), // а load.s.pri - из локальной (по смещению относительно фрейма стека). // Оператор __emit контролирует это и помогает скриптеру не запутаться __emit load.pri x; // error 001: expected token: "-data offset-", but found "-local variable-" __emit load.s.pri y; // error 001: expected token: "-local variable-", but found "-data offset-" __emit load.s.pri x; // ok __emit load.pri y; // ok }
Кроме того, с оператором __emit компилятор знает, сколько операндов должно быть у той или иной инструкции AMX, и если обнаружит несоответствие, то обязательно укажет вам на него. К примеру, в инструкции const.pri требуется только 1 операнд, поэтому компилятор не позволит вам использовать эту инструкцию без операнда:
// С директивой #emit данный неправильный код скомпилируется // и, скорее всего, приведёт к генерации невалидного байткода AMX // (сервер просто откажется загружать такой скрипт). #emit const.pri // Но с оператором __emit компилятор выдаст ошибку, поскольку требуется операнд (константное значение). __emit const.pri; // error 001: expected token: "-numeric value-", but found ";" __emit const.pri 0; // ok (целое число) __emit const.alt cellmax; // ok (встроенная константа cellmax)
Трудноуловимые ошибки с лишними операндами тоже отлавливаются на раз-два:
__emit const.pri 1; __emit add 2; // error 001: expected token: ";", but found "-integer value-" // Суть ошибки выше: лишний операнд в инструкции 'add' // (для прибавления константного значения используется опкод 'add.c').
Различаются следующие типы данных:
Помимо перечисленных выше типов данных есть также дополнительные типы, характерные для специфических инструкций:
Выражения в операндах инструкций
Оператор __emit позволяет для аргументов типа "любое значение" использовать выражения.
В таких выражениях нельзя использовать переменные и вызовы функций, т.к. результатом должно быть константное значение.
Пример:
__emit const.pri (MAX_PLAYERS + 1);
Обратите внимание: выражение должно быть взято в круглые скобки - это нужно, чтобы компилятор мог правильно отличить выражение от одиночного аргумента.
Кроме сложения ("+") в выражениях внутри __emit можно использовать любые другие виды операций, которые доступны в обычных выражениях.
__emit const.pri (((MAX_PLAYERS / 2) & (cellbits / charbits - 1)) * 2 >>> 1);
Возможность указать метку, объявленную после блока __emit
Здесь всё просто: с помощью директивы #emit возможно использование инструкций перехода только с метками, объявленными выше места использования, когда с оператором __emit можно делать переходы и на метки, объявленные ниже.
#emit jump label1 // error 017: undefined symbol "label1" label1:
__emit jump label1; // ok label1:
Возможность указать 2 и более операндов в инструкции
В директиве #emit инструкции могут быть либо только с одним операндом, либо без операндов. В свою очередь, оператор __emit знает точное количество аргументов для каждой инструкции, а потому с ним можно указать любое количество операндов для инструкции (при условии, что это количество правильное). Благодаря этому становится возможным использование макроинструкций, которые предусматривают наличие 2 и более операндов.
Рассмотрим это на примере того же обмена значениями между переменными:
new a = 4, b = 5; __emit { load.s.both a, b // вместо цепочки 'load.s.pri a\ load.s.alt b' используем макроинструкцию load.s.both stor.s.alt a // для stor.s.pri/alt нет аналогичной макроинструкции, stor.s.pri b // поэтому используем их "как есть" }
Кто-то скажет, что данная возможность будет бесполезной для пользователей SA-MP, т.к. на сервере нельзя использовать макроинструкции из-за более старой версии интерпретатора Pawn.
И это могло бы быть правдой, если не ещё одна фича, описанная ниже.
Автозамена макроинструкций на обычные инструкции
Если в операторе __emit используются макроопкоды, то при компиляции кода без макрооптимизаций (т.е. с ключами "-O0", "-O1" или "-d2") макроопкоды будут автоматически заменены на обычные опкоды.
Например, макроопкод 'push5.c' будет заменён на последовательность из пяти 'push.c', а 'sysreq.n' - на 'push.c \ sysreq.c \ stack'.
Пример:
static const msg[] = "Hi there"; __emit { // SendClientMessage(0, 0xFFFFFFFF, msg); push3.c msg 0xFFFFFFFF 0 }
При компиляции с флагом -d2 получится следующий байткод:
push.c 00000000 push.c ffffffff push.c 00000000 push.c 0000000c sysreq.c 00000000 ; SendClientMessage stack 00000010
Универсальные псевдоинструкции
Начиная с версии компилятора 3.10.10 появились так называемые "универсальные" псевдоинструкции, которые в зависимости от операнда компилируются в разные инструкции AMX.
Пример:
SomeFunction() { new local_var; static static_local_var; const CONSTANT_VALUE = 1; __emit load.u.pri local_var; // Скомпилируется в "load.s.pri local_var" __emit load.u.pri static_local_var; // => "load.pri static_local_var" __emit load.u.pri CONSTANT_VALUE; // => "const.pri CONSTANT_VALUE" }
Основное назначение таких псевдоинструкций - использование в макросах, чтобы обрабатывать любые операнды, указанные пользователем:
#define increase(%0) __emit(inc.u %0) // Увеличивает значение переменной на 1 new global_var = 0; new global_array[2]; main() { new local_var = 0; static local_static_var = 0; static local_static_array[2]; increase(global_var); increase(global_array[1]); increase(local_var); increase(local_static_var); increase(local_static_array[1]); }
Реализованы следующие псевдоинструкции:
Заключение
Оператор __emit изначально создавался с расчётом на расширяемость и безопасность: он снабжён множеством полезного функционала, обеспечивает больший контроль над кодом и защиту от ошибок, продолжает пополняться новым функционалом с каждым новым релизом компилятора, и в целом подходит для общего пользования гораздо больше, чем директива #emit, которую с наличием оператора впору считать устаревшей.
Автор статьи: Daniel_Cortez
Благодарности:
- VVWVV - изначальная реализация оператора __emit. Собственно, благодаря его работе и существует данная статья.
Y_Less, Slice, Southclaws - предложения по названию оператора и "обновлённому" синтаксису.
Специально для Pro-Pawn.ruКопирование данной статьи на других ресурсах без разрешения автора запрещено!