Генерация Си кода из Engee Function

Автор
avatar-nikfilaretovnikfilaretov
Notebook

Генерация кода для Engee Function с помощью шаблонов генератора кода

Мотивация

Engee позволяет использовать пользовательский код в моделях при помощи блока Engee Function. Однако, в этом случае генерация кода будет невозможна, так как генератор кода не сможет "понять" пользовательский код. Эту проблему решает написание шаблона для генератора исходного кода. В качестве примера рассмотрим написание шаблона для блока Engee Function из модели ef_adder.

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

Задачу будем решать при помощи написания шаблона для генератора исходного кода.

Анатомия шаблона генератора исходного кода

Шаблоны генератора исходного кода делятся на два класса:

  • Шаблоны для блоков
  • Шаблоны функции main()

В этом примере мы касаемся только шаблонов для блоков.

Шаблон для блоков имеет расширение *.cgt и начинается с комментария вида

/*!
 BlockType = :EngeeFunction!AddDemo
 TargetLang = :C
 */

BlockType - это тип блока, который можно получить из его параметров TargetLang - это целевой язык

Далее следует сам код шаблона. Шаблон содержит код на целевом языке, размеченный специальными комментариями и макросами. Внутри этих комментариев и макросов содержится код на языке Julia (т.н. хост-язык).

Шаблон для векторизованного сложения

Чтобы посмотреть содержимое текстового файла, создадим макрос:

In [ ]:
macro showfile(file::String)
    f = open(file)
    s = read(f, String);
    println(s);
    close(f)
end
Out[0]:
@showfile (macro with 1 method)

Теперь можно рассмотреть код шаблона:

In [ ]:
@showfile "add_vectors.cgt"
/*!
 BlockType = :EngeeFunction!AddDemo
 TargetLang = :C
 */

//! @Toplevel
//! sz = prod(input_size(1))
//! vector_width = convert(Int32, sz * (input(1).ty.bits)/8)
//! vType = "$(input_datatype_name(1))_v4_t"

typedef $(input_datatype_name(1)) $vType __attribute__ ((vector_size ($(vector_width))));

//! @Definitions
int vlen = $(sz);
$(vType) add1,add2,sum;
static $(output_datatype_name(1)) $(output(1))[$(prod(input_size(1)))];

//! @Step
memcpy(&add1,&$(input(1)), vlen * sizeof($(input_datatype_name(1))));
memcpy(&add2,&$(input(1)), vlen * sizeof($(input_datatype_name(1))));

sum = add1 + add2;

memcpy(&$(output(1)), &sum, vlen * sizeof($(input_datatype_name(1))));

Видно, что шаблон разделен на секции при помощи конструкций вида //! @

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

Так, при помощи макроса //! @Toplevel мы указываем генератору кода, что весь код ниже до следующего макроса будет помещен в начало заголовочного файла модели.

Обратим внимание на следующие строки шаблона. Они интересны тем, что демонстрируют вызовы хост-языка для шаблона.

Например, если строка начинается с //!, то эта строка будет распознана как код на хост языке:

//! vector_width = convert(Int32, prod(input_size(1)) * (input(1).ty.bits)/8)

С помощью такого комментария можно вычислить какую-либо константу для дальнейшего использования в коде:

typedef $(input_datatype_name(1)) $vType __attribute__ ((vector_size ($(vector_width))));

Чтобы встроить результаты работы некоторой функции или значение переменных в сгенерированном коде мы оборачиваем вызов функции или переменной в конструкцию вида $()

При этом, если необходимо написать многострочный код на хост-языке, то мы можем сделать многострочный комментарий. Например:

/*! 
if isempty(input_size(1))
	sz = 1;
else
	sz = input_size(1)
end
*/

В коде шаблона видны вызовы функций вида `output_datatype_name(1)` - это вызовы специальных функций шаблонов, которые позволяют работать со свойствами входов, выходов и состояний блоков.

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

Для того чтобы применить наш шаблон, мы должны добавить его папку в путь поиска Engee:

In [ ]:
demoroot = @__DIR__
engee.addpath(demoroot)

