Engee documentation
Notebook

Code generation for Engee Function using Code Generator Templates

Motivation

Engee allows you to use custom code in models using the Engee block Function. However, in this case, code generation will not be possible, as the code generator will not be able to "understand" the user's code. Writing a template for a source code generator solves this problem. As an example, let's consider writing a template for the Engee Function block from the ef_adder model.

The AddDemo block implements the addition of two inputs. The block inputs are fixed-size vectors, 16 in length. Additionally, we will complete the task by putting forward a condition for the use of vector calculations.

We will solve the problem by writing a template for the source code generator.

The anatomy of a source code generator template

The source code generator templates are divided into two classes:

  • Templates for blocks
  • Templates for the main() function

In this example, we are only dealing with templates for blocks.

The template for the blocks has the *.cgt extension and starts with a comment like

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

BlockType is the type of block that can be obtained from its parameters.
TargetLang is the target language

The template code itself follows. The template contains code in the target language, marked up with special comments and macros. These comments and macros contain code in the Julia language (the so-called host language).

Template for vectorized addition

To view the contents of a text file, create a macro:

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

Now we can review the template code.:

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))));

It can be seen that the template is divided into sections using constructions like //! @

These are macros that provide a mechanism for specifying where to insert the source code from the template into the corresponding sections of the generated code.

So, using a macro //! @Toplevel we tell the code generator that all the code below, until the next macro, will be placed at the beginning of the model header file.

Let's pay attention to the following lines of the template. They are interesting because they demonstrate the calls of the host language for the template.

For example, if the line starts with //! then this string will be recognized as a code in the host language .:

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

Using such a comment, you can calculate a constant for further use in the code.:

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

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

At the same time, if it is necessary to write multi-line code in the host language, then we can make a multi-line comment. For example:

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

The template code shows function calls like `output_datatype_name(1)` - these are calls to special template functions that allow you to work with the properties of inputs, outputs, and block states.

Code generation using a template

In order to apply our template, we need to add its folder to the Engee search path.:

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

Then, we will generate the code using the engee command.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

Let's look at the generated 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];

}

It can be seen that the code from the sections @Step and @Defenitions was built into the function step() models. The same is true for the model header file.:

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 */

Let's check that the received code can be assembled without errors.:

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

Now let's make sure that the block in Engee and the generated code work the same way. To do this, we will create the adder_ccall test model and put the C Function block in it, which calls the generated code. Then we'll build a test model.:

image.png

For testing, don't forget to set up the C Function block and save the model.:

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)

After running the model, we will get the values of the outputs:

image.png

It can be seen that they match, which means that the code and the model work the same way.

At the end of the work, do not forget to return the search path to its original state.:

In [ ]:
engee.rmpath(demoroot)

Conclusions and next steps

Source Code Generator templates are a powerful source code customization tool that allows you to generate any code, including the use of compiler extensions. However, since the user is given full control over the logic of code generation, the user is responsible for ensuring that the template is correct.

Blocks used in example