Engee documentation

C Function

Usage of C code in models.

blockType: CFunction

Path in the library:

/Basic/User-Defined Functions/C Function

Description

The C Function block allows Engee models to use C code and shared libraries that have a C API.

When connecting a dynamic library that uses internal static variables, you should make sure that it provides an API for initialising/deinitialising these variables. The corresponding functions should be called in the StartCode and TerminateCode tabs of the source code editor. Otherwise, static variables will retain their last values between simulations, which may lead to incorrect results when the model is rerun.

Usage

To integrate source C code into an Engee model, you must:

  • Add to the model the block C Function from the Basic/User Functions section of the block library;

  • Click on the Edit Source Code button and the source code editor will be opened.

c function settings

Source code editor

The source code editor workspace consists of four tabs:

  • OutputCode - contains code executed at each step of the model calculation;

  • StartCode - contains code executed once when the model is initialised;

  • TerminateCode - contains code executed once when the model is stopped;

  • SharedCode - contains code that allows exchanging global (shared) variables and functions between instances of the block C Function.

C syntax highlighting is performed in each of the sections.

The OutputCode section contains a block of special comments that are important for the simulator and the code generator.

Comments allow you to associate variables in the source code with signals (inputs and outputs) of the block C Function, as well as to specify additional information necessary for the assembly of the code and its usage in the model.

c function code editor

Features of C code generation from the block C Function

The C Function block supports C code generation. When generating code, the C Function block is turned into functions that wrap the contents of all four sections of the source code.

At this time, the C code generator does not provide additional information to the build system based on the directives of the C Function block . The directives are specified in the Build options section of the block settings:

build options c function

Block usage reuse

The SharedCode tab of the source code of the C Function block allows you to specify the common code available for sharing data and functions between instances of the C Function blocks with the same name in the parameters Function name.

To use the SharedCode functionality, you need to create a block C Function, name it in the parameters Function name, and then save that block to user library user library 1. The block can then be added to the model in several places where it will retain access to common data and functions. As a result, modelling creates only one common shared library and one set of functions that is used by all instances of the block C Function.

When generating code for blocks with the same name in the Function name parameters, only the shared functions init, step and term are created, and the code from the SharedCode tab is added to the project once. This simplifies code management and provides access to common data in all instances of the block C Function.

Working variables

The WorkVariables tab of the C Function block allows you to set static working variables that remain specific to each block instance, even if multiple blocks have the same value for the parameters Function name. These working variables are similar to parameters, but differ in their ability to change their values from the block’s source code at runtime. This approach allows you to store and modify data that is only used within a particular block instance.

For each working variable, the WorkVariables tab specifies Name and Size in bytes. Within the source code, these variables are accessible via a pointer to the Work structure, which is present in the interfaces of all three block functions: step, init, and term. Example:

work variable 1

Alignment is used here in the source code, where the is_aligned macro checks that the variables are aligned correctly. The variables work_variable1 and work_variable3 are predefined on the tab WorkVariables and are available through the structure Work.

Working with arrays

When data is passed from Julia to C code, it is important to consider the differences in indexing and array structure. This is relevant for multidimensional arrays, which can be transposed (change the order of the indexes) during transfer. Below are the main points to consider when working with arrays:

For more information about working with arrays.
  • One-dimensional arrays - when passing a one-dimensional array from Julia to C, the array retains its structure. Indexing in C starts from 0, so when accessing array elements you need to take this shift into account. For example, if a one-dimensional array is written as a = [1, 2, 3] in the block parameter C Function, then in C:

    #include <stdio.h>
    
    int main() {
        int a[3] = {1, 2, 3};
        printf("%d", a[2]); // Выведет 3
        return 0;
    }

    In this example, a[2] is invoked to get the value of the array element with index 2, which will return the value 3.

  • Multidimensional Arrays - Multidimensional arrays can change index order when passed from Julia to C. In C code, the array will be transposed, which means that when accessing elements of the array, the change in index order must be accounted for. For example, there is an array in Julia:

    a = [1 2 3; 4 5 6]
    
    #Julia интерпретирует это как матрицу 2x3
    2x3 Matrix{Int64}
     1 2 3
     4 5 6
    
    #доступ к элементу массива
    a [1,2]
    2

    Then in C:

    #include <stdio.h>
    
    int main() {
        int a[3][2] = {
            {1, 4},
            {2, 5},
            {3, 6}
        };
        printf("%d", a[1][0]); // Выведет 2
        return 0;
    }

    The array [1 2 3; 4 5 6] in Julia becomes the array a[2][3] in C, where the first index points to a row and the second index points to a column. In this C example, the indexing a[1][0] is used to get an element with value 2. Consequently, the array in C actually stores the elements of the Julia transposed array, which must be taken into account when accessing the elements.

