Ты ещё скажи, что ты сам свой движог написал.
Ну, до С-синтаксиса я далеко не дотянул. Я ориентировался на задачу попроще - НЕ на написание новых функций для программы, а на использование уже встроенных в ехе-шник функций.
Плюс для внедрения моего скриптового движка в проект нужно лишь инклюднуть хеадер - никаких библиотек и ДЛЛек не требуется.
Сделаю вид, что не заметил удара под дых от Трёхмерного, и продолжу рассказ о своём скриптовом движке.
Глава 2 - правила синтаксиса и возможности моего ScriptEngine.
Скрипт состоит из логических фраз двух типов: описание переменных и вызовы функций. Компиллируются в конечную процедуру лишь вызовы функций, а переменные создаются и инициализируются во время компилляции.
Вызов функции имеет С-шный синтаксис - имя функции и аргументы в круглых скобках:
FuncName(Arg1, Arg2, Arg3)
Точкозапятых не надо нигде. Названия функций и переменных могут содержать пробелы, точки, минусы, плюсы, знаки равенства и тильды и многие другие символы, отличные от функциональных (пока что это круглые, квадратные, треугольные и фигурные скобки, а также двойная кавычка и запятая) - главное, чтобы имя не могло быть интерпретировано компиллятором, как число.
Фразы создания переменных состоят из трёх частей: название, аллокатор и инициализатор.
Аллокатор - это число в квадратных скобках сразу после названия переменной. Аллокатор показывает размер переменной В БАЙТАХ (а точнее, размер буффера, отводимого под переменную). Пример создания стобайтного буффера и ассоциированного с ним имени:
MyBuffer [100]
Пробелов между именем и аллокатором можно не ставить.
Инициализатор идёт сразу за аллокатором и содержит данные для заполнения созданного буффера. Инициализаторы у меня двух видов: текстовый и числовой. Текстовый инициализатор - это текст в двойных кавычках. Всё, что между кавычками ("...") а еще плюс ноль на конце, пихается в буффер. Пример создания указателя на текстовый буффер:
MyText "Hello, world!"
А если хотите явно задать размер буффера под строку с текстом:
MyText [100] "Hello, world!"
Причём если аллокатор слишком маленький, и весь текст с нулём на конце не влезет в буффер заданного размера, то размер буффера будет определён автоматически. Как и в случае, если аллокатор не дан вообще.
Поскольку символы от первой кавычки до второй не сканируются, то символы переноса строки (если текст разбит в файле скрипта на несколько строк ) так и войдут в буффер (это два байта: '\r', '\n').
Числовой инициализатор - это перечень значений, разделённых запятыми и взятый в фигурные скобки:
MyVars { -1, +2.E+0, 0x3, 0x100b }
Я специально показал 4 распознаваемых моим компиллятором варианта записи чисел: десятичное целочисленное, десятичное с плавающей запятой, шестнадцатеричное, двоичное. Плавающие запятые переводятся в float, все остальные - в int.
Как и в случае текстовых инициализаторов, допускается задать бОльший буффер, чем требуется для впихивания перечисленных данных (рассчёт у моего компиллятора простой -по 4 байта на "аргумент").
Числовой инициализатор пофункциональнее текстового, поскольку позволяет включать смешанные аргументы:
MyMixedBuffer { 123, MyVars, "This is text", [100], {1.0, 2.0, 3.0} }
В данном случае, будет создан не один буффер MyMixedBuffer, а ещё три дополнительных - один с текстом, другой с "мусором", третий - с тремя флоатами. И лишь указатели на них войдут в MyMixedBuffer. Т.е. его размер в реале будет 5*4=20 байт, а содержимое будет состоять из числа (первые четыре байта) и 4-х указателей (остальные четвёрки байт). Отмечу, что создаваемые три буффера можно было бы и назвать, чтобы указатели на них можно было использовать в дальнейшем коде скрипта.
Подитожу синтаксис создания переменных. Объявление новой переменной состоит из имени, ассоциированного с указателем на буффер, в котором переменная находится; аллокатора, задающего размер буффера; инициализатора, заполняющего буффер переменной начальными данными НА МОМЕНТ КОМПИЛЛЯЦИИ, т.е. один раз, перед тем, как скрипт будет в первый раз запущен.
Если дано голое имя без аллокатора и без инициализатора, то это будет воспринято компиллятором как указатель на уже существующую переменную. Если есть и(или) аллокатор и(или) инициализатор, переменная будет создана. Имя переменной существует лишь во время компилляции и не добавляется в список имён наравне с теми, что добавляет программист с помощью функции ScriptEngine.Register(...), однако это можно сделать с помощью уже встроенной функции Declare_pi(const char* Name, int Value):
Declare_pi("MyVars", MyVars)
После ИСПОЛНЕНИЯ скрипта имя объявленной в скрипте переменной появится в общем списке и станет доступным для использования в скриптах, скомпиллированных после прогона этого. В дополнение есть ещё функция NewVar_pi(const char* Name, int Size), создающая буффер указанного размера и добавляющая указатель на него в общий список под указанным именем (если такое имя уже используется, переменная не будет создана). Пример:
NewVar_pi("NewBuffer", 100)
В заключение добавлю, что буффера под все переменные создаются с помощью функции malloc и указатели на создаваемые переменные автоматически добавляются в список внутри ScriptEngine, который освобождает память, вызывая free для каждого указателя в списке. Это случается перед закрытием программы.
Теперь о функциях. Парочку я добавил сам ("встроил" в ScriptEngine; с ними я Вас познакомлю позже). Остальные добавляет программист, вот так:
void __stdcall MyFunc(Arg1,Arg2/*И т.д.*/){
//Трям-парям-паря-па-пам
}
//И где-до перед началом компилляции скриптов...
ScriptEngine.Register("MyFunc",MyFunc);
И точно так же объявляются указатели на переменные, а можно даже просто константы так регистрировать. Грубо говоря, это регистрация чисел - текстовое имя и ассоциированое с ним число. Скриптописатель пишет имя, компиллятор при компилляции ищет в списке соответствующее этому имени число. С точки зрения функций, регистрируется физический адрес начала кода функции; с точки зрения переменных, регистрируется адрес памяти, где переменная хранится. Обратите внимание, что при регистрации адреса функции НЕ нужна закорючка &, а если регистрируется переменная, то да:
static int SomeInt;
ScriptEngine.Register("SomeInt",&SomeInt);
При регистрации адресов переменных будьте осторожны с динамическими и статическими адресами: если переменная обявляется внутри функции, её адрес динамический (если явно static не написать), т.е. во время повторного вызова этой функции переменная вовсе не обязательно будет создана в той же ячейки памяти.
Адреса же функций всегда статические. Но тут нужно быть осторожным со стандартом вызова функции - есть __cdecl и __stdcall. Мой ScriptEngine умеет вызывать только __stdcall - это стандарт ОпенГЛевских функций, а также любимый стандарт моего Борланда (стоит по-умолчанию). Если зарегистрировать функцию, которая __cdecl и вызвать её в скрипте, то эффект будет тот же, как если запихать неверное количество аргументов в функцию - в лучшем случае, красный крест.
Плюс ещё нельзя давать скриптописателям на вооружение функции, возвращающие классы, float'ы и double'ы - только int'ы, char'ы и указатели (тут уже не важно на какой тип данных - физически это тупо-адрес).
Ещё добавлю, что вызов незарегистрированной функции (или зареганой, но написанной в скрипте с ошибками) просто не компиллируется - его тупо не будет в функции скрипта. А значения незареганных переменных (указателей) равняются нулю.
Ах да, комменты. Начинаются решёткой (#) и кончаются там, где начинается новая строка.
Ещё можно делать маркеры в скрипте:
<MyLabel>
Маркеры - это адреса внутри скомпиллированной функции скрипта. Поскольку она состоит только из вызовов функций, то маркер ассоциируется с той точкой кода, где начинается вызов следующей функции. Таким образом, самый первый маркер совпадает с адресом самОй функции скрипта.
Маркеры можно вызывать:
MyLabel()
И даже регистрировать, как любые другие переменные:
Declare_pi("MyLabel", MyLabel)
Т.е. по сути, в одном скрипте можно таким образом создать несколько точек входа. Но раз можно делать несколько точек входа, хотелось бы и несколько точек выхода, т.е. как насчёт return'a? Ну, один добавляется автоматически компиллятором после вызова последней функции.
...Поскольку мой компиллер умеет компиллировать лишь вызовы функций, то и возврат из функции скрипта также осуществляется посредством вызова функции! Эта функция встроенная, и жёстко написана на ассемблере. Причем есть несколько вариантов выхода - тупо выход:
Return()
И выход при условии, что возвращённое
предыдущей функцией значение равно или не равно нулю:
ReturnIfZero()
ReturnIfNotZero()
Я пошёл чуть дальше, и добавил ещё и прыжки к указанному маркеру:
Jump_p(MyLabel)
JumpIfZero_p(MyLabel)
JumpIfNotZero_p(MyLabel)
Отличие прыжка к маркеру от прямого вызова маркера в том, при прыжке процессор больше не возвращается в точку, откуда прыгнул, а при вызове - возвращается, нарвавшись на Return. Т.е. Return после
вызова маркера вернёт проц в точку после вызова маркера и исполнение скрипта продолжится, а после Jump'a к маркеру, следующий Ретурн выкинет проц из самОй функции скрипта.
Еще одна ассемблнутая "хакерская" встроенная функция - это SaveResult_p(int* Pointer). Эта функция записывает значение, возвращённое предыдущей функцией. Аргумент - указатель на переменную (размером в 4 байта минимум), куда записать возвращённое значение. Да, немножко завёрнуто и неудобно писать вместо простого и привычного a = Func(b) фразуху вида
Func(b)
SaveResult_p(a)
но сделайте скидку на примитивность языка и простоту компиллятора.
В заключение познакомлю ещё с двумя встроенными функциями. Это функция копирования, аналогичная стандартной memcpy:
Source{123}
Destination[4]
Copy_ppi(Destination,Source,4)
А также функция, компилирующая скрипт, находящийся в указанном файле, и добавляющая адрес полученной функции в регистрационный список, ассоциируя его с указанным именем:
Compile_pp("Folder1//Folder2//Superscript.txt", "Superscript")
---
Пока что всё. Интересно было бы узнать, не взругается ли Вижуалка, если инклюднуть в проект хидер с моим скриптовым движком -
#include "Scripts.h"