Документация Engee

Генерация кода на основе пользовательских шаблонов

Страница в процессе разработки.

Engee позволяет генерировать код с использованием шаблонов (templates). Шаблоны — это текстовые файлы с расширением .cgt (codegen template), в которых используется специальный синтаксис для автоматического добавления данных и создания кода. Генерация кода с помощью шаблонов поддерживается для языков Cи и Chisel.

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

Существует два вида пользовательских шаблонов — для блоков и для функции main:

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

  • Шаблон для функции main фокусируется на общей структуре выполнения модели, включая инициализацию, пошаговую обработку и завершение работы. Этот подход предпочтителен для последовательности выполнения задач в многозадачных моделях.

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

Генерация кода для блоков

Пользовательские шаблоны могут использоваться для генерации кода для блоков моделей Engee. После добавления шаблона в путь нажмите кнопку «Сгенерировать код» codegen icon 1. В папке с результатами вы найдете сгенерированный файл на выбранном языке.

Если шаблон находится в папке (любая директория кроме /user), то его нужно добавить в путь. Для этого выберите папку с шаблоном, щелкните по ней правой кнопкой мыши и добавьте в путь. После добавления папка будет отображаться с синим значком blue directory icon. Это необходимо, чтобы генератор кода мог обойти все указанные пути, найти файлы шаблонов и использовать их для создания кода блоков.
Генератор кода анализирует все шаблоны, добавленные в путь. В случае нахождения нескольких шаблонов генератор кода будет использовать последний добавленный шаблон.
Для работы генерации файл шаблона для блоков должен иметь расширение .cgt.

Синтаксис шаблонов блоков

Синтаксис шаблонов основан на комментариях. Они могут быть однострочными, начиная с //!, или многострочными, заключенными в /*! */. Внутри комментариев можно писать любой корректный код на языке Julia. Комментарии используются, чтобы код шаблона выполнялся, но не попадал в итоговый сгенерированный файл.

Незакомментированные части шаблона напрямую записываются в файл. В этих областях должен быть код на языке Cи или Chisel. Для динамической генерации используется синтаксис $(). Внутри скобок можно указать любой рабочий код на Julia, который будет выполнен, а его результат заменит эту конструкцию в итоговом файле. Если выражение понятно и однозначно, то можно использовать $ без скобок, но вариант со скобками всегда рекомендуется.

Синтаксис $() использует специальные функции только для работы с входами, выходами и состояниями, но не для параметров блока (кроме функции state_addr).

Специальные поддерживаемые для генерации кода функции внутри $() представлены ниже:

Список поддерживаемых функций
Категория Функция Описание

Функции для входов

input

Возвращает входной порт блока

input_datatype_name

Возвращает тип данных входного порта

input_size

Возвращает размерность входного порта

input_ndims

Возвращает количество размерностей входного порта

input_iscomplex

Возвращает комплексность входного порта

input_sampletime

Возвращает шаг расчета входного порта

input_ports_num

Возвращает количество входных портов блока

Функции для выходов

output

Возвращает выходной порт блока

output_datatype_name

Возвращает тип данных выходного порта

output_size

Возвращает размерность выходного порта

output_ndims

Возвращает количество размерностей выходного порта

output_iscomplex

Возвращает комплексность выходного порта

output_sampletime

Возвращает шаг расчета выходного порта

output_ports_num

Возвращает количество выходных портов блока

Функции для состояний

state

Возвращает состояние блока

state_datatype_name

Возвращает тип данных состояния

state_size

Возвращает размерность состояния

state_ndims

Возвращает количество размерностей состояния

state_iscomplex

Возвращает комплексность состояния

state_num

Возвращает количество состояний блока

Функции для работы с путями до блока

get_blockname

Возвращает имя блока

Функции для работы с планировщиком

block_sampletime

Возвращает шаг расчета блока (можно использовать output_sampletime(1), если у блока есть выход)

get_rates_num

Возвращает количество частот дискретизации в модели

get_baserate

Возвращает базовую частоту дискретизации модели

get_model_rates

Возвращает массив соотношений базовой и других частот дискретизации, например, [1 3 5]

is_singlerate_model

Возвращает true, если модель одночастотная

is_singletask_model

Возвращает true, если модель однозадачная

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

Список макросов метакода
Категория Макрос Описание

Буферы шаблона

@Definitions

Используется для определения глобальных переменных, типов данных и макросов. Содержимое буфера эмитируется в начале файла после всех #include.

@Epilogue

Добавляет код в конец сгенерированного файла. Обычно используется для завершения работы или добавления комментариев.

@Init

Содержит код, который выполняется при инициализации модели. Буфер эмитируется в функции model_init.

@Inports

Используется для генерации описания входных портов блока. Буфер включается в соответствующую секцию кода модели.

@Outports

Содержит код для описания выходных портов блока. Генерируется в секции, связанной с обработкой выходов.

@Prologue

Добавляет код в начало файла, до секции @Definitions. Может содержать предварительные определения или настройки.

@States

Используется для описания состояний блока, включая их размеры, типы данных и инициализацию. Генерируется в секции состояния модели.