When working with three-dimensional and more complex arrays, the indexing can also change. For example, if an array in Julia has dimension [2, 3, 4], in C it will be represented as [4][3][2]. This must be taken into account when referring to array elements in C code.

Example of a three-dimensional array in Julia:

a = reshape(1:24, 2, 3, 4)

[:, :, 1] =
 1  3  5
 2  4  6

[:, :, 2] =
 7   9  11
 8  10  12

[:, :, 3] =
13  15  17
14  16  18

[:, :, 4] =
19  21  23
20  22  24


println(a[1, 2, 3]) # Выведет 13

IN S:

#include <stdio.h>

int main() {
    int a[4][3][2] = {
        {
            {1, 4},
            {2, 5},
            {3, 6}
        },
        {
            {7, 10},
            {8, 11},
            {9, 12}
        },
        {
            {13, 16},
            {14, 17},
            {15, 18}
        },
        {
            {19, 22},
            {20, 23},
            {21, 24}
        }
    };
    printf("%d", a[2][1][0]); // Выведет 13
    return 0;
}

The general rule for converting indexes from Julia to C is to change the order:

(D1, D2, …​, Dn) -> [Dn][Dn-1]…​[D1].

This means that the indices in C are written in reverse order.

Working with buses

The C Function block supports bus operation both on the input/output ports and in the block parameters. The specifics of bus configuration and usage are described below:

Bus on ports

