Описание платформы Gide
Содержание |
Введение
Роль скриптов в проекте
Многие объекты виртуального мира обладают некоторым специфичным поведением. Например, при нажатии на кнопку зажигается лампочка, а для открытия некоторых дверей требуется ключ (пароль). Для того, чтобы задать специфичное поведение объекту, предусмотрено два способа:
- написание плагина к физическому движку Диптауна, расширяющего стандартное дерево классов объектов;
- написание скрипта на специализированном языке программирования.
Еще одна важная область применения скриптов - т.н. процедурные модели и текстуры. Суть их заключается в следующем.
Как известно, классически текстура задается в виде массива чисел, каждое из которых определяет цвет соответствующей точки. Даже при достаточно хорошем сжатии, текстуры имеют большой размер и их очень "дорого" передавать по сети. Альтернативой являются процедурные текстуры. Процедурная текстура задается некоторой программой (скриптом), которая рисует изображение. При этом по сети передается относительно маленькая программа, которая затем исполняется на машине пользователя, а полученное изображение используется в качестве текстуры.
Аналогично, процедурная модель - это скрипт, создающий трехмерную модель.
Итак, скрипты в проекте используются, в основном, в трех направлениях:
- задание поведения объектов;
- создание процедурных моделей;
- создание процедурных текстур.
Терминология
В этом документе будут использованы следующие термины:
- Gide (гайд) - единый низкоуровневый язык;
- байт-код gide - бинарный код, получаемый после компиляции кода gide;
- виртуальная машина gide - программа, исполняющая байт-код.
Основные концепции
Для различных задач удобны различные языки и различные наборы функций. Поэтому, во избежание повторного написания низкоуровневых библиотек, а также для удобства программирования скриптовых языков, было решено разработать единый низкоуровневый язык - gide. Код любого другого языка компилируется в код gide, затем компилятор gide создает платформенно-независимый байт-код, который исполняется виртуальной машиной gide. Механизм компиляции и исполнения кода изображен на рис. 1.
Преимущества такого подхода очевидны:
- использование библиотек одного языка другими;
- единая виртуальная машина для всех языков.
Язык gide берет за основу модульную архитектуру. Модули gide - это динамически подключаемые библиотеки, которые бывают двух типов:
- библиотеки, представляющие собой байт-код gide (управляемые модули);
- "обычные" динамически-подключаемые библиотеки (неуправляемые модули).
Любые из этих библиотек можно подключать как статически, так и динамически. Область задач gide:
- расчет процедурных текстур, объектов и, возможно, звуков;
- управление поведением объектов в виртуальном пространстве;
- управление объектами интерфейса на сервере интерфейса.
Gide должен предоставлять максимально удобный механизм управления "внешними" объектами. Набор таких объектов определяет набор решаемых задач, т.е. некий контекст того или иного скрипта.
Модель языка
Основополагающим понятием в модели Gide является объект.
Объект - это некоторая абстрактная сущность, для которой определены свойства (поля) и операции (методы). Свойства объекта также являются (именованными) ссылками на объекты. И набор свойств, и набор операций - это динамические наборы, т.е. наличие того или иного свойства/операции определяется в момент выполнения, а не в момент компиляции. Gide позволяет оперировать ссылками на объекты. То есть единственный существующий в нем тип данных - это ссылка на объект. Ссылка может либо указывать на объект, либо быть равной 0.
Набор методов объекта определяется его классом.
Класс - это набор функций, каждая из которых имеет доступ к объекту, для которого она была вызвана. У класса может быть один или несколько родительских классов: при поиске метода, он ищется сначала в классе объекта, а затем - рекурсивно - во всех родительских классах, в порядке их перечисления.
Функция, в свою очередь - это некоторый набор операторов, выполняющих определенные действия и составляющие тело функции. Функция может быть методом какого-либо класса, либо отдельной функцией, не являющейся методом. Функция может принимать некоторое количество параметров и возвращать результат (объект).
Набор классов, предоставляемых скрипту, формирует так называемый контекст исполнения, т.е. по сути стандартную библиотеку языка.
Эта библиотека, для тех или иных вариантов использования, может включать в себя:
- объекты стандартных типов данных:
- целые числа,
- вещественные числа,
- строки,
- идентификаторы объектов,
- указатели,
- массивы;
- стандартные операции для этих объектов:
- арифметические операции,
- операции сравнения,
- математические функции,
- и пр.
Синтаксис языка gide
Синтаксис языка Gide максимально упрощен - с рассчетом на то, что поверх него будут писаться реализации языков более высокого уровня.
Каждая непустая строка исходного кода gide - это некоторая инструкция, записанная в следующей форме:
ключевое_слово аргумент1, аргумент2, ..., аргументN
Некоторые инструкции допустимы только за пределами тела функции, некоторые, наоборот, допустимы только внутри тела функции.
Аргументы для инструкции - это произвольные текстовые строки. Если значение аргумента содержит управляющие символы (пробел, табуляция, перевод строки, запятая, символы # или ") - его необходимо заключить в двойные кавычки. Внутри таких кавычек также допустимы escape-последовательности \r, \n, \t, \" и \\.
ЗАМЕЧАНИЕ: вставка произвольных символов \xNN на данный момент не поддерживается, но запланирована на будущее.
Весь текст от символа # и до конца строки - комментарий, он игнорируется.
Внутри тела функции также возможно вставлять т.н. метки. Метка - это некоторая уникальная в пределах данной функции ссылка на инструкцию. Метка выглядит следующим образом:
имя_метки:
Она может быть записана как на отдельной строке, так и на строке с инструкцией.
Подключение внешних модулей
Для использования классов и функций, объявленных во внешних модулях (любого типа), необходимо их подключить, используя инструкцию use. Она записывается следующим образом:
use адрес
Для неуправляемых модулей, адрес - это просто имя такого модуля.
Для управляемых модулей, адрес - это префикс 'gbc:' плюс URL-адрес потока модуля. Например, "gbc:diss:/lib/module.gbc".
Зависимости модулей - рекурсивные. Другими словами, если модуль B подключил модуль A, а модуль C подключил модуль B, то из модуля C будут доступны объявления модуля A.
Объявление классов
Для того, чтобы объявить класс, нужно написать следующую инструкцию:
class имя_класса
Имя класса - это произвольная строка. Данная инструкция объявляет класс, в котором еще нет ни одного метода.
Инструкция
inherit имя_класса, имя_родителя
добавляет класс имя_родителя в качестве родительского класса. Она должна быть записана после объявления класса. В этой инструкции, и класс имя_класса, и класс имя_родителя могут быть объявлены во внешнем модуле.
Существует также понятие родителя по-умолчанию. Они могут быть заданы только в неуправляемых модулях. Такие родители добавляются автоматически, если не указаны другие родительские классы.
Объявление глобальных переменных
Для того, чтобы объявить глобальную переменную - т.е. переменную, доступную из любой функции - нужно написать инструкцию
global имя_переменной
В качестве имени переменной, опять же, может выступать любая строка.
Глобальные переменные доступны только в пределах данного модуля. Для получения их значений из других модулей, следует использовать функции.
По-умолчанию все глобальные переменные инициализируются нулем.
Объявление функции
Функция объявляется следующим образом:
function имя_функции, аргумент1, аргумент2, ..., аргументN тело функции end
Имя функции - это произвольная текстовая строка. Если имя имеет вид
имя_класса:имя_метода
, метод имя_метода добавляется в класс имя_класса (этот класс должен быть предварительно объявлен, но не обязательно в текущем модуле). В противном случае, объявляется функция, а не метод.
Аргументы - это формальные параметры функции, они доступны в теле как локальные переменные.
Список аргументов в какой-то мере условен. При вызове функции всегда можно указывать произвольное число параметров, это нигде не проверяется. Если фактических параметров меньше, чем формальных - оставшиеся аргументы будут инициализированы нулем. Если фактических больше - некоторые из них не будут доступны напрямую, однако стандартная библиотека может предоставлять функции, открывающие к ним доступ.
Методы класса могут быть публичными (public), защищенными (protected) или частными (private). Публичные методы можно вызывать без ограничений, защищенные - только из данного класса и его потомков, частные - только из данного класса. Эти проверки делаются во время исполнения.
По-умолчанию, метод является публичным. Для объявления защищенных и частных методов, нужно заменить ключевое слово function в объявлении метода на func_protected и func_private соответственно.
Если метод/функция уже был объявлен ранее, он переобъявляется - т.е. данная реализация перекрывает предыдущую. Вызвать предыдущую реализацию можно при помощи оператора recall (см. ниже).
Функция со специальным именем @@module_init является конструктором модуля; она вызывается в момент загрузки данного модуля. В ней, в частности, можно инициализировать глобальные переменные модуля.
Метод @@init - конструктор класса. Он вызывается в момент создания объекта данного класса. Порядок вызова конструкторов таков: сначала рекурсивно вызываются конструкторы родительских классов в порядке объявления родителей, затем - конструктор данного класса. В конструкторе, в частности, можно инициализировать поля объекта.
Специальные переменные
Существует 6 зарезервированных имен переменных:
- 0 (число ноль) - означает отсутствие объекта;
- @true - указывает на специальный объект "истина". Этот объект не имеет полей и методов; он бывает полезен для написания логических конструкций;
- @false - то же, что и 0;
- @this - внутри функции-метода, указывает на текущий объект; внутри обычной функции равен 0;
- @result - результат последней вызванной функции или оператора access;
- @exception - объект исключения, которое требуется обработать.
Операторы функции
Операторы - это управляющие инструкции, составляющие тело функции.
Поскольку единственный существующий тип переменной - это указатель на объект, значительная часть операторов не требуется. Все операторы реализуются на уровне самих объектов в виде методов.
Существуют следующие операторы:
new переменная, имя_класса, строка
- создает новый объект класса имя_класса и записывает результат в указанную переменную. Если переменная была объявлена ранее - результат записывается в нее; если нет - создается новая локальная переменная (т.е. переменная, доступная только в пределах данной функции). Третий параметр - это некоторые данные для инициализации объекта. Они имеют значение только для некоторых классов в неуправляемых модулях. Кроме того, любой класс может быть инициализирован пустой строкой.
bless переменная, имя_класса
- эквивалент new без последнего параметра.
mov переменная, источник
- копирует в переменную переменная ссылку на существующий объект источник (аналогично, если переменная не была объявлена ранее - создается новая локальная переменная). Важно подчеркнуть, что при этой операции не создается нового объекта; две переменные будут указывать на один и тот же объект.
goto имя_метки
- осуществляет безусловный переход к оператору, помеченному меткой имя_метки.
if переменная, имя_метки
- осуществляет условный переход к оператору имя_метки: если управляющая переменная (первый параметр) имеет отличное от нуля значение, осуществляется переход; в противном случае этот оператор игнорируется и выполнение продолжается со следующей инструкции.
call переменная, имя_функции, аргумент1, ..., аргументN
- если переменная - 0, вызывает функцию имя_функции; в противном случае вызывает метод объекта, на который ссылается переменная. Как уже говорилось ранее, количество аргументов может быть любым вне зависимости от количества формальных параметров. Результат выполнения функции записывается в специальную переменную @result.
- имя_функции может быть задано в форме имя_класса:имя_метода. В этом случае осуществляется вызов метода конкретного класса, а не текущего (имя_класса должно либо совпадать с классом объекта, либо быть его родительским классом). Это позволяет вызывать методы родительских классов, несмотря на перекрытие в дочерних классах.
return переменная
- осуществляет выход из текущей функции с возвратом объекта, на который ссылается переменная. Если выполнение функции доходит до конца, функция возвращает 0.
access переменная, имя_поля
- получает значение поля имя_поля объекта, на который ссылается переменная. Значение записывается в специальную переменную @result. Если указанного поля в классе нет, в @result записывается 0.
mutate переменная, имя_поля, значение
- записывает объект, на который ссылается значение, в поле имя_поля объекта, на который ссылается переменная. Если такого поля ранее не существовало, оно создается.
recall
- вызывает замещенную функцию с теми же параметрами, которые были переданы данной функции; осуществляет выход из текущей функции с результатом, который вернула замещенная функция.
upgrade переменная, имя_класса
- расширяет объект, на который ссылается переменная, до класса имя_класса. Это делается путем вызова конструкторов классов, которые в дереве иерархии находятся между классом переменной и указанным классом. Очевидно, класс объекта должен быть родительским для класса имя_класса. После выполнения данной инструкции, класс объекта станет равным имя_класса, т.е. для него будут доступны все методы этого класса.
use_locals имя_функции
- открывает данной функции доступ к локальным переменным функции имя_функции. Этот оператор может быть указан только в самом начале тела функции и только единожды. имя_функции записывается как при объявлении: для обычной функции - просто ее имя, для метода класса - имя класса и имя метода, разделенные двоеточием. Для того, чтобы локальные переменные реально стали доступны, функция имя_функции должна вызвать текущую функцию специальным образом.
- ПРИМЕЧАНИЕ: такой специальный вызов на данный момент невозможно сделать напрямую. Класс Closure стандартной библиотеки позволяет создать указатель на метод, при вызове которого (через этот указатель) происходит требуемый тип вызова. В будущем планируется добавить оператор вызова ccall, выполняющий эту операцию.
- данная инструкция требуется для реализации замыканий.
Кроме перечисленных, существуют еще операторы для работы с исключениями; они рассмотрены в следующем подразделе.
Обработка исключений
Исключения - это специальный механизм, предназначенный для передачи информации об ошибках. Он заключается в следующем.
Любая функция может бросить исключение. Само исключение - это некоторый объект произвольного класса. Когда такое происходит, виртуальная машина начинает искать ближайший обработчик исключений. Если сама функция, бросившая исключение, может его обработать - исполнение передается соответствующей инструкции. В противном случае, начинается раскрутка стека вызовов до первой функции, которая может обработать исключение. Если ни одна функция этого сделать не может, выполнение программы прекращается, а вызвавшему модулю сообщается о необработанном исключении.
В функции может быть установлено несколько обработчиков исключений, которые формируют стек. Обработчик - это некоторая метка в пределах функции. Когда функции требуется обработать исключение, управление передается оператору, на который ссылается эта метка, а сама эта метка извлекается из стека обработчиков. Другими словами, в функции может быть установлено несколько обработчиков, которые, при необходимости, будут вызываться в порядке, обратном их установке. Если же возникает исключение, а в функции не установлено ни одного обработчика - происходит выход из функции, а исключение передается функции, которая вызвала данную функцию, и т.д.
Для работы с исключением существуют следующие четыре оператора:
throw переменная
- кидает исключение, передав в качестве параметра объект, на который ссылается переменная.
except_push имя_метки
- добавляет метку имя_метки в стек обработчиков исключений для данной функции. Когда возникает исключение, управление передается указанной метке, а объект исключения записывается в специальную переменную @exception.
except_pop
- изымает из стека обработчиков метку, находящуюся на вершине стека.
except_clr
- очищает стек обработчиков исключений для данной функции.
Кроме оператора throw, исключения могут кидаться неуправляемыми модулями. Кроме того, существует механизм, при помощи которого ошибки, возникающие на уровне самой виртуальной машины, передаются в стандартную библиотеку, которая кидает исключение.
Формат байт-кода gide
ПРИМЕЧАНИЕ: в данном разделе приведена устаревшая информация, которая не соответствует действительности. Раздел оставлен для ознакомления с общей концепцией хранения байт-кода. Через некоторое время он будет переписан.
Байт-код gide представляет собой популярный BER-формат: весь файл разбит на секции, каждая из которых начинается с описания ее типа и размера.
Следует сразу оговориться, что все числа в gide представляются в формате big endian (т.е. наименее значимый байт впереди).
Заголовок BER-секции представляет собой последовательность кода секции (1 байт) и ее размер в BER-формате.
Все целые числа в байт-коде, если это не оговорено специально, представляются в BER-формате: для записи числа требуется столько байт, сколько нужно; в каждом байте записывается 7 битов числа. Если самый старший бит равен 1, требуется прочитать/записать следующий байт. Если старший бит равен 0 - это последний байт, описывающий число.
Секция имен
Тип секции - код символа N.
Для большей компактности кода используется секция имен. В ней последовательно записываются все текстовые строки (имена переменных/функций, данные конструкторов и пр.), которые встречаются в скрипте. Одинаковые текстовые строки записываются только один раз. Таким образом, каждая текстовая строка получает свой уникальный номер, и в байт-коде вместо строк используются эти номера.
Поскольку в скрипте часто встречаются одинаковые строки (например, работа с одной и той же переменной), это сильно уменьшает размер кода.
Все строки записываются следующим образом:
- длина строки (в BER формате, см. выше);
- текст.
Нумерация строк начинается не с нуля, а с 16. Меньшие номера зарезервированы для стандартных имен:
- Переменная @null (она же 0) имеет номер 0;
- Переменная @this имеет номер 1;
- Переменная @false имеет номер 2;
- Переменная @true имеет номер 3.
В случае обращения к полю объекта, в байт-код записывается все составное имя объекта (вместе с разделяющими точками). Виртуальная машина сама разбирает составные имена.
Секция экспорта
Тип секции - ASCII-код символа E
Данная секция предназначена для упрощения навигации по секции кода.
Для каждой функции, описанной в модуле, записывается:
- индекс имени функции;
- смещение функции относительно начала секции кода.
Секция кода
Тип секции кода - ASCII-код символа C.
В эту секцию записываются тела функций Gide.
Для каждой функции записывается:
- индекс имени функции (он должен быть смещен от начала секции кода на количество байт, указанных в секции экспорта);
- количество аргументов функции;
- индексы имен аргументов функции;
- размер блока операторов;
- для каждого оператора:
- индекс имени оператора;
- количество передаваемых параметров функции (только для оператора call);
- индексы имен всех параметров.
Индексы операторов:
- Простые операторы имеют номера от 1 до 9 в порядке их перечисления в п.2.1;
- Оператор создания объекта имеет номер 10;
- Оператор throw имеет номер 13;
- Оператор except_push имеет номер 16;
- Оператор except_pop имеет номер 17;
- Оператор создания переменной пользовательского класса имеет номер 11;
- Оператор расширения наследования имеет номер 15;
- Оператор use имеет номер 12;
- Оператор external номер 14;
Секция импорта
Тип секции импорта - ASCII-код символа I
В эту секцию записываются подключаемые операторами use и external библиотеки.
Для каждой библиотеки записывается:
- число 1 в случае оператора external, 0 - в случае use;
- индекс пути к библиотеке.
Секция наследования классов
Тип секции - код символа H.
В эту секцию записывается статическая таблица наследований классов.
Для каждого оператора inherit записывается:
- индекс наследующего класса;
- индекс наследуемого класса.
Секция номеров строк
Тип секции - код символа D.
Эта секция не является обязательной. В ней записываются соответствия операторов и номеров строк в исходных файлах, на которых указан соответствующий оператор.
В секцию записывается массив следующих структур:
- индекс имени файла;
- размер блока функций;
- для каждой функции:
- индекс имени функции;
- размер блока операторов;
- для каждого оператора:
- номер оператора в теле функции;
- номер строки кода, на которой объявлен оператор.