@Step

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

@Term

Содержит код, выполняемый при завершении работы модели. Буфер эмитируется в функции model_term.

@Toplevel

Добавляет код, который должен быть размещен в самом начале модели, сразу после #include.

Для работы с параметрами блока в шаблонах используется глобальная структура данных param. Это именованный кортеж Julia, который содержит описание всех параметров блока, включая сигнало-подобные параметры (например, значение параметра Gain в блоке Gain) и все остальные характеристики блока.

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

Пример:

/*!
 * BlockType = :SomeBlockINeedToExplore
 * TargetLang = :C
 */

/* Let's see the block description:
$param
*/

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

Пример шаблона

Рассмотрим пример шаблона для следующей модели:

codegen model 1

//! BlockType = :Product
//! TargetLang = :C

//! @Step
//! if contains(param.Inputs,"/")
    /* Выполняется деление, если в параметрах блока указан символ "/" */
    /* Чтобы избежать ошибки деления на ноль, добавлена проверка входного значения */
    $(output_datatype_name(1)) $(output(1)) = $(input(2)) == 0 ? $(input(1)) : $(input(1)) / $(input(2));
//! else
    /* Выполняется умножение, если деление не задано в параметрах */
    $(output_datatype_name(1)) $(output(1)) = $(input(1)) * $(input(2));
//! end

В приведенном примере показан обновленный шаблон для генерации кода блока Product на языке Си. В этом коде:

  • BlockType = :Product: Указывает тип блока, для которого генерируется код;

  • TargetLang = :C: Указывает целевой язык для генерации, в данном случае — Си.

Код шаблона использует секцию @Step, которая определяет вычисления, выполняемые на каждом шаге расчета:

#! @Step
if contains(param.Inputs,"/")
    /* Кастомное деление с проверкой деления на ноль */
    $(output_datatype_name(1)) $(output(1)) = $(input(2)) == 0 ? $(input(1)) : $(input(1)) / $(input(2));
else
    /* Ветка для случая, если это блок умножения */
    $(output_datatype_name(1)) $(output(1)) = $(input(1)) * $(input(2));
end

В этом коде:

  • Условие if contains(param.Inputs,"/") проверяет, содержит ли параметр Inputs строку "/". Если это так, то выполняется операция деления:

    • $(input(2)) == 0 — проверка деления на ноль. Если знаменатель равен нулю, то результат берется равным $(input(1)).

    • Иначе выполняется деление $(input(1)) / $(input(2)).

  • В случае если деление не используется, выполняется умножение $(input(1)) * $(input(2)).

Принцип работы:

  • Код секции @Step исполняется на каждом шаге расчета, определяя поведение блока в зависимости от конфигурации параметров.

  • Использование $(input(n)) и $(output(n)) позволяет динамически обращаться к входным и выходным портам блока.

  • Переменные типа данных выходного порта определяются через $(output_datatype_name(1)), что делает код универсальным для разных типов данных.

Для блоков C Function и Engee Function синтаксис шаблона отличается. Вместо стандартного указания типа блока через BlockType, используется формат //!BlockType = :EngeeFunction!EngeeFunction, где после ! указывается имя блока из модели, преобразованное для целевого языка. Если указанный BlockType некорректен, то система выдаст ошибку генерации.

В результате использования шаблона для модели newmodel_1 с блоком Product будет сгенерирован файл newmodel_1.c со следующим содержимым:

#include "newmodel_1.h"

/* External inputs */
Ext_newmodel_1_U newmodel_1_U;

/* External outputs */
Ext_newmodel_1_Y newmodel_1_Y;

/* Model initialize function */
void newmodel_1_init() {
	/* (no initialize code required) */
}

/* Model terminate function */
void newmodel_1_term() {
	/* (no terminate code required) */
}

/* Model step function */
void newmodel_1_step() {

	/* Product: /Product incorporates:
	 *  Inport: /In2
	 *  Inport: /In1
	 */
	const double Product = newmodel_1_U.In2 * newmodel_1_U.In1;
	/* Product: /Divide incorporates:
	 *  Inport: /In1
	 *  Inport: /In1
	 */
	const double Divide = newmodel_1_U.In1 / newmodel_1_U.In1;
	/* Outport: /Out1 incorporates:
	 *  Product: /Divide
	 */
	newmodel_1_Y.Out1 = Divide;
	/* Outport: /Out2 incorporates:
	 *  Product: /Product
	 */
	newmodel_1_Y.Out2 = Product;

}

В сгенерированном коде реализуются основные функции для работы модели: инициализация (newmodel_1_init), выполнение шага расчета (newmodel_1_step) и завершение работы (newmodel_1_term).

Функция newmodel_1_step выполняет ключевые операции модели. В этом примере реализованы два вычисления: умножение (Product) и деление (Divide). Результаты этих операций записываются в соответствующие выходные параметры модели Out1 и Out2. Для расчета значений используются данные, поступающие через входы In1 и In2, определенные в структуре Ext_newmodel_1_U.