Затем, сгенерируем код с помощью команды engee.generate_code:

In [ ]:
modelName = "ef_adder"
engee.generate_code(joinpath(demoroot,"$(modelName).engee"),
                    joinpath(demoroot,"vector_code"),
                    target = "c"
                    )
[ Info: Generated code and artifacts: /user/work/code_generation/block_template/vector_code

Посмотрим на сгенерированный код:

In [ ]:
@showfile "vector_code/ef_adder.c"
/* Code generated by Engee
 * Model name: ef_adder.engee
 * Code generator: release-1.1.17
 * Date: Mon Jun 16 06:27:19 2025
 */

#include "ef_adder.h"

/* External inputs */
Ext_ef_adder_U ef_adder_U;

/* External outputs */
Ext_ef_adder_Y ef_adder_Y;

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

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

/* Model step function */
void ef_adder_step() {
	int vlen = 16;
double_v4_t add1,add2,sum;
static double AddDemo[16];


	
memcpy(&add1,&ef_adder_U.Inport, vlen * sizeof(double));
memcpy(&add2,&ef_adder_U.Inport, vlen * sizeof(double));

sum = add1 + add2;

memcpy(&AddDemo, &sum, vlen * sizeof(double));
	/* Outport: /Out1 incorporates:
	 *  EngeeFunction: /AddDemo
	 */
	for (int i = 0; i < 16; i++) ef_adder_Y.Out1[i] = AddDemo[i];

}

Видно, что код из секций @Step и @Defenitions был встроен в функцию step() модели. Аналогично и для заголовочного файла модели:

In [ ]:
@showfile "vector_code/ef_adder.h"
/* Code generated by Engee
 * Model name: ef_adder.engee
 * Code generator: release-1.1.17
 * Date: Mon Jun 16 06:27:19 2025
 */

#ifndef HEADER_ef_adder_h
#define HEADER_ef_adder_h

#include <stddef.h>
#include <stdint.h>
#include <stdlib.h>
#include <stdbool.h>
#include <math.h>
#include <float.h>
#include <limits.h>
#include <string.h>

	typedef double double_v4_t __attribute__ ((vector_size (128)));



#ifdef __cplusplus
extern "C"
{
#endif /* __cplusplus */

typedef struct {
	double Inport[16];                     /* /Вход1 */
} Ext_ef_adder_U;

/* External inputs */
extern Ext_ef_adder_U ef_adder_U;

typedef struct {
	double Out1[16];                       /* /Out1 */
} Ext_ef_adder_Y;

/* External outputs */
extern Ext_ef_adder_Y ef_adder_Y;

/* Model entry point functions */
extern void ef_adder_init();
extern void ef_adder_step();
extern void ef_adder_term();

/* Here is the system hierarchy for this model:
 *
 * 'ef_adder' : '/'
 */

#ifdef __cplusplus
} /* extern "C" */
#endif /* __cplusplus */

#endif /* HEADER_ef_adder_h */

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

In [ ]:
;gcc -shared -fPIC ./vector_code/ef_adder.c -I ./vector_code -march=native -O3

Теперь удостовремся, что блок в Engee и сгенерированный код работают одинаково. Для этого мы создадим тестовую модель adder_ccall и поместим в нее блок C Function, который вызывает сгенерированный код. Затем соберем тестовую модель:

image.png

Для тестирования не забудем настроить блок C Function и сохраним модель:

In [ ]:
mdl = engee.load("adder_ccall.engee")
engee.set_param!("adder_ccall/C Function","SourceFiles"=>"$(demoroot)/vector_code/ef_adder.c","IncludeDirectories"=>"$(demoroot)/vector_code");
engee.save(mdl,"adder_ccall.engee";force=true)
engee.close(mdl;force=true,sync_gui=true)

После запуска модели получим значения выходов:

image.png

Видно, что они совпадают, а значит код и модель работают одинаково.

По окончанию работы, не забудем вернуть путь поиска к исходному состоянию:

In [ ]:
engee.rmpath(demoroot)

Выводы и следующие шаги

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