Поскольку RTCG в перспективе должен стать основным инструментом, используемым в разработки пакетов, то имеет смысл написать небольшой цикл статей-лекций, вводящий в курс дела в первую очередь тех, кто еще не знаком с кодогенерацией данного типа, и тех, кто с ней знаком на примере FTCG. Основное внимание будет уделено не синтаксису скрипта (который можно узнать, потратив 5 минут на чтение спецификации), а его архитектуре и взаимодействию со средой и ее компонентами.
Примеры, приводимые по ходу подачи информации в основном будут относиться к пакету Lazarus, но могут быть перенесены на другие пакеты с точностью до целевого языка.
Общая архитектура
Ниже представлена схема основных блоков кодогенератора RTCG и ее сравнение с кодогенератором прошлого поколения:
Архитектура кодогенераторов FTCG и RTCG
Пояснение к блокам:
- Точка входа - так называется элемент на схеме, с которого начинается любая кодогенерация в пакетах на базе *TCG. Название элемента может быть любым, но его скрипт обязан содержать как минимум ф-цию doStart, на которую и передается управление после начала процедуры сборки проекта
- Обход элементов - это процедура последовательного вызова функций в скриптах элементов в соответствии с их подключением на схеме. Поскольку процесс рекурсивный (т.е. каждый элемент вызывает метод другого элемента самостоятельно), то их "лобовое" кольцевание на схеме в кодогенераторах *TCG исключено и приводит к ошибке
- Загрузка скрипта - загрузка текста скрипта с диска в оперативную память
- Парсинг скрипта - разбор синтаксиса скрипта и формирование байт кода для его дальнейшего исполнения
- Выполнение скрипта - в FTCG выполнение скрипта совмещено с парсингом и выполняется для каждого метода каждого элемента при каждом вызове, в RTCG выполняется байт код, полученный на предыдущем шаге
- Контекст элемента - под этим термином понимается наличие некоторого набора переменных, значения которых зависят от того, для какого элемента в данный момент выполняется скрипт
- Обход свободных элементов - блок аналогичен блоку Обход элементов с той лишь разницей, что кодогенератор самостоятельно в цикле обходит все элементы схемы, никак не связанные с другими элементами и вызывает для каждого из них метод с названием init
- Создание кода из блока Result - завершающая операция, которая получает содержимое блока Result ввиде текста и отправляет его в качестве результата кодогенерации в среду.
Отличия в архитектурах FTCG и RTCG
Как можно заметить по схеме отличий не так уж и много:
- пользовательские расширения кодогенератора заменены на системный юнит с именем Sys, который парсится один раз перед вызовом метода doStart
- этапы парсинга и выполнения кода элемента разнесены в отдельные блоки, причем парсинг выполняется только один раз при первом обращении к элементу
- контекст элемента в FTCG автоматически выставлялся только при вызове метода другого элемента оператором event, теперь же за правильностью контекста следит сам кодогенератор и никакого ручного вмешательства не требуется
Системный модуль Sys
Как уже было отмечено выше это модуль, который призван заменить файл direct.inc, встраиваемый непосредственно в кодогенератор и компилируемый вместе с ним. Основное его назначение - реализовать функционал, который будет описывать некоторые особенности целевого языка, необходимые кодогенератору для правильного функционирования.
#hws
// вызывается один раз при загрузки модуля
func create(entry)
// настройка кодогенератора
// создание блоков
// регистрация типов и т.д.
end
// вызывается один раз при выгрузки модуля
func destroy(entry)
// уничтожение блоков
end
// вызывается всякий раз при необходимости привести выражение value к типу type
func to_type(value, type)
end
// пользовательский не обязательный метод
func my_proc()
// что-то с чем-то делаем
end
Кроме того в данном модуле можно разместить любое количество пользовательских методов, доступ к которым есть в любом скрипте через объект sys:
#hws
sys.my_proc()
Типичная структура пакета
В простейшем случае весь цикл процессов от нажатия пользователем кнопки Build в среде, до получения кода целевого проекта можно уложить в несколько простых шагов:
- вызов метода sys.create()
- вызов метода EntryPoint.doStart() (где EntryPoint это основной, родительский, элемент текущего проекта)
- последовательный вызов методов всех соединенных друг с другом элементов (если они есть)
- вызов метода sys.destroy()
При этом результат работы кодогенератор ожидает в блоке с именем "result" - как, чем и какими судьбами формируется код в этом блоке - ему совершенно не важно. Поэтому в самом простом случае полностью рабочий пакет на RTCG можно сделать только из одного элемента с одной строкой кода:
#hws
block.reg("result").println('void main() { }')
Очевидно, что для полноценного пакета этого не достаточно. Даже при наличии совсем простого целевого языка весь будущий код приложения необходимо собрать по кусочкам, слепленным из разных элементов схемы. Т.е. встает задача формирования отдельных сегментов, которые собираются в конечное цельное приложение только в самом конце всего процесса. Именно на этом принципе и построены все пакеты *TCG - в самом начале создается множество блоков, отвечающих за накопление переменных, объявление и реализацию методов, за код инициализации и код деинициализации и т.д. и т.п. и только в конце формируется тот код, который уже пригоден для непосредственной компиляции.
Рассмотрим эту архитектуру подробнее на примере пакета Lazarus с целевым языком Pascal. Пакет построен таким образом, что любой уровень вложенности схемы (в том числе и самый первый, в котором расположен основной элемент проекта) представляет из себя unit с одним классом. Например, код пустой схемы с одной формой имеет такую структуру:
#pas
// --- начало блока BODY
Unit hiMainForm1;
interface
// --- начало блока UNITS
uses Classes, SysUtils, FileUtil, LResources, Forms, Controls, StdCtrls;
// --- конец блока UNITS
type
TMainForm1 = class(TForm)
protected
procedure DoCreate; override;
procedure addWidget(widget:TControl; x,y,w,h:integer);
public
// --- начало блока PRIVATE VARS
{ для пустого проекта эта секция останется пустой }
// --- конец блока PRIVATE VARS
// --- начало блока METHODS INT
{ для пустого проекта эта секция останется пустой }
// --- конец блока METHODS INT
end;
implementation
procedure TMainForm1.addWidget(widget:TControl; x,y,w,h:integer);
begin
widget.parent := self;
widget.left := x;
widget.top := y;
widget.width := w;
widget.height := h;
end;
procedure TMainForm1.DoCreate;
// --- начало блока LOCAL VARS
{ для пустого проекта эта секция останется пустой }
// --- конец блока LOCAL VARS
begin
Caption := 'Form1';
// --- начало блока INIT
{ для пустого проекта эта секция останется пустой }
// --- конец блока INIT
end;
// --- начало блока METHODS IMP
{ для пустого проекта эта секция останется пустой }
// --- конец блока METHODS IMP
end.
// --- конец блока BODY
- UNITS используемые системные модули
- PRIVATE VARS переменные, доступные любому элементу схемы в рамках текущего уровня вложенности
- METHODS INT заголовки методов
- METHODS IMP реализации методов
- LOCAL VARS локальные переменные, доступные только одному конкретному элементу
- INIT секция инициализации
С точки зрения кодогенератора весь процесс происходит следующим образом:
- создается 6 описанных выше блоков
- вызывается метод onStart, в котором по цепочке выполняются методы всех элементов схемы
- элементы добавляют в блоки нужные для их работы куски кода
- все 6 блоков склеиваются в один блок BODY в соответствии с шаблоном выше
- блок BODY сохраняется на диск ввиде готового unit-а
- все блоки удаляются
Посмотрим, как будет выглядеть код unit-а главной формы, если в схему добавить кнопку Push, по нажатию которой в Label-е появляется текст "Hello world":
#pas
// --- начало блока BODY
Unit hiMainForm1;
interface
// --- начало блока UNITS
uses Classes, SysUtils, FileUtil, LResources, Forms, Controls, StdCtrls;
// --- конец блока UNITS
type
TMainForm1 = class(TForm)
protected
procedure DoCreate; override;
procedure addWidget(widget:TControl; x,y,w,h:integer);
public
// --- начало блока PRIVATE VARS
Button2:TButton;
Label3:TLabel;
// --- конец блока PRIVATE VARS
// --- начало блока METHODS INT
procedure onClick2(Sender:TObject);
// --- конец блока METHODS INT
end;
implementation
procedure TMainForm1.addWidget(widget:TControl; x,y,w,h:integer);
begin
widget.parent := self;
widget.left := x;
widget.top := y;
widget.width := w;
widget.height := h;
end;
procedure TMainForm1.DoCreate;
// --- начало блока LOCAL VARS
{ эта секция останется пустой }
// --- конец блока LOCAL VARS
begin
Caption := 'Form1';
// --- начало блока INIT
Button2 := TButton.Create(self);
addWidget(Button2, 35, 20, 55, 30);
Button2.Caption := 'Push';
Button2.onClick := @onClick2;
Label3 := TLabel.Create(self);
addWidget(Label3, 60, 30, 40, 30);
Label3.Caption := 'Label';
// --- конец блока INIT
end;
// --- начало блока METHODS IMP
procedure TMainForm1.onClick2(Sender:TObject);
begin
Label3.Caption := 'hello world';
end;
// --- конец блока METHODS IMP
end.
// --- конец блока BODY
Хорошо видно, что новые элементы на схеме задействовали несколько блоков, куда разместили код, обеспечивающий их корректную работу в соответствии с заявленными функциями.