Модули разрабатываются на языке C++ в среде Microsoft Visual Studio 2013 (или более новой редакции). Заготовка модуля генерируется специальным генератором и располагается в папке modules. Для выполнения задания предоставляется готовый скелет модуля. Содержимое типовой заготовки показано ниже:
Любой модуль имеет описание на языке M. Оно в обязательном порядке содержит: версию (в значении ключа Version), список поддерживаемых поколений (массив в значении ключа Generations), имя динамической библиотеки (в значении ключа Module) и зависимости (имена и поколения других модулей, которые потребуются данному модулю для работы). Каждый модуль предоставляет точное описание необходимых ему протоколов в своих мета-данных (при этом используется стандартный механизм указания зависимостей).
; Описание системной библиотеки-расширения микро-ядра <root>: Uniteller\Images Uniteller.Framework.Kernel.System Version : 1.0.0 ; Версия Generations : {1} ; Поддерживаемые модулем поколения Protocols ; Поддерживаемые модулем протоколы Kernel.System : 1 ; Поддерживаемый протокол, поколение Module : Uniteller.Framework.Kernel.System.dll ; Имя модуля Dependencies ; Зависимости Uniteller.Framework.Kernel : 1 Uniteller.InterchangeFormats.Kernel.System : 1
Конечный автомат не программируется в модуле вручную. В модуль встраивается модель конечного автомата, заданная на языке M. Изначально эта модель разрабатывается в графическом редакторе. Для встраивания модели в модуль выполняется ее трансформация в декларативное описание. Такое описание предполагает перечисление всех состояний, событий и переходов в автомате. Ядро реализует интерпретатор этого описания. В момент прихода сообщения срабатывает переход. В этот момент вызывается функция-обработчик перехода в главном классе модуля. Имя этой функции определяется автоматически по имени состояний («из» и «в» а также типу события).
Декларативное описание не разрабатывается вручную. Для получения декларативного описателя по модели конечного автомата в формате Visio необходимо использовать специальную утилиту — VisioExporter. Для запуска этой утилиты следует использовать следующую командную строку:
VisioExporter.exe -model <путь к файлу-модели (в формате vsd)> -out <выходная папка>
Подробнее:
Полученный файл <имя модуля>.utx следует скопировать в папку модуля.
Протокол подключается к модулю на уровне заголовочного файла <имя протокола>.h. Если используется несколько протоколов, то нужно получить все их заголовочные файлы. Прежде чем переходить к следующему этапу следует убедиться в том, что все необходимые файлы протоколов находятся в папке [references].
Главный экспортируемый класс модуля обычно является основным разрабатываемым элементом модуля. Заготовка этого класса содержится в двух файлах: machine.h и machine.cpp. Заготовки этих файлов генерируются автоматически. В автоматически созданной заготовке объявляется структура класса, незаполненная карта обработчиков, конструктор, связывающий класс и его декларативным образом описанный автомат. Заготовку следует доработать вручную.
На первом этапе доработки следует открыть модель конечного автомата модуля и определить все переходы, которые должны обрабатываться. Для каждого такого перехода необходимо внести запись в карту обработчиков. Карта обработчиков переходов ограничена строками:
BEGIN_HANDLERS_MAP() // Для заполнения карты следует использовать макрос REGISTER_HANDLER (зарегистрировать обработчик) // Сигнатура REGISTER_HANDLER(StateFrom, StateTo, EventCode, Handler) // StateFrom - Состояние из которого идет ребро в конечном автомате // StateTo - Состояние в которое идет ребро в конечном автомате // EventCode - код сообщения, которым от отмечено ребро // Handler - указатель на метод, который необходимо вызвать при срабатывании обработчика ... - содержимое карты обработчиков переходов END_HANDLERS_MAP()
При активации перехода по этой карте определяется и вызывается нужный обработчик. Для каждого обработчика требуется указать запись вида:
REGISTER_HANDLER(From, To, Event, Handler).
Например:
REGISTER_HANDLER("Idle", "Idle", Protocol::G1::Codes::ReplaceAll, &Machine::ReplaceAllHandler);
Такая строка зарегистрирует обработчик ReplaceAllHandler, срабатывающий в момент перехода из состояния Idle в состояние Idle при получении сообщения ReplaceAll. Обработчик ReplaceAllHandler в общем случае является функцией, принимающей сообщение ReplaceAll (сообщение ReplaceAll содержит команду для выполнения замены всех вхождений некоторой подстроки-образца на определенную строку-значение в некоторой строке).
Подобным образом следует зарегистрировать в карте все необходимые обработчики. По-хорошему, для каждого перехода должен быть свой обработчик. Однако это не является обязательным условием.
Естественно, что все обработчики, использованные в карте обработчиков, должны быть должным образом объявлены в классе Machine. Как уже было сказано выше, обработчики являются функциями, принимающими одно сообщение (сообщение, активировавшее переход, вызвавший функцию-обработчик). Объявляются они в области видимости protected с помощью конструкции DECLARE_HANDLER. Напимер:
DECLARE_HANDLER(ReplaceAllHandler, Protocol::G1::ReplaceAll);
Подобная запись разворачивается в объявление обработчика:
void ReplaceAllHandler(const Protocol::G1::ReplaceAll & message);
После объявления следует разместить в файле реализации (cpp) заготовки тел обработчиков. Например:
void Machine::ReplaceAllHandler(const Protocol::G1::ReplaceAll & message) { }
Для иллюстрации реализуем данный обработчик:
void Machine::ReplaceAllHandler(const Protocol::G1::ReplaceAll & message) { // message - сообщение-активатор Protocol::G1::ReplaceAllAnswer answer; // answer - сообщение-ответ answer.Result = Utils::ReplaceAll(message.GetSource(), message.GetPattern(), message.GetValue()); // Отправляем адресное сообщение ответ SendMessage(message.GetSourceId(), answer); }
После получения (в момент срабатывания перехода) команда исполняется (вызовом функции из утилитарного слоя Utils::ReplaceAll — эта функция реализована в соответствующем файле, включенном в проект, и подключена с помощью #include нужного заголовочного файла). Результат исполнения заносится в сообщение-ответ. Сообщение-ответ отправляется тому, кто прислал сообщение-команду (определено с помощью message.GetSourceId()).
Обработчики могут реализовывать и более сложную логику и не обязаны генерировать только одно ответное сообщение. При этом может быть и обратная ситуация, когда модуль вообще никак не показывает своей реакции, не отправляя никаких ответных сообщений вовсе (просто что-то выполняя с полученными параметрами и меняя свое состояние).
Следует отметить, что в некоторых случаях модуль может генерировать сообщения для самого себя. Для этого он использует при вызове SendMessage свой собственный адрес (который можно получить вызвав функцию GetUnicalId()). Важно понимать, что отправка сообщения для модуля не является событием!
После того как все обработчики занесены в карту и их тела реализованы, создан и подкреплен модульными тестами утилитарный слой в модуле, и код компилируется без ошибок, можно перейти к тестированию модуля.