Добро пожаловать на Pro Pawn - Портал о PAWN-скриптинге.
Страница 1 из 2 1 2 ПоследняяПоследняя
Показано с 1 по 10 из 12
  1. #1
    Аватар для Daniel_Cortez
    "Это не хак, это фича"

    Статус
    Оффлайн
    Регистрация
    06.04.2013
    Адрес
    Novokuznetsk, Russia
    Сообщений
    2,192
    Репутация:
    2589 ±

    Оператор __emit

    Всем привет.


    Начиная с версии 3.10.5 в компиляторе Pawn появился оператор __emit. В данной статье я постараюсь рассказать обо всех особенностях этого оператора. За основу будет взят материал из подготовленного мной ранее описания оператора __emit на GitHub (1, 2).


    В отличие от директивы #emit, оператор __emit представляет собой более продуманную реализацию встроенного ассемблера для Pawn.
    Основные преимущества оператора перед директивой:
    • 2 варианта синтаксиса (одиночный и блоковый).
    • Возможность применения в макросах и выражениях.
    • Проверка типов операндов в инструкциях.
    • Выражения в операндах инструкций.
    • Возможность указать метку, объявленную после блока __emit.
    • Возможность указать 2 и более операндов в инструкции.
    • Автозамена макроинструкций на обычные инструкции.
    • Универсальные псевдоинструкции.


    Варианты синтаксиса

    Оператор __emit имеет два варианта синтаксиса: одиночный и блоковый.
    Пример одиночного синтаксиса:
    1. __emit const.pri 1;

    Здесь всё просто: разрешается только одна инструкция на предложение (под предложением имеется в виду всё то, что идёт до знака ";").
    Для внедрения в код более одной инструкции AMX следует использовать либо блоковый синтаксис, либо несколько одиночных предложений __emit:
    1. new a = 4, b = 5;
    2. // Сделаем так, чтобы переменные "a" и "b" обменялись значениями
    3. __emit load.s.pri a;
    4. __emit load.s.alt b;
    5. __emit stor.s alt a;
    6. __emit stor.s.pri b;
    7. // Теперь a = 5, b = 4


    С помощью блокового синтаксиса возможно внедрение любого числа инструкций AMX, однако данный синтаксис разделяется на 2 подвида: с круглыми скобками и с фигурными.
    В синтаксисе с фигурными скобками на одну строку должна приходиться только одна инструкция.
    Примеры:
    1. __emit { const.pri = 1 }
    2.  
    3. new a = 4, b = 5;
    4. __emit
    5. {
    6. load.s.pri a
    7. load.s.alt b
    8. stor.s.pri b
    9. stor.s.alt a
    10. }
    11.  
    12. __emit
    13. {
    14. // Да, под "любым числом" я имел в виду не только 1 и более инструкций,
    15. // но и их полное отсутствие тоже.
    16. }
    17. __emit {}

    Данный вариант синтаксиса хорошо подходит для простых ассемблерных вставок, как с директивой #emit.

    Также есть ещё один вариант блокового синтаксиса, с круглыми скобками.
    1. __emit( push.alt, stack 0, move.pri, pop.alt );

    Обратите внимание: в этом варианте не требуется использовать только одну инструкцию на строку, а сами инструкции разделяются между собой запятыми.


    Применение в макросах и выражениях

    Поскольку __emit является оператором, в отличие от директивы #emit, его можно использовать в макросах и выражениях.
    Поскольку у каждого выражения есть результат, будучи в выражении, оператор __emit возвращает значение, которое было в регистре PRI после выполнения последней инструкции AMX в блоке.
    1. new x = __emit( const.pri 1 ); // x = 1


    Можно соединять несколько подвыражений __emit в цепочку, разделяя их запятыми, как с вызовами функций:
    1. new x = (__emit stack 0, __emit move.pri);

    Благодаря этому можно внедрить в выражение более чем 1 инструкцию AMX.
    Но удобнее в таких случаях использовать блоковый синтаксис с круглыми скобками:
    1. new x = __emit( stack 0, move.pri );

    Кроме того, можно объединить в цепочку и __emit с круглыми скобками:
    1. __emit nop, __emit( stack 0, move.pri ), __emit( nop, nop );


    __emit с одиночным синтаксисом вполне подходит для использования в макросах:
    1. #define DoSomething() __emit const.pri 1


    Также в макросах можно указать несколько инструкций AMX с помощью блокового синтаксиса, однако рекомендуется именно вариант с круглыми скобками.
    Причину такого выбора проще рассмотреть на примере:
    1. #define DoSomething() __emit { nop }

    Вариант с фигурными скобками требует ставить только одну инструкцию на строку. В то же время всё содержимое макроса понимается компилятором как одна строка, поэтому больше 1 инструкции в __emit с фигурными скобками не укажешь.
    Мало того, такой вариант __emit нельзя использовать в выражениях, чего может крайне не хватать для макросов, маскирующихся под функции.
    Другое дело - вариант с круглыми скобками:
    1. #define GetFreeStackSpace() ( \
    2.   __emit( \
    3.   lctrl 4, \
    4.   heap 0, \
    5.   sub \
    6.   ) \
    7. )
    8.  
    9. main()
    10. {
    11. // Здесь значение, возвращаемое последним блоком __emit,
    12. // используется как аргумент функции printf().
    13. printf("Free stack/heap space: %d", GetFreeStackSpace());
    14.  
    15. // И так тоже можно, возвращаемое значение останется неиспользованным.
    16. GetFreeStackSpace();
    17. }

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


    Проверка операндов инструкций

    В отличие от одноимённой директивы, оператор __emit проверяет тип и количество операндов у каждой указанной в нём инструкции. К примеру, операндом инструкции перехода (JUMP, JZER, JNZ и пр.) может быть только название метки, иначе компилятор выдаст ошибку.
    1. new x = 1;
    2. __emit jump x; // error 019: not a label: "x"
    3. __emit jump 0; // error 001: expected token "-label-", but found "-integer value-"
    4.  
    5. label1:
    6. __emit jump label1; // ok

    Помимо меток для инструкций перехода, проверяются и другие типы значений:
    1. main()
    2. {
    3. new x = 1;
    4. static y = 1;
    5.  
    6. // Инструкция load.pri загружает значение из глобальной переменной (т.е. из секции данных),
    7. // а load.s.pri - из локальной (по смещению относительно фрейма стека).
    8. // Оператор __emit контролирует это и помогает скриптеру не запутаться
    9. __emit load.pri x; // error 001: expected token: "-data offset-", but found "-local variable-"
    10. __emit load.s.pri y; // error 001: expected token: "-local variable-", but found "-data offset-"
    11.  
    12. __emit load.s.pri x; // ok
    13. __emit load.pri y; // ok
    14. }

    Кроме того, с оператором __emit компилятор знает, сколько операндов должно быть у той или иной инструкции AMX, и если обнаружит несоответствие, то обязательно укажет вам на него. К примеру, в инструкции const.pri требуется только 1 операнд, поэтому компилятор не позволит вам использовать эту инструкцию без операнда:
    1. // С директивой #emit данный неправильный код скомпилируется
    2. // и, скорее всего, приведёт к генерации невалидного байткода AMX
    3. // (сервер просто откажется загружать такой скрипт).
    4. #emit const.pri
    5.  
    6. // Но с оператором __emit компилятор выдаст ошибку, поскольку требуется операнд (константное значение).
    7. __emit const.pri; // error 001: expected token: "-numeric value-", but found ";"
    8.  
    9. __emit const.pri 0; // ok (целое число)
    10. __emit const.alt cellmax; // ok (встроенная константа cellmax)

    Трудноуловимые ошибки с лишними операндами тоже отлавливаются на раз-два:
    1. __emit const.pri 1;
    2. __emit add 2; // error 001: expected token: ";", but found "-integer value-"
    3. // Суть ошибки выше: лишний операнд в инструкции 'add'
    4. // (для прибавления константного значения используется опкод 'add.c').

    Различаются следующие типы данных:
      Открыть/закрыть

    Название Описание Пример(ы)
    Целое число Любое целочисленное значение
    1. __emit lctrl 6;
    2. __emit jrel (cellbits / charbits * 2);
    Данные Глобальная переменная, либо статическая (static) локальная переменная
    1. static x = 0;
    2. __emit load.pri x;
    Локальная переменная Локальная (расположенная в стеке) переменная или аргумент функции
    1. SomeFunction(x)
    2. {
    3. new y;
    4. __emit load.s.pri x;
    5. __emit load.s.alt y;
    6. }
    Метка Любая метка, на которую можно совершить переход (в т.ч. объявленная внутри блока __emit{})
    1. label1:
    2. __emit jnz label1;
    Pawn-функция Функция, реализованная на языке Pawn
    1. __emit call MyFunction;
    Нативная функция Встроенная функция, предоставляемая интерпретатором
    1. __emit sysreq.c SendClientMessage;
    Любое значение Значение любого из перечисленных выше типов
    1. new x;
    2. __emit {
    3. label1:
    4. label2:
    5. const.pri label1
    6. eq.c.pri label2
    7. add.c x
    8. }

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

    Неотрицательное число Любое целое неотрицательное число
    1. __emit cmps (cellbits / charbits * 128);
    Величина сдвига Величина битового сдвига (целое число от 0 до 31)
    1. __emit shl.c.pri 3; // PRI = PRI << 3
    Размер данных Количество байт для данных, загружаемых/сохраняемых при помощи lodb.i/strb.i (1, 2 или 4)
    1. __emit lodb.i 2; // PRI = [PRI] (2 байта)


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

    Оператор __emit позволяет для аргументов типа "любое значение" использовать выражения.
    В таких выражениях нельзя использовать переменные и вызовы функций, т.к. результатом должно быть константное значение.
    Пример:
    1. __emit const.pri (MAX_PLAYERS + 1);

    Обратите внимание: выражение должно быть взято в круглые скобки - это нужно, чтобы компилятор мог правильно отличить выражение от одиночного аргумента.
    Кроме сложения ("+") в выражениях внутри __emit можно использовать любые другие виды операций, которые доступны в обычных выражениях.
    1. __emit const.pri (((MAX_PLAYERS / 2) & (cellbits / charbits - 1)) * 2 >>> 1);


    Возможность указать метку, объявленную после блока __emit

    Здесь всё просто: с помощью директивы #emit возможно использование инструкций перехода только с метками, объявленными выше места использования, когда с оператором __emit можно делать переходы и на метки, объявленные ниже.
    1. #emit jump label1 // error 017: undefined symbol "label1"
    2. label1:

    1. __emit jump label1; // ok
    2. label1:


    Возможность указать 2 и более операндов в инструкции

    В директиве #emit инструкции могут быть либо только с одним операндом, либо без операндов. В свою очередь, оператор __emit знает точное количество аргументов для каждой инструкции, а потому с ним можно указать любое количество операндов для инструкции (при условии, что это количество правильное). Благодаря этому становится возможным использование макроинструкций, которые предусматривают наличие 2 и более операндов.
    Рассмотрим это на примере того же обмена значениями между переменными:
    1. new a = 4, b = 5;
    2. __emit
    3. {
    4. load.s.both a, b // вместо цепочки 'load.s.pri a\ load.s.alt b' используем макроинструкцию load.s.both
    5. stor.s.alt a // для stor.s.pri/alt нет аналогичной макроинструкции,
    6. stor.s.pri b // поэтому используем их "как есть"
    7. }

    Кто-то скажет, что данная возможность будет бесполезной для пользователей SA-MP, т.к. на сервере нельзя использовать макроинструкции из-за более старой версии интерпретатора Pawn.
    И это могло бы быть правдой, если не ещё одна фича, описанная ниже.


    Автозамена макроинструкций на обычные инструкции

    Если в операторе __emit используются макроопкоды, то при компиляции кода без макрооптимизаций (т.е. с ключами "-O0", "-O1" или "-d2") макроопкоды будут автоматически заменены на обычные опкоды.
    Например, макроопкод 'push5.c' будет заменён на последовательность из пяти 'push.c', а 'sysreq.n' - на 'push.c \ sysreq.c \ stack'.
    Пример:
    1. static const msg[] = "Hi there";
    2. __emit
    3. {
    4. // SendClientMessage(0, 0xFFFFFFFF, msg);
    5. push3.c msg 0xFFFFFFFF 0
    6. sysreq.n SendClientMessage (3 * cellbits / charbits)
    7. }

    При компиляции с флагом -d2 получится следующий байткод:
    1. push.c 00000000
    2. push.c ffffffff
    3. push.c 00000000
    4. push.c 0000000c
    5. sysreq.c 00000000 ; SendClientMessage
    6. stack 00000010


    Универсальные псевдоинструкции

    Начиная с версии компилятора 3.10.10 появились так называемые "универсальные" псевдоинструкции, которые в зависимости от операнда компилируются в разные инструкции AMX.
    Пример:
    1. SomeFunction()
    2. {
    3. new local_var;
    4. static static_local_var;
    5. const CONSTANT_VALUE = 1;
    6.  
    7. __emit load.u.pri local_var; // Скомпилируется в "load.s.pri local_var"
    8. __emit load.u.pri static_local_var; // => "load.pri static_local_var"
    9. __emit load.u.pri CONSTANT_VALUE; // => "const.pri CONSTANT_VALUE"
    10. }

    Основное назначение таких псевдоинструкций - использование в макросах, чтобы обрабатывать любые операнды, указанные пользователем:
    1. #define increase(%0) __emit(inc.u %0) // Увеличивает значение переменной на 1
    2.  
    3. new global_var = 0;
    4. new global_array[2];
    5.  
    6. main()
    7. {
    8. new local_var = 0;
    9. static local_static_var = 0;
    10. static local_static_array[2];
    11.  
    12. increase(global_var);
    13. increase(global_array[1]);
    14. increase(local_var);
    15. increase(local_static_var);
    16. increase(local_static_array[1]);
    17. }

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

    Название Тип операнда Затирает значение в 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
    Благодарности:
    • VVWVV - изначальная реализация оператора __emit. Собственно, благодаря его работе и существует данная статья.
      Y_Less, Slice, Southclaws - предложения по названию оператора и "обновлённому" синтаксису.


    Специально для Pro-Pawn.ru
    Копирование данной статьи на других ресурсах без разрешения автора запрещено!
    Последний раз редактировалось Daniel_Cortez; 26.02.2020 в 22:25. Причина: upd (2)
    Индивидуально в ЛС по скриптингу не помогаю. Задавайте все свои вопросы здесь (click).

  2. 8 пользователя(ей) сказали cпасибо:
    DeimoS (31.12.2017) Desulaid (31.12.2017) Geebrox (31.12.2017) Nurick (31.12.2017) Outsider (31.12.2017) pawnoholic (19.05.2018) speeyx (18.01.2021) VVWVV (31.12.2017)
  3. #2
    Аватар для VVWVV
    ?

    Статус
    Оффлайн
    Регистрация
    09.07.2015
    Сообщений
    731
    Репутация:
    353 ±
    Теперь можно и в pawn 4x такую фичу отправить. Хотя вряд ли ее кто-то ждет.

  4. #3
    Аватар для Daniel_Cortez
    "Это не хак, это фича"

    Статус
    Оффлайн
    Регистрация
    06.04.2013
    Адрес
    Novokuznetsk, Russia
    Сообщений
    2,192
    Репутация:
    2589 ±
    Цитата Сообщение от VVWVV Посмотреть сообщение
    Теперь можно и в pawn 4x такую фичу отправить. Хотя вряд ли ее кто-то ждет.
    Нет, конечно же. Во-первых, я уже говорил о стагнации в Pawn 4.
    Во-вторых, директива #emit была удалена ещё в версии 3.3. Ни в истории изменений, ни в документации (Language Guide, Implementer's Guide) не было никаких объяснений причин данного шага, но менее чем через месяц в код загрузки скриптов была добавлена куча новых проверок для верификации байткода (попытка перекрыть уязвимости с доступом к памяти за пределами секции данных - хотя несколько опкодов, включая PUSH*.s, видимо, забыли, ибо в них адреса доступа не проверялись). Не нужно быть гением, чтобы понять связь между этими двумя изменениями и догадаться, насколько в 4-й версии нужно будет хоть что-то, связанное с #emit.


    UPD: Добавил недостающий пример в секции "Автозамена макроинструкций".
    Последний раз редактировалось Daniel_Cortez; 01.01.2018 в 20:42. Причина: --
    Индивидуально в ЛС по скриптингу не помогаю. Задавайте все свои вопросы здесь (click).

  5. #4
    Аватар для MassonNN
    Пользователь

    Статус
    Оффлайн
    Регистрация
    16.03.2018
    Адрес
    Москва
    Сообщений
    129
    Репутация:
    6 ±
    вопрос следующий, можно ли как-то с помощью emit создать глобальную переменную из функции?

  6. #5
    Аватар для Daniel_Cortez
    "Это не хак, это фича"

    Статус
    Оффлайн
    Регистрация
    06.04.2013
    Адрес
    Novokuznetsk, Russia
    Сообщений
    2,192
    Репутация:
    2589 ±
    Цитата Сообщение от Nestyreff Посмотреть сообщение
    вопрос следующий, можно ли как-то с помощью emit создать глобальную переменную из функции?
    Нет, нельзя (да и зачем такое вообще может понадобиться?) Оператор __emit только подставляет на место использования инструкции Pawn AMX, это не какая-то универсальная "серебряная пуля".
    К слову, я использовал в предложении выше именно "__emit" (да, конкретно этот вариант, с нижними подчёркиваниями в начале), и вам тоже советую использовать только его, т.к. другой вариант - "emit" - будет удалён в следующей версии компилятора.
    Индивидуально в ЛС по скриптингу не помогаю. Задавайте все свои вопросы здесь (click).

  7. #6
    Аватар для vvw
    Пользователь

    Статус
    Оффлайн
    Регистрация
    09.08.2019
    Сообщений
    45
    Репутация:
    9 ±
    Цитата Сообщение от MassonNN Посмотреть сообщение
    вопрос следующий, можно ли как-то с помощью emit создать глобальную переменную из функции?
    Я реализовал первую версию __emit с целью уменьшения числа ошибок при разработке. В целом это все еще тот же #emit, но более строгий и универсальный.

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

  8. #7
    Аватар для MassonNN
    Пользователь

    Статус
    Оффлайн
    Регистрация
    16.03.2018
    Адрес
    Москва
    Сообщений
    129
    Репутация:
    6 ±
    Цитата Сообщение от vvw Посмотреть сообщение
    Я реализовал первую версию __emit с целью уменьшения числа ошибок при разработке. В целом это все еще тот же #emit, но более строгий и универсальный.

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

  9. #8
    Аватар для vvw
    Пользователь

    Статус
    Оффлайн
    Регистрация
    09.08.2019
    Сообщений
    45
    Репутация:
    9 ±
    Цитата Сообщение от MassonNN Посмотреть сообщение
    То есть выделить ячейки из одной функции и использовать это пространство в другой нельзя?
    Можно выделить, но удерживать это пространство будет довольно сложно. Что-то подобное реализовал Y_less в своей библиотеке y_malloc. Там он выделяет большой объем памяти в сегменте стек-куча через #pragma dynamic <число>, чтобы выходной файл не занимал много места на носителе. Поддерживает этот кусок памяти через таймеры.

  10. #9
    Аватар для Daniel_Cortez
    "Это не хак, это фича"

    Статус
    Оффлайн
    Регистрация
    06.04.2013
    Адрес
    Novokuznetsk, Russia
    Сообщений
    2,192
    Репутация:
    2589 ±
    Цитата Сообщение от vvw Посмотреть сообщение
    Что-то подобное реализовал Y_less в своей библиотеке y_malloc. Там он выделяет большой объем памяти в сегменте стек-куча через #pragma dynamic <число>, чтобы выходной файл не занимал много места на носителе. Поддерживает этот кусок памяти через таймеры.
    Эта реализация уже давно устарела и по умолчанию память выделяется из глобального массива в 16 Мб.
    Индивидуально в ЛС по скриптингу не помогаю. Задавайте все свои вопросы здесь (click).

  11. #10
    Аватар для vvw
    Пользователь

    Статус
    Оффлайн
    Регистрация
    09.08.2019
    Сообщений
    45
    Репутация:
    9 ±
    Цитата Сообщение от Daniel_Cortez Посмотреть сообщение
    Эта реализация уже давно устарела и по умолчанию память выделяется из глобального массива в 16 Мб.
    Это в новой версии?

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

 

 
Страница 1 из 2 1 2 ПоследняяПоследняя

Информация о теме

Пользователи, просматривающие эту тему

Эту тему просматривают: 1 (пользователей: 0 , гостей: 1)

Ваши права

  • Вы не можете создавать новые темы
  • Вы не можете отвечать в темах
  • Вы не можете прикреплять вложения
  • Вы не можете редактировать свои сообщения
  •