PDA

Просмотр полной версии : [Урок] Оператор __emit



Daniel_Cortez
31.12.2017, 15:49
Всем привет.


Начиная с версии 3.10.5 в компиляторе Pawn появился оператор __emit. В данной статье я постараюсь рассказать обо всех особенностях этого оператора. За основу будет взят материал из подготовленного мной ранее описания оператора __emit на GitHub (1 (https://github.com/Zeex/pawn/pull/180#issuecomment-323531746), 2 (https://github.com/Zeex/pawn/pull/211#issue-275106280)).


В отличие от директивы #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().
printf("Free stack/heap space: %d", GetFreeStackSpace());

// И так тоже можно, возвращаемое значение останется неиспользованным.
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 lctrl 6;
__emit jrel (cellbits / charbits * 2);




Данные
Глобальная переменная, либо статическая (static) локальная переменная


static x = 0;
__emit load.pri x;




Локальная переменная
Локальная (расположенная в стеке) переменная или аргумент функции


SomeFunction(x)
{
new y;
__emit load.s.pri x;
__emit load.s.alt y;
}




Метка
Любая метка, на которую можно совершить переход (в т.ч. объявленная внутри блока __emit{})


label1:
__emit jnz label1;




Pawn-функция
Функция, реализованная на языке Pawn


__emit call MyFunction;




Нативная функция
Встроенная функция, предоставляемая интерпретатором


__emit sysreq.c SendClientMessage;




Любое значение
Значение любого из перечисленных выше типов


new x;
__emit {
label1:
label2:
const.pri label1
eq.c.pri label2
add.c x
}





Помимо перечисленных выше типов данных есть также дополнительные типы, характерные для специфических инструкций:



Неотрицательное число
Любое целое неотрицательное число


__emit cmps (cellbits / charbits * 128);




Величина сдвига
Величина битового сдвига (целое число от 0 до 31)


__emit shl.c.pri 3; // PRI = PRI << 3




Размер данных
Количество байт для данных, загружаемых/сохраняемых при помощи lodb.i/strb.i (1, 2 или 4)


__emit lodb.i 2; // PRI = [PRI] (2 байта)






Выражения в операндах инструкций

Оператор __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
sysreq.n SendClientMessage (3 * cellbits / charbits)
}

При компиляции с флагом -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]);
}

Реализованы следующие псевдоинструкции:


НазваниеТип операндаЗатирает значение в PRI/ALT?
load.u.pri/altГлобальная переменная-
Локальная переменная-
Константное значение-
Ссылка-
Ячейка массиваPRI, ALT

stor.u.pri/altГлобальная переменная-
Локальная переменная-
Ссылка-
Ячейка массиваPRI, ALT

addr.u.pri/altГлобальная переменная-
Локальная переменная-
Ссылка-
Ячейка массиваPRI, ALT

push.uГлобальная переменная-
Локальная переменная-
Константное значение-
СсылкаPRI
Ячейка массиваPRI, ALT

push.adr.uГлобальная переменная-
Локальная переменная-
СсылкаPRI
Ячейка массиваPRI, ALT

zero.uГлобальная переменная-
Локальная переменная-
СсылкаPRI
Ячейка массиваPRI, ALT

inc.uГлобальная переменная-
Локальная переменная-
СсылкаPRI
Ячейка массиваPRI, ALT

dec.uГлобальная переменная-
Локальная переменная-
СсылкаPRI
Ячейка массиваPRI, ALT



Заключение

Оператор __emit изначально создавался с расчётом на расширяемость и безопасность: он снабжён множеством полезного функционала, обеспечивает больший контроль над кодом и защиту от ошибок, продолжает пополняться новым функционалом с каждым новым релизом компилятора, и в целом подходит для общего пользования гораздо больше, чем директива #emit, которую с наличием оператора впору считать устаревшей.


Автор статьи: Daniel_Cortez (http://pro-pawn.ru/member.php?100-Daniel_Cortez)
Благодарности:
VVWVV (http://pro-pawn.ru/member.php?4348-VVWVV) - изначальная реализация оператора __emit. Собственно, благодаря его работе и существует данная статья.
Y_Less (https://github.com/Y-Less), Slice (https://github.com/oscar-broman), Southclaws (https://github.com/Southclaws) - предложения по названию оператора и "обновлённому" синтаксису.



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

VVWVV
31.12.2017, 20:42
Теперь можно и в pawn 4x такую фичу отправить. Хотя вряд ли ее кто-то ждет.

Daniel_Cortez
01.01.2018, 13:56
Теперь можно и в pawn 4x такую фичу отправить. Хотя вряд ли ее кто-то ждет.
Нет, конечно же. Во-первых, я уже говорил о стагнации в Pawn 4 (http://pro-pawn.ru/showthread.php?14174-nPawn&p=88388#post88388).
Во-вторых, директива #emit была удалена (https://github.com/compuphase/pawn/commit/57f7de9e97b0f20de036bf8c027ac841b0e60e6a) ещё в версии 3.3. Ни в истории изменений, ни в документации (Language Guide, Implementer's Guide) не было никаких объяснений причин данного шага, но менее чем через месяц в код загрузки скриптов была добавлена куча новых проверок (https://github.com/compuphase/pawn/commit/5ba48f660b6ad2d43952c2fe5ca2a3c2d3c0498a#diff-37fc4b146a27c8f9593ac0d3075b1d14R734) для верификации байткода (попытка перекрыть уязвимости с доступом к памяти за пределами секции данных - хотя несколько опкодов, включая PUSH*.s, видимо, забыли, ибо в них адреса доступа не проверялись). Не нужно быть гением, чтобы понять связь между этими двумя изменениями и догадаться, насколько в 4-й версии нужно будет хоть что-то, связанное с #emit.


UPD: Добавил недостающий пример в секции "Автозамена макроинструкций".

MassonNN
19.12.2019, 19:26
вопрос следующий, можно ли как-то с помощью emit создать глобальную переменную из функции?

Daniel_Cortez
19.12.2019, 20:07
вопрос следующий, можно ли как-то с помощью emit создать глобальную переменную из функции?
Нет, нельзя (да и зачем такое вообще может понадобиться?) Оператор __emit только подставляет на место использования инструкции Pawn AMX, это не какая-то универсальная "серебряная пуля".
К слову, я использовал в предложении выше именно "__emit" (да, конкретно этот вариант, с нижними подчёркиваниями в начале), и вам тоже советую использовать только его, т.к. другой вариант - "emit" - будет удалён в следующей версии компилятора.

vvw
23.12.2019, 01:16
вопрос следующий, можно ли как-то с помощью emit создать глобальную переменную из функции?

Я реализовал первую версию __emit с целью уменьшения числа ошибок при разработке. В целом это все еще тот же #emit, но более строгий и универсальный.

Касательно вашего вопроса: можно создать с помощью __emit глобальную переменную, но для этого нужно заранее выделить в сегменте данных n ячеек.

MassonNN
23.12.2019, 08:24
Я реализовал первую версию __emit с целью уменьшения числа ошибок при разработке. В целом это все еще тот же #emit, но более строгий и универсальный.

Касательно вашего вопроса: можно создать с помощью __emit глобальную переменную, но для этого нужно заранее выделить в сегменте данных n ячеек.

То есть выделить ячейки из одной функции и использовать это пространство в другой нельзя?

vvw
23.12.2019, 09:08
То есть выделить ячейки из одной функции и использовать это пространство в другой нельзя?

Можно выделить, но удерживать это пространство будет довольно сложно. Что-то подобное реализовал Y_less в своей библиотеке y_malloc. Там он выделяет большой объем памяти в сегменте стек-куча через #pragma dynamic <число>, чтобы выходной файл не занимал много места на носителе. Поддерживает этот кусок памяти через таймеры.

Daniel_Cortez
23.12.2019, 09:27
Что-то подобное реализовал Y_less в своей библиотеке y_malloc. Там он выделяет большой объем памяти в сегменте стек-куча через #pragma dynamic <число>, чтобы выходной файл не занимал много места на носителе. Поддерживает этот кусок памяти через таймеры.
Эта реализация уже давно устарела и по умолчанию память выделяется из глобального массива в 16 Мб.

vvw
23.12.2019, 11:04
Эта реализация уже давно устарела и по умолчанию память выделяется из глобального массива в 16 Мб.

Это в новой версии?

UPD: помотрел реализацию YSI5 и YSI4 - там все тоже самое. Там есть YSI_NO_HEAP_MALLOC, которая не дает компилятору добавить с сегмент стека-кучи доп. значение, а использует глобальный массив. Но YSI_NO_HEAP_MALLOC нигде не определена.

MassonNN
23.12.2019, 11:09
Эта реализация уже давно устарела и по умолчанию память выделяется из глобального массива в 16 Мб.

Баг фиксанули или специально возможность закрыли?!?!? На самом деле объявление глобальной переменной из функции очень удобно к примеру при реализации новых типов данных и динамических массивов.

Daniel_Cortez
26.02.2020, 22:28
Статья обновлена.
Все упоминания "emit" заменены на "__emit" (ключевое слово emit удалено в релизе компилятора 3.10.10, оставлен только вариант "__emit").
Добавлена таблица типов данных (абзац "Проверка операндов инструкций").
Добавлен новый абзац "Универсальные псевдоинструкции" (новая возможность, начиная с релиза 3.10.10).