To set a bus on an input or output port:

  1. In the Type parameters of the corresponding port (Ports tab in the block settings), select the BusSignal type.

  2. The Input bus type (or Output bus type) parameters will then automatically take the form:

    BusSignal{(), Tuple(), ()}

    This is where it should be stated:

    • Bus type explicitly, e.g. BusSignal{(:s1, :s2), Tuple(Float64, Int54), ), (}. :s1, :s2 - names of signals in the bus; Tuple(Float64, Int64) - their types; ), ( - their dimensions;

    • Or the name of a variable containing the description of the bus type defined in the Engee workspace (in the variables window) or in C code.

All bus types must be specified explicitly - this is a feature of the C Function block .

Bus names should not contain spaces as they are used in C code where spaces in variable names are not allowed.

Tyres in parameters

If the bus is used as a parameter:

  • In the Value field, the tyre must be specified as a named tuple, e.g.:

    (s1 = 4.4, s2 = (b1 = 5, b))
  • If the parameters are named, for example, p, then in C code (source code of the block C Function) you can refer to bus elements as structures:

    output1 = input1 * (p.s1 + p.s2.b1);

Main

Number of input ports - number of input ports
1 (By default).

Details

Specifies the number of input ports of the block. The value of the parameters Number of input ports will correspond to the number of input ports.

Number of output ports - number of output ports
1 (By default).

Details

Specifies the number of output ports of the block. The value of the parameter Number of output ports will correspond to the number of output ports.

Function name - identifier of the common code for several blocks
no (By default)

Details

Specify a string value to merge the code of blocks C Function. Blocks with the same name in the Function name parameters will use a common code and share data through the source code tab SharedCode of the C Function block .

If the field is left blank, the selected C Function block will remain standalone (by default).

In the Function name parameters you can specify both absolute path (starts with /user) and relative path - from the current working directory (it can be found out using the pwd command, which outputs the full path to the current folder).

It is also acceptable to specify only the file name: in this case Engee first looks for the file in the current directory, and then in the search paths described in Engee path editor.

Sample time - interval between calculation steps
-1 (By default)

Details

Specify the interval between calculation steps as a non-negative number. To inherit a calculation step, set this parameter to -1.

Ports

Input

Input port 1 - input port #1
scalar | vector | matrix

Details

Fill in/set up the following fields to operate the input port:

  • Label - name (text label) of the input port. By default, the name cell is not filled in (name is not set).

  • Variable name - name of the input port variable in the source C code. By default input1.

    /* Этот код вызывается на каждом шаге расчета модели */
    output1 = input1 * param1;

    where input1 is the name of the input port variable; output1 is the name of the output port variable; param1 is the name of the parameters variable.

  • Type - data type of the input port variable in the source C code. The data type of the variable must coincide with the data type of the signal. Basic C language data types are supported:

    double (by default) | float | int8_t | uint8_t | int16_t | uint16_t | int32_t | uint32_t | int64_t | uint64_t | int128_t | uint128_t | bool | BusSignal.

  • Size - input signal dimension 1 (by default).

    The Size field uses Julia notation where the size is written as a tuple, e.g. (2, 3, 4). However, when working with this port in C code, you must take into account the change in index order. This means that if a port is specified with the dimension (2, 3, 4), then in C code the variable will have the dimension [4][3][2]. This rule is similar to the way parameters work: index order is transposed in C compared to Julia.

Output

Output port 1 - output port #1
scalar | vector | matrix

Details

Fill in/set up the following fields to operate the output port:

  • Label - name (text label) of the output port. By default, the name cell is not filled in (name is not set).

  • Variable name - the name of the output port variable in the source C code. By default output1.

    /* Этот код вызывается на каждом шаге расчета модели */
    output1 = input1 * param1;

    where output1 is the name of the output port variable; input1 is the name of the input port variable; param1 is the name of the parameters variable.

  • Type - data type of the output port variable in the source C code. The data type of the variable must coincide with the data type of the signal. Basic C language data types are supported:

    double (by default) | float | int8_t | uint8_t | int16_t | uint16_t | int32_t | uint32_t | int64_t | uint64_t | int128_t | uint128_t | bool | BusSignal.

  • Size - dimension of the output signal 1 (by default).

    The Size field uses Julia notation where the size is written as a tuple, e.g. (2, 3, 4). However, when working with this port in C code, you must take into account the change in index order. This means that if a port is specified with the dimension (2, 3, 4), then in C code the variable will have the dimension [4][3][2]. This rule is similar to the way parameters work: index order is transposed in C compared to Julia.

Parameters

Number of parameters - specify the number of parameters
1 (By default)

Details

Number of parameters used in the block.

Parameter 1 - defines parameters as a variable
1 (By default)

Details

Defines the parameters as a variable for usage in source code:

  • Name - parameter name, by default param1. Can be changed. New parameters are named param2 and then in ascending order.

  • Value - parameter value, by default 0. Can be changed. The value of the new parameters is also zero by default.

For parameters to work, go to the block code editor C Function (Edit Source Code) to the OutputCode tab and enter the name of the required parameter. By default:

/* Этот код вызывается на каждом шаге расчета модели */
output1 = input1 * param1;

where input1 is the input signal variable; output1 is the output signal variable.

WorkVariables

Number of work variables - number of operating variables
0 (By default).

Details

The number of working variables used in the block to store data specific to each instance. Working variables are accessible through the Work structure and can be used in init, step and term functions to write and read data.

Each variable is set with the parameters Name and Size in bytes, which allows storing data of different types. If a variable is assigned a larger size, it can be used as an array.

WorkVariable 1 - defines a work variable
1 (By default)

Details

Defines a working variable for usage in the source code:

  • Name - parameter name, by default work_variable1. Can be changed. New work variables are named work_variable2 and then in ascending order.

  • Size in bytes - the amount of memory allocated for the variable, in bytes. This parameter allows you to specify the size of the variable, whether it is a scalar or an array.

To work variables, you must go to the code editor of the block C Function (Edit Source Code) to the OutputCode tab and enter the name of the variable.

Build options

Source files - connecting source code files
no (By default)

Details

Used to connect additional source code files. Must include the path and filename along with the extension. For example:

/user/project/src/example.c

Include directories - defining the path to the header file directory
none (By default)

Details

Used to define a path to a directory containing header files. For example:

/user/project/include

Library directories - library catalogue path definition
none (By default)

Details

Used to define a path to a directory containing shared libraries. For example:

/user/project/third_party

Headers - header file connection
stdint.h math.h (by default)

Details

Used to connect header files. Must include the name of the header file along with the extension. For example:

example.h

Defines - definition of additional directives #define
none (By default)

Details

Used to define additional #define directives. For example:

defines LOWER=0 UPPER=300 STEP=20

Libraries - library connection
no (By default).

Details

Used to connect libraries. Must include the library name along with the extension. For example:

libexample.so
The actual data type, as well as the support for possible data types, depends on the user code within the block.

Additional options

C code generation: Yes