В функциях инициализации (newmodel_1_init) и завершения работы (newmodel_1_term) указано /* (no initialize code required) / и / (no terminate code required) */. Это означает, что для данной модели на этапе генерации кода не потребовалось добавлять дополнительный пользовательский код для выполнения операций инициализации или завершения работы. Эти функции остаются пустыми, но все равно включаются в код для поддержки синтаксиса шаблона.

В сгенерированном коде функции init и term создаются всегда, даже если в шаблоне их секции не добавлены явно (@Init или @Term). Это сделано для того, чтобы структура сгенерированного кода оставалась единой и готовой к добавлению новой логики в будущем.

Например, в приведенном шаблоне для блока Product есть только секция @Step. В результате:

Функции init и term в сгенерированном коде также будут присутствовать, но останутся пустыми, с комментариями /* (no initialize code required) / и / (no terminate code required) */. При этом, содержимое секции @Step будет вставлено в функцию step, так как это единственная секция, заданная в шаблоне.

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

Генерация функции main

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

Для создания функции main используется шаблон на языке Julia, в котором структура генерируемого кода формируется с помощью комментариев в Си-стиле (/* …​ */, как указано в разделе синтаксиса выше), и функция engee.generate_code в командной строке или редакторе скриптов. Сигнатура функции generate_code выглядит следующим образом:

@with_exception_handling function generate_code(
    model_path::String,
    output_path::String;
    subsystem_id::Maybe{String} = nothing,
    subsystem_name::Maybe{String} = nothing,
    target::Maybe{String} = nothing,
    template_path::Maybe{String} = nothing
)

Для генерации кода функции main в функции engee.generate_code требуется указать путь к шаблону с помощью именованного параметра template_path.

Пример вызова функции:

engee.generate_code(
    "/user/codegen_model.engee", # путь к модели из которой генерируется код
    "/user/codegen_dir"; # путь куда будет сгенерирован код
    template_path = "/user/main_template.jl" # путь к шаблону функции main
)

Пример шаблона

Рассмотрим пример шаблона для генерации функции main:

Шаблон для генерации
function main_template() :: String
	#=%
		#include "$model_name.h"

		void do_step(void){
	%=#
	if !is_singlerate_model && !is_singletask_model
		zeroes = "0"
		for ji = 2:rates_num zeroes *= ", 0" end
		#=%
				static bool OverrunFlags[$rates_num] = {$zeroes};
				static bool eventFlags[$rates_num] = {$zeroes};
				static int taskCounter[$rates_num] = {$zeroes};

				if (OverrunFlags[0]) return;
		%=#
		for ji = 2:rates_num
			i = ji - 1
			rate_ratio = Int(rates[ji] / get_baserate())
			#=%
					if (taskCounter[$i] == 0) {
						if (eventFlags[$i]) {
							OverrunFlags[0] = false;
							OverrunFlags[$i] = true;
							/* Sampling too fast */
							return;
						}
						eventFlags[$i] = true;
					}
					taskCounter[$i]++;
					if (taskCounter[$i] == $rate_ratio) {
						taskCounter[$i] = 0;
					}
					/* Step the model for base rate */
					$(model_substep(0))
					/* Indicate task for base rate complete */
					OverrunFlags[0] = false;
			%=#
		end
		for ji = 2:rates_num
			i = ji - 1
			#=%
					/* If task {0} is running, do not run any lower priority task */
					if (OverrunFlags[$i]) return;
					/* Step the model for subrate */
					if (eventFlags[$i]) {
						OverrunFlags[$i] = true;
						/* Step the model for subrate $i */
						$(model_substep(i))
						/* Indicate task complete for subrate */
						OverrunFlags[$i] = false;
						eventFlags[$i] = false;
					}
			%=#
		end
	else
		#=%
				static bool OverrunFlag = false;

				/* Check for overrun */
				if (OverrunFlag) {
					return;
				}

				OverrunFlag = true;

				/* Step the model */
				$model_step

				/* Indicate task complete */
				OverrunFlag = false;
		%=#
	end

	print_sig(s) = "$(s.ty) $(s.full_qual_name)$(dims(s))"
	#=%
		}

		int main(int argc, char *argv[])
		{
			(void) argc;
			(void) argv;

			/* Initialize model */
			$model_init

			while (1) {
				/* Perform application tasks here */
				// Signals available:
				// INS
				// $(print_sig.(ins))
				// OUTS
				// $(print_sig.(outs))
			}

			/* Terminate model */
			$model_term
			return 0;
		}

	%=#
end

здесь:

  • В начале шаблона происходит подключение заголовочного файла модели через #include "$model_name.h" для доступа к функциям модели.

  • Выполняется основной шаг моделирования с помощью вызова $(model_substep(0)) и сброса флагов состояния. Для задач субчастот контролируется выполнение, обрабатываются шаги с помощью $(model_substep(i)) и обновляются флаги завершения.

  • Для однозадачных моделей реализован контроль перегрузки через флаг OverrunFlag и выполнение модели вызывается через $model_step.

  • В функции main вызывается $model_init для подготовки модели к выполнению.

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

  • Перед завершением работы вызывается $model_term для корректного завершения модели и освобождения ресурсов.