Vcc. Компилятор виртуальных команд

Последнее обновление: 26 апреля 2001

Достаточно часто в реальной жизни мы пользуемся описанием некоторой последовательности действий. Классическое описание - рецепт из поваренной книги. Приведем пример. Напиток из земляники: 200 г растертой земляники, 150 г молока и одну столовую ложку сахара размешивают, добавляют немного соли и взбивают до образования однородной массы. Естественный язык плохо подходит для формального описания алгоритма - это связано с неоднозначностями, присущими естественному языку. Примеры неоднозначностей - "немного соли", "взбивать до состояния однородной массы". Если бы мы захотели создать автомат по производству напитка из земляники, то нам бы потребовался более точный и строго определенный способ описания алгоритма. Для этого мы могли бы воспользоваться одним из универсальных языков программирования; но у этого подхода есть несколько недостатков. Во-первых, если алгоритм автомата программируется кулинаром или технологом, то трудно требовать от него специфических знаний, присущих программисту. Это означает, что язык описания алгоритма должен быть проблемно-ориентированным и содержать те понятия, которыми пользуется специалист данной предметной области (в данном случае - кулинар). Во-вторых, совсем не обязательно, чтобы алгоритм выполнялся на том устройстве, на котором происходит программирование. Например, кулинар мог бы создавать алгоритмы-рецепты и отсылать их по сети в конкретные устройства-автоматы. Это означает, что устройства могут иметь различную физическую реализацию, но программируются совершенно одинаково.

Примечание 1. С подобной проблемой сталкиваются пользователи Интернета. Компьютеры могут быть различных типов (IBM PC, Macintosh, Sun), использовать различные операционные системы (Windows, Unix), различные броузеры (Microsoft Explorer, Netscape Navigator, Apache), но во всех этих случаях требуется, чтобы пользователи видели одни и те же web-странички. С одной стороны эта проблема решается использованием HTML - гипертекстового языка c разметкой. При использовании HTML в web-страничку кроме собственно текста включаются дополнительные маркеры-теги, которые сами не отображаются, но позволяют задавать шрифт, вставлять картинки, ссылки и так далее. HTML-страница интерпретируется при ее отображении в броузере специальным интерпретатором, который в свою очередь зависит и от операционной системы и от типа машины, но на внешнем уровне это один и тот же HTML-код. Недостаток HTML - это его статичность. Попытки внесения динамики в web-странички привели к созданию ряда языков программирования (например: Perl, Java). Все эти языки - интерпретируемые: это связано с тем, что выполняться они могут на различных машинах под управлением различных операционных систем, но исходный текст (или промежуточный код) для них один и тот же.

Примечание 2. Между языками VCL и Java есть определенное сходство в плане их реализации - и тот и другой компилируются в байт-код, который интерпретируется виртуальным процессором. В обоих случаях структура кода - это однобайтовая команда, за которой следуют аргументы. Но в Java набор байт-кодов жестко стандартизован, в то время как в VCL стандартизованы только 2 команды, а все остальные - гибко определяемые. Другое отличие языков состоит в том, что виртуальный процессор Java перед интерпретацией команд пересылает операнды из памяти программ в стек, в то время как в VCL команды сами должны получать свои операнды из памяти программ. Указанные особенности позволяют использовать VCL даже на микроконтроллерах с оперативной памятью 128 байт и постоянной памятью программ 2 килобайта.

В этой работе решается вопрос создания проблемно-ориентированных языков, которые могут быть использованы для описания алгоритмов различных предметных областей, и в частности, для задач, связанных с управлением различными технологическими процессами. Основное понятие подхода, который используется в работе - виртуальный процессор. Виртуальный процессор в некотором смысле подобен реальному процессору; так же, как и реальный процессор, он имеет собственную систему команд, которую исполняет и программный счетчик (PC), который указывает на текущий шаг программы (текущую команду). Но, в отличие от реального процессора, который ориентирован на архитектуру машины (работа с памятью, регистрами, портами ввода-вывода), виртуальный процессор ориентирован на действия-операции той предметной области, для которой он создается.

В работе рассматриваются следующие темы:

Для описания сценариев действий в терминах виртуальных команд используется язык vcl (Virtual command language) и компилятор vcc (Virtual Command Compiler). Хотя язык и компилятор были разработаны мной для конкретной задачи и для конкретного проекта, они могут использоваться для всех тех случаев, где требуется проблемно-ориентированное описание сценариев. Основные понятия этого подхода: виртуальная команда, компилятор виртуальных команд, виртуальный процессор.

Основные понятия

Виртуальная команда

Описание сценариев можно мыслить как последовательность некоторых действий. Каждое действие имеет символическое имя, код (уникальное число, однозначно характеризующее действие) и набор аргументов-операндов, которые параметризуют действие. Приведем пример сценария, который задает алгоритм производства напитка из земляники:

добавить растертая_земляника, 200;
добавить молоко, 100;
добавить сахар, 10;
добавить соль, 1;
взбивать 30;
вылить;

В этом сценарии 3 действия: добавить, взбивать, вылить. Каждое действие представляет собой виртуальную команду. Команда добавить имеет два аргумента-операнда - ингредиент (задаваемый именем-константой) и его количество (в граммах). Команда взбивать имеет один аргумент - время взбивания в секундах, а команда вылить аргументов не имеет. Символ ";" используется для разделения команд, а "," - для разделения аргументов команды.

Виртуальная команда может быть локальной или сетевой. Имя сетевой команды состоит из двух частей: имени узла и имени команды, соединенных точкой, например, пусть команды добавить выполняются на сетевом узле с именем склад, команда взбивать - на сетевом узле миксер, а команда вылить - на текущем сетевом узле:

склад.добавить растертая_земляника, 200;
склад.добавить молоко, 100;
склад.добавить сахар, 10;
склад.добавить соль, 1;
миксер.взбивать 30;
вылить;

Виртуальная команда без указания сетевого узла выполняется на данном виртуальном процессоре, а команда с указанием сетевого узла передается на исполнение (в виде короткого пакета) другому виртуальному процессору.

Виртуальная программа (сценарий, скрипт)

Сценарий - это последовательность виртуальных команд. На уровне пользователя виртуальная программа - это текст, а на уровне виртуального процессора - это последовательность кодов, соответствующих тексту. Из того, что уже сказано, становится понятным, что виртуальная программа может быть транслирована в последовательность фиксированных кодов, так же как обычная программа транслируется в последовательность машинных команд. Сценарий может быть:

Компилятор виртуальных команд

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

Виртуальный процессор

Так же, как и реальный процессор, виртуальный процессор оперирует программным счетчиком (PC), который указывает на текущую виртуальную команду. Виртуальный процессор выбирает очередную команду (а точнее, получает ее кодовый номер) и вызывает процедуру с указанным номером. Процедура самостоятельно получает свои аргументы (следующие за кодом команды) и выполняет действия, реализующие эту команду. Процедура может быть написана на любом удобном алгоритмическом языке в зависимости от того, в какой среде будет интерпретироваться виртуальная программа. Для данной предметной области нужно определить и написать столько виртуальных команд-процедур, сколько требуется действий для описания любого возможного алгоритма. Каждый виртуальный процессор должен реализовывать, кроме проблемно-ориентированных команд, две системные команды: конец программы и передача виртуальной команды в сеть.

В случае локального сценария для выборки очередной команды используется программный счетчик. Виртуальный процессор является как источником (клиентом), так и исполнителем команд (сервером).

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

Указанный выше рецепт можно было бы эквивалентным образом определить не на языке сценариев, а на обычном алгоритмическом языке, например, на Паскале, в виде вызовов процедур:

добавить(растертая_земляника, 200);
добавить(молоко, 100);
добавить(сахар, 10);
добавить(соль, 1);
взбивать(30);
вылить;

Вроде бы разница совсем небольшая. Зачем тогда нужно было ломать копья и писать свой язык? Аргументов несколько:

Описание языка VCL

В этом разделе приводится неформальное описание языка задания сценариев как последовательности виртуальных команд. Язык VCL использует лексические соглашения языка С.

Лексемы

Идентификатор

Идентификатор - это последовательность букв, цифр и знака подчерка _, которая не должна начинаться с цифры. Регистр букв имеет значение, то есть, идентификаторы включить и Включить - это различные идентификаторы. Длина идентификаторов не ограничена. Идентификаторы используются для символического именования команд, констант, типов, узлов, меток. Компилятор автоматически преобразует русские буквы, имеющие похожее написание, в соответствующие латинские. Например, идентификатор PC (пи си) написанный латинскими буквами означает то же, что и РС (эр эс), написанный русскими буквами.

Ключевое слово

Ключевое слово - это идентификатор, который не может быть использован как обычное имя - он зарезервирован для специального использования. Список ключевых слов:

Число

В языке определены только целые числа. Это связано с тем, что плавающая арифметика реализована на различных машинах по-разному, а кроме того, ее может и совсем не быть (например, в однокристальных микроконтроллерах). Числа могут быть заданы в десятичном формате, восьмиричном (начинается с 0, например 077), двоичном (начинается с 0b, например, 0b101000) и шестнадцатиричном формате (начинается с 0x, например, 0xFF). Внутреннее представление числа на уровне компилятора всегда имеет 32 бита, но, в виртуальной программе оно может быть ограничено до 8 бит (1 байт), 16 бит (2х байтовое целое) или 32 бит (4х байтовое длинное).

Строка

Строка может появляться только в операторе используется для задания имени используемого файла. Строка заключается в двойные кавычки.

Комментарий

Комментарий может быть многострочным (начинается с /* и заканчивается */) или однострочным (начинается от // и продолжается до конца строки).

Синтаксис

Определение виртуальной команды

команда код имя список_операндов ;
код
целое число в диапазоне от 2 до 255. Это код виртуальной команды. Все команды должны иметь уникальные коды. Для одной команды можно задавать несколько синонимов, они должны иметь один и тот же код и список_аргументов. Коды 0 и 1 используются особым образом для системных команд. Код 0 - это код оператора конец - локальная команда завершения или зацикливания программы. Код 1 используется для задания сетевого адреса при распределенном управлении,
имя
уникальное символическое имя команды,
список_операндов
перечисление через запятую аргументов команды с указанием их размера: имя[размер]

Имя носит характер комментария, поясняющего назначение операнда. Размер может быть 1, 2 или 4. Размер 1 соответствует одному байту, 2 - двухбайтовому целому, 4 - четырехбайтовому длинному целому. Отметим, что в выходной код целое и длинное числа записываются в последовательности от младшего разряда к старшему.

Примеры команд для приведенного выше рецепта:

команда 2 добавить ингредиент[1], вес[4];
команда 3 взбивать секунд[2];
команда 4 вылить;

Определение константы

имя = выражение ;
имя
символическое имя константы,
выражение
произвольное арифметическое выражение, которое может содержать числа, имена ранее определенных констант, операции + - * / и скобки ( ).

Примеры декларации констант:

растертая_земляника = 0;
молоко = 1;
сахар = 2;
соль = 3;
стакан = 200;
полстакана = стакан / 2;
столовая_ложка = 10;
на_кончике_ножа = 1;

Использование файлов

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

используется "имя_файла" ;

Этот оператор играет ту же роль, что и #include в языке C - он включает содержимое указанного файла в данное место программы-сценария. Имя файла можно указывать без расширения, в этом случае автоматически используется расширение vcc. Если путь файла явно не указан, то файл ищется сначала в текущем каталоге, а потом в списке путей, заданных в опции -I компилятора. Файл может содержать полный путь, при этом нужно учитывать, что символ "\" используется как специальный, поэтому его нужно вводить дважды, например:

используется "c:\\vvcuses\\cook.vcc";

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

Определение составного типа

Несколько определений констант и команд можно объединить в структуру, называемую типом.

тип имя_типа
  определения_команд
  определения_констант
  оператор_использования
конец

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

имя_типа . имя_константы

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

Определение узла

узел номер имя_узла тип имя_типа ;

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

имя_узла . имя_команды_или_константы

Если команда задается с указанием узла, то она не интерпретируется виртуальным процессором данного узла, а передается на исполнение указанному узлу.

Определение данного узла

этот_узел тип имя_типа ;

Оператор этот_узел определяет набор команд и констант локальными для этого узла. Все команды, указанные в типе будут относиться к данному узлу и интерпретироваться виртуальным процессором. Это эквивалентно тому, чтобы определить команды и константы без всякого типа, но более удобно, так как позволяет структурировать описание системы виртуальных команд по узлам.

Выполнение команды

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

имя_команды список_операндов ;
  или
имя_узла . имя_команды список_операндов ;

Именно их и пишет конечный пользователь, в то время, как определение форматов команд, значений констант, типов и узлов - задача программиста, создающего виртуальный процессор и его систему команд. Первый формат используется для локальных команд, второй - для передачи команды на исполнение указанному узлу. Каждый операнд-аргумент может быть числом, константой, меткой или произвольным арифметическим выражением, включающим операции + - * / и скобки ( ). Количество аргументов должно в точности соответствовать тому количеству, которые было указано в определении данной команды. Операнды разделяются между собой запятыми.

Команда может занимать одну или несколько строк так, как это будет удобно для написания. Завершается команда символом "точка_с_запятой".

Оператор "конец"

Этот оператор можно использовать для явного указания конца программы. Отметим, что это не является обязательным, так как компилятор вставляет этот оператор в конец программы автоматически. Оператор формирует системную виртуальную команду с кодом 0 без операндов. Виртуальный процессор может обрабатывать эту команду, например, путем остановки, перехода к другому сценарию или зацикливания данного сценария (установка PC в 0).

Определение метки

Как правило, сценарий - это простая последовательность действий. Но могут быть случаи, когда необходим более сложный сценарий - с циклами, ветвлениями и подпрограммами. Поскольку язык сценариев - ассемблерного типа, то в нем не определяются какие-либо структурные операторы, а в вместо этого вводится понятие меток - адресов перехода. Метки используются для переходов внутри сценария только в том случае, если это автономный (локальный) сценарий. Внутреннее представление метки - это целое число (2 байта), которое содержит адрес программного счетчика для перехода. Любую команду в программе можно пометить так:

имя_метки :

Для перехода по меткам необходимо определить виртуальные команды, которые имеют адрес перехода как двухбайтовый операнд. Адрес перехода означает абсолютное значение программного счетчика (от 0).

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

// Виртуальные команды
команда 2 добавить ингредиент[1], вес[4];
команда 3 взбивать секунд[2];
команда 4 вылить;
команда 5 если_не_нужен ингредиент[1], куда_перейти[2];
команда 6 ждать_кнопку;

// Константы
растертая_земляника = 0;
молоко              = 1;
сахар               = 2;
соль                = 3;
ванилин             = 4;
стакан              = 200;
полстакана          = стакан / 2;
минута              = 60;
полминуты           = минута / 2;
столовая_ложка      = 10;
на_кончике_ножа     = 1;

// Виртуальная программа - сценарий
начало:
  ждать_кнопку;
  добавить растертая_земляника, 200;
  добавить молоко, полстакана;
  добавить сахар, столовая_ложка;
  добавить соль, 1;

  если_не_нужен ванилин, дальше;
    добавить ванилин, на_кончике_ножа;

дальше:  
  взбивать полминуты;
  вылить;
  конец

В этой системе команд виртуального процесса определена команда если_не_нужен, которая при своем выполнении проверяет значение переключателя и, если переключатель не стоит в позиции ванилин, то значение программного счетчика устанавливается равным второму операнду - это приведет к обходу команды добавления ванилина. Далее, в разделе реализации, мы покажем реализацию системы команд, которая будет включать команды if, goto, call, return - это позволит виртуальному процессору выполнять сценарии теоретически любого алгоритма.

Для повышения удобства программирования сложных сценариев, в языке определяется метасимвол "$", который означает текущее значение программного счетчика и встроенная функция размер. Аргумент функции - имя команды, а возвращаемое значение - полная длина команды со всеми операндами. С учетом этих возможностей, предыдущий фрагмент примера можно переписать без использования метки дальше:

если_не_нужен ванилин, $ + размер(если_не_нужен) + размер(добавить);
  добавить ванилин, на_кончике_ножа;

Значение выражения

$ + размер(если_не_нужен) + размер(добавить)

в точности соответствует адресу метки дальше.

Метасимвол "$" удобно использовать для зацикливания команды, которая активно ожидает некоторого условия, например:

ждать пуск, $;

Реализация этой команды проверяет значение пуск и пока его нет, переустанавливает значение PC, равное значению второго операнда. Формальное определение языка.

Для формального описания языка VCL используется нотация Бэкуса-Наура:

единица_компиляции ::=
    пусто
  | единица_компиляции ;
  | единица_компиляции оператор_использования
  | единица_компиляции декларация_команды
  | единица_компиляции декларация_константы
  | единица_компиляции декларация_типа
  | единица_компиляции декларация_узла
  | единица_компиляции метка
  | единица_компиляции оператор
  | единица_компиляции конец

оператор_использования ::= используется СТРОКА ;

декларация_команды ::= 
  команда ЧИСЛО ИДЕНТИФИКАТОР список_размеров_операндов ;

список_размеров_операндов ::=
    пусто
  | размер_операнда
  | список_размеров_операндов , размер_операнда

размер_операнда ::= ИДЕНТИФИКАТОР [ ЧИСЛО ]

декларация_константы ::= ИДЕНТИФИКАТОР = выражение ;

декларация_типа ::= 
  тип ИДЕНТИФИКАТОР список_деклараций_типа конец

список_деклараций_типа ::=
    пусто
  | элемент_декларации_типа
  | список_деклараций_типа элемент_декларации_типа

элемент_декларации_типа ::=
    декларация_команды
  | декларация_константы
  | оператор_использования

декларация_узла ::= 
    узел ЧИСЛО ИДЕНТИФИКАТОР тип ИДЕНТИФИКАТОР ;
  | этот_узел тип ИДЕНТИФИКАТОР ;

метка ::= ИДЕНТИФИКАТОР :

оператор ::= квалифицированное_имя список_аргументов ;

квалифицированное_имя ::=
    ИДЕНТИФИКАТОР
  | ИДЕНТИФИКАТОР . ИДЕНТИФИКАТОР

список_аргументов ::=
    пусто
  | аргумент
  | список_аргументов , аргумент

аргумент ::= выражение

выражение ::=
    ЦЕЛОЕ_ЧИСЛО
  | квалифицированное_имя
  | '$'
  | размер ( квалифицированное_имя )
  | - выражение
  | выражение * выражение
  | выражение / выражение
  | выражение + выражение
  | выражение - выражение
  | ( выражение )

Компилятор vcc

Компилятор - это программа, которая транслирует исходный текст программы-сценария, написанного на языке VCL в двоичный код для виртуального процессора. Разработано 2 версии компилятора:

Кроме различных имен программы, кодировок и типа операционной системы, оба компилятора совершенно одинаковы.

Вызов компилятора:

vcc [опции] вх_файл [вых_файл]
   или
vcc32 [опции] вх_файл [вых_файл]

Опции (регистр букв не имеет значения):

-FX - выходной формат
В данной версии реализованы 4 выходных формата:
  • -Fb - выходной файл содержит последовательность байт в непосредственном двоичном формате. Файл в этом формате можно динамически загружать для интерпретации прямо с диска
  • -Fa - текстовый ассемблерный - выходной файл имеет такой текстовый вид задания байтовых констант на ассемблере. Такой файл можно непосредственно вкючать в текст на ассемблере. Начало кода соответствует первой команде виртуальной программы (PC = 0). Например:
    VirtProg:
      DB 6,  12, 3,  200, 25
  • -Fс - текстовый десятичный для C и С++ - выходной файл имеет такой текстовый вид задания байтовых констант в десятичном виде. Такой файл можно непосредственно включить в программу на C или C++. Пример:
    6,  12, 3,  200, 25
    Для включения такого файла в программу нужно сделать в программе определение массива:
    const unsigned char VirtProg[] = {
      # include "VirtProg.h"
    };
  • -Fd - выходной файл, это текст модуля (unit) для Delphi. Виртуальная программа определяется как константный массив, который используется в виртуальном процессоре. Если эта -F не указана, то по умолчанию предполагается -Fс.
-LX - выводить листинг в файл X
По умолчанию листинг не выводится. Если задана опция -L без имени файла, то имя файла листинга соответствует имени входного файла, но с расширением lst.
-DX - выводить дамп программы в файл X
По умолчанию дамп не выводится. Если задана опция -D без имени файла, то имя файла дампа соответствует имени входного файла, но с расширением dmp. В дамп выводятся все символические имена, константы и их значения
-SX - выводить статистику использования команд в файл X
По умолчанию статистика не выводится. Если задана опция -S без имени файла, то имя файла статистики соответствует имени входного файла, но с расширением sta. В файл статистики выводятся все локально вызываемые команды с указанием числа обращений к каждой команде. Список статистики упорядочен в соответствии с частотой использования команд.
-IX - путь поиска для используемых файлов
Если используемые файлы находятся в каком-то отдельном библиотечном каталоге (или нескольких каталогах), то путь к используемым файлам можно указать в этой опции, где X - это список каталогов, разделенных символом ";", например: "-ic:\gsv\lib;d:\vp\inc". При указании такой опции компилятор сначала будет искать используемый файл в текущем каталоге, затем в каталоге "c:\gsv\lib" и затем в "d:\vp\inc". Если опция не указана, то по умолчанию используемые файлы ищутся сначала в текущем каталоге, а потом в подкаталоге "inc".
-JX - максимальное число ошибок компиляции
Если в виртуальной программе есть ошибки, то в стандартный файл вывода будут выводиться сообщения об ошибках с указанием имени файла, номера строки и сообщения о причине ошибки. Если эта опция не задана, то компилятор выдаст максимум 20 ошибок и на этом прекратит компиляцию. Очень часто одна ошибка может породить целый каскад связанных ошибок. Можно ограничится заданием -j1, что приведет к выводу сообщения о первой ошибке и завершении компиляции.
-V
при работе компилятор выводит информацию о именах файлов, числе проходов и объеме сгенерированного кода. По умолчанию опция отключена.

Расширения файлов по умолчанию:

Имя выходного файла можно задать явно, если же оно не задано, то используется имя входного файла, но с расширением, зависящим от выходного формата.

Виртуальный процессор на С++

Этот раздел - базовый, в нем рассмотрение способов построения виртуальных процессоров будет наиболее полным, в то время как для других языков будут даны только специфические особенности. Для С++ наиболее удобно строить виртуальные процессоры как наследники базового класса. Таким классом может быть класс TVirtualProcessor. Здесь приведен сокращенный код, полный код можно посмотреть в файлах vp.h, vp.cpp, test.cpp в подкаталоге vcc\cpp

typedef unsigned char Byte;
class TVirtualProcessor
{
  private:
    Byte* Code;
    int   CodeSize;
    int   PC;
    int   SP;
    int   Stack[StackSize];

  protected:
    int   SubCommand;
    int   CurrentPriority;
    int   BasePriority;
    Byte  GetCommand();
    Byte  GetByte();
    int   GetInt();
    long  GetLong();
    void  GoTo(int aPC);
    void  Call(int aPC);
    void  Return();
    void  RemoteCommand();
    virtual void TransferCommand(int aAddress, void* aData, int aSize);

  public:
    TVirtualProcessor(const Byte* aCode, int aCodeSize);
    TVirtualProcessor(const char* aFile);
    ~TVirtualProcessor();
    void Start();
    void Stop();
    void SetPriority(int aPriority);
    bool Ready();
    int  Priority();
    virtual bool Step() = 0;
    virtual bool RemoteStep();
};

Конструирование объекта виртуального процессора

Код виртуального процессора может быть получен из трех источников:

  1. константный байтовый массив кода,
  2. буфер драйвера удаленного доступа,
  3. двоичный файл

В первом случае (первый конструктор) при создании объекта, конструктору передается адрес константного массива кода. Это случай статичной программы. Приведем пример создания объекта типа TTest, порожденного от TVirtualProcessor:

const Byte VirtProg[] = {
# include "virtprog.h"
};
VirtProc = new TTest(VirtProg, sizeof(VirtProg));

В этом примере файл virtprog.h получен в результате компиляции сценария с опцией -fc. Двоичный код сценария в этом будет частью той программы, в которую встраивается виртуальный процессор. Хотя после конструирования виртуального процессора код не меняется, тем не менее после завершения одного сценария можно уничтожить данный виртуальный процессор и создать новый уже с другим сценарием.

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

В третьем случае используется второй конструктор, которому передается имя двоичного файла программы. Двоичный код программы получается в результате компиляции сценария с ключом -fb. Это пример динамической загрузки программы из файла.

Шаг виртуального процессора

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

сm2_Add();       // добавить
сm3_ShakeUp();   // взбивать
сm4_PourOut();   // вылить
сm5_IfNotNeed(); // если_не_нужен
сm6_WaitKey();   // ждать_кнопку

Желательно придерживаться такого правила именования процедур: префикса cm (команда), за которым следует код команды, затем подчерк и за ним - имя команды. Функция Step возвращает значение true, если процесс выполнил шаг и false, если он закончил свою работу:

bool TExample::Step()
{
  if (Ready())
  { switch (GetCommand())
    { case 0: Stop();          break;
      case 1: RemoteCommand(); break;
      case 2: cm2_Add();       break;
      case 3: cm3_ShakeUp();   break;
      case 4: cm4_PourOut();   break;
      case 5: cm5_IfNotNeed(); break;
      case 6: cm6_WaitKey();   break;
    }
    return true;
  }
  return false;
}

То есть, при каждом вызове она выполняет одну команду (один шаг программы), получая код команды с помощью функции GetCommand. Если процедура, исполняющая команду имеет аргументы, то первым делом она должна их получить функциями GetByte, GetInt, GetLong. Получать все аргументы нужно обязательно, того типа и в том порядке, в котором они декларируются в сценарии.

Среди команд могут быть такие, которые выполняют длительные по времени операции и ожидания. Если предполагается параллельное выполнение нескольких сценариев, то недопустимо, чтобы один сценарий монопольно занимал все время. Поэтому в состав виртуального процессора кроме команд входят также подкоманды. С учетом подкоманд код функции Step становится таким:

bool TExample::Step()
{
  if (Ready())
  {
    switch (SubCommand)
    {
      case 0:
        switch (GetCommand())
        {
          // Основные команды
        }
        break;
      case N:
        // Подкоманды
    }
    return true;
  }
  return false;
}

То есть, если код подкоманды (SubCommand) равен 0, то выполняется основная команда, иначе - подкоманда. Приведем пример команды, выполняющей задержку времени:

void TExample::cm7_Pause()
{ PauseTime = CurrentTime() + GetInt();
  SubCommand = 1;
}
void TExample::scm1_Pause()
{ if (CurrentTime() >= PauseTime)
    SubCommand = 0;
}

Команда cm7_Pause устанавливает значение переменной PauseTime равное значению текущего времени плюс время задержки и устанавливает значение подкоманды, равное 1 - это код подкоманды реализации паузы. При каждом вызове scm1_Pause проверяется, не вышло ли время ожидания, если нет, то ничего не происходит, если да, то выходим из подкоманды и переходим к обработке следующей команды. Подкоманда, в отличие от команды не адресуется и не изменяет значение программного счетчика. Коды команд и подкоманд можно задать перечислением, но удобнее включать номер команды в ее имя. Имя подкоманды удобно начинать с префикса scm (SubCommand).

Для помощи в реализации переходов и вызова подпрограмм сценария класс TVirtualProcessor имеет процедуры GoTo, Call, Return. При вызове подпрограммы текущее значение PC сохраняется в стеке, а при возврате - восстанавливается из стека. Процедура перехода просто устанавливает новое значение PC.

Команда RemoteCommand выбирает свои аргументы и вызывает для удаленной передачи команды процедуру TransferCommand, которая реализует конкретный, зависящий от сети, способ транспортировки пакета. Эта процедура должна быть переписана в наследуемом классе, если предполагается возможность распределенного выполнения сценариев.

Если виртуальный процессор предназначен для выполнения удаленных команд, то драйвер удаленного доступа вызывает не Step, а RemoteStep. Реализация функции RemoteStep очень проста - вначале обнуляется PC, а затем вызывается функция Step.

Диспетчеризация (планирование) сценариев

Если выполняется только один сценарий (имеется только один процесс), то планировка может быть очень простой:

while (vp->Step())
  ;

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

do
{ ReadyProcesses = 0;
  for (i = 0; i < ProcessesAmount; ++i)
    if (vp[i]->Step())
      ReadyProcesses++;
  SystemProcess();
}
while (ReadyProcesses);

Здесь в цикле каждый из процессов делает один шаг, затем выполняется процедура некоторого системного процесса (если он, конечно, есть). Весь планировщик завершает свою работу тогда, когда не останется ни одного готового процесса. Системный процесс может выполнять какие-то действия, необходимые для работы всей программы, например, отслеживать принудительное завершение всей программы.

Процедура Stop оставливает выполнение виртуального процессора - его готовность контролируется вызовом функции Ready. Процедура Start позволяет возобновить работу остановленного процессора. Планировщик процессов всегда вызывает функцию Step, которая выполняется только в том случае, если Ready возвращает true.

Для изменения приоритета процесса можно присвоить значению Priority число, большее 0. Чем больше число, тем меньше приоритет процесса. Для реализации приоритетного обслуживания код функции Step дополнительно может включать такую проверку

if (CurrentPriority-- <= 0)
{ CurrentPriority = BasePriority;
  // выполнение команд и подкоманд
}
else return true;

При каждом выполнении шага процесса контролируется значение текущего приоритета и, если он меньше или равен 0, то шаг выполняется и значение текущего приоритеа становится равном приоритету процесса (который задается процедурой SetPriority), в противном случае - шаг пропускается и значение текущего приоритета декрементируется. Таким образом, если приоритет равен 0, то выполняется каждый шаг процесса, если 1, то шаг процесса выполняется через раз и так далее. Очевидно, что 0 - это самый высокий приоритет. Если все процессы имеют равные приоритеты, то включать указанную проверку в функцию Step не нужно.

Пример

В каталоге vcc\cpp расположены следующие файлы:

Программа выполняет один сценарий или массив одинаковых параллельных сценариев и позволяет наблюдать их работу. Для компиляции и запуска сценария, находящегося в test.vcc, нужно запустить пакетный файл start.bat:
   start
или
   start N
где: N - число параллельных процессов 1..10

Ход работы программы будет протоколироваться в файл test.log, который можно проанализировать после завершения программы.

Виртуальный процессор на C

В этом разделы мы рассмотрим, как можно создавать виртуальные процессоры и планировщик применительно к реализации на языке Си для микроконтроллеров с малым объемом оперативной памяти и памяти программ.

Наиболее целесообразно каждый виртуальный процессор писать в отдельном файле, делая все декларации локальными (const и static) и только одну функцию StepX нужно делать extern. Приведем пример самого простого из возможных виртуальных процессов, который реализует только одну команду cm2_X и выполняет свой локальный сценарий циклически.

/* код сценария. Сценарий prog1.vcc должен компилироваться с опцией -fc */ 
const Byte vProg[] = {
# include "prog1.h"
};

static int PC = 0; /* программный счетчик */

/* для повышения быстродействия, функция GetByte реализована как макрос */
# define GetByte vProg[PC++]

static cm2_X()
{ /* получение аргументов */
  . . .
  /* реализация действий команды */
  . . .
}

extern void Step1()
{ switch (GetByte())
  { case 0:  PC = 0;  break;
    case 1:  Error(); break;
    case 2:  cm2_X(); break;
    default: Error(); break;
  }
}

Если процедура cm2_X требует аргументов целого и длинного типа, то они получаются на основе макроса GetByte. Процедура Error предназначена для целей диагностики ошибок. Все современные Си-компиляторы могут генерировать эффективный код для оператора switch на основе перехода по таблицам, которые строит сам компилятор. Для увеличения эффективности кода все альтернативы case должны быть расположены в монотонно возрастающем порядке кодов от 0. Еще более эффективный код даст исключение альтернативы default. Увеличить эффективность для 8-ми битных контроллеров при объеме сценария меньше 255 байт можно за счет объявления программного счетчика как

static Byte PC = 0;

Если в программе несколько параллельных сценариев, то построить для них планировщик можно очень просто:

void main()
{ /* инициализация */
  . . .
  for (;;)
  { Step1();
    Step2();
    . . .
    /* системные действия */
    . . .
  }
}

Каждый из виртуальных процессоров можно изменять независимо друг от друга, например, для поддержки вызова подпрограмм нужно определить стек нужного размера, указатель стека и пару команд - Call и Return. Можно усложнять виртуальный процессор вплоть до того, как он был описан в предыдущем разделе для C++.

История модификаций VCC

Версия 1.1

Добавлены строковые константы, которые определяются аналогично обычным целочисленным константам, но содержат не числа и числовые выражения, а строки. Синтаксис декларации строковой константы следующий:

декларация_строковой_константы ::= ИДЕНТИФИКАТОР = СТРОКА ;

СТРОКА - это последовательность символов в двойных кавычках. В строке можно использовать специальные символы, аналогичные тем, которые используются в языке C:
   \\
   \'
   \"
   \n
   \t
   \r
   \f
   \a
   \b
   \v
   \xNN

Пример:

сброс = ":0AFFD3";

Строки заносятся в конец кода программы, после команды конец. Каждая строка завершается символом '\0'. Строку можно использовать в аргументах команд аналогично метке. Использование строковой константы как аргумента соответствует 2х байтовому значению ее адреса в виртуальном коде (также, как и адреса метки). Например:

соседний_контроллер сброс;

Для строковых констант определена функция:

размер(имя_строковой_константы)

Эта функция возвращает длину строки без учета завершающего нуля.

Кодировка символов зависит от версии vcc: компилятор vcc использует кодировку ASCII (для DOS), а компилятор vcc32 - кодировку ANSI (для Windows).

Download

Downloadvcc.zip - Исходные коды и компилятор (123K).

Версия 1.2 от 26 апр 2001. Архив включает в себя:

  1. две версии компилятора vcc: DOS-приложение и консольное 32х разрядное Windows-приложение,
  2. исходные тексты компилятора vcc. Большая часть компилятора vcc написана на C++, cинтаксический анализатор - на языке YACC, лексический анализатор и кодогенератор используют мою библиотеку gclib. Компиляторы проблемно-ориентированных языков мне приходилось писать довольно часто, поэтому для облегчения работы я разработал собственный инструментарий - библиотека gclib это один из моих инструментов. Как и многие другие инструменты, библиотека разработана на C++ с использованием русскоязычной лексики. Смотрите также Link2klib,
  3. пример реализации виртуального процессора на C++.