Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Header Generator

C code and Structured Text code can call each other through linked functions. Both need compatible declarations. For

FUNCTION scale: DINT
    VAR_INPUT
        value: DINT;
        factor: INT;
    END_VAR
    VAR_IN_OUT
        total: DINT;
    END_VAR
END_FUNCTION

the C declaration is int32_t scale(int32_t value, int16_t factor, int32_t* total);. The widths, pointer parameters, and symbol name must match. The header generator translates the validated project’s declarations into a C header. The examples below show its mappings and current limits.

flowchart LR
    parse[Parse] --> index[Index] --> annotate[Annotate] --> validate[Validate] --> codegen[Codegen] --> link[Link]
    validate --> header[Header]
    style header fill:#bfdbfe,stroke:#000,stroke-width:1px,stroke-dasharray:4 3,color:#0f172a

The generator runs after validation, in place of codegen: the run stops once the headers are written and no object file is produced. It reads the declarations of every unit and the global index; the bodies are never looked at.

Invocation

These commands request headers and stop the pipeline after validation:

CommandHeadersName
plc --generate-headers a.st b.stone per source file, next to ita.h, b.h
plc --generate-headers a.st b.st -o apione combined header, next to the first source with declarationsapi.h
plc generate plc.json headersone per source of the build description, next to itsource name

--header-output <dir> moves the headers into a directory, which is created when it is missing. It works with both commands. The three remaining options belong to the generate subcommand only. --header-prefix <name> names the header after the prefix instead of after the source, so every unit writes to the same file and only the declarations of the last unit survive. --header-language rust is accepted by the command line and rejected by the generator, because only C is implemented. --include-stubs is parsed and ignored.

A unit whose declarations are all external or included, such as an -i include file, produces an empty model and no file. The include guard is built from the path of the header, relative to the working directory when the header is inside it: each / and . becomes _, a character that is not a letter, a digit, or _ is dropped, the result is upper case, and one _ is appended. So include/motor.h gets INCLUDE_MOTOR_H_. A combined header is the exception, because its guard is built before the .h is appended: -o api gets API_.

From declarations to a template

The generator first collects declarations in a template model, then renders that model as text:

pub struct TemplateData {
    /// Aliases (typedefs), structs, and enums
    pub user_defined_types: UserDefinedTypes,

    /// extern declarations, including one instance per program
    pub global_variables: Vec<Variable>,

    /// Prototypes: functions, function block bodies, methods, actions
    pub functions: Vec<Function>,
}

pub struct Variable {
    /// Already a C type name, such as `int32_t` or `Point*`
    pub data_type: String,

    pub name: String,

    /// Default, Array(size), Declaration(value), Variadic, Struct, or MultidimensionalArray(sizes)
    pub variable_type: VariableType,
}

The generator visits globals, user types, POUs, and implementations. It skips external and included declarations, generated constructors, and types with names that start with __. The sizes of an inline array declaration, such as a: ARRAY[0..1, 0..2] OF DINT inside a POU, are read from the helper type that the pre-processor created for it.

Built-in types are translated from the compiler’s table of built-in types. Integers use intN_t or uintN_t; BOOL uses bool; REAL and LREAL use float_t and double_t. Date and time types use time_t. Strings become arrays of char or int16_t, including a terminator slot. User type names stay unchanged.

Array bounds and string lengths that are constant expressions rather than literals are evaluated through the index, so STRING[MESSAGE_LEN] with a constant MESSAGE_LEN := 80 becomes char[81]. A bound that is not constant stops the run with an error.

The example, rendered

The project below covers the main declaration forms. Each following subsection pairs a construct with trimmed C output.

TYPE Speed: (Slow, Fast := 10, Turbo); END_TYPE

TYPE Point:
    STRUCT
        x: DINT;
        label: STRING[20];
    END_STRUCT
END_TYPE

TYPE Message: STRING[80]; END_TYPE
TYPE Grid: ARRAY[0..1, 0..2] OF DINT; END_TYPE
TYPE PointRef: REF_TO Point; END_TYPE
TYPE Percent: INT(0..100); END_TYPE

VAR_GLOBAL
    counter: DINT;
    origin: Point;
    callback: __FPOINTER scale;
END_VAR

FUNCTION scale: DINT
    VAR_INPUT
        value: DINT;
    END_VAR
    VAR_INPUT {ref}
        p: Point;
    END_VAR
    VAR_IN_OUT
        total: DINT;
    END_VAR
    VAR_OUTPUT
        overflow: BOOL;
    END_VAR
END_FUNCTION

FUNCTION sum: DINT
    VAR_INPUT
        args: {sized} DINT...;
    END_VAR
END_FUNCTION

FUNCTION_BLOCK Buffer
    VAR_INPUT
        limit: INT;
    END_VAR
    VAR
        count: DINT;
    END_VAR
    METHOD push: BOOL
        VAR_INPUT
            value: DINT;
        END_VAR
    END_METHOD
END_FUNCTION_BLOCK

ACTIONS Buffer
    ACTION reset
    END_ACTION
END_ACTIONS

FUNCTION_BLOCK RingBuffer EXTENDS Buffer
    VAR
        head: DINT;
    END_VAR
END_FUNCTION_BLOCK

PROGRAM main
    VAR
        i: DINT;
    END_VAR
END_PROGRAM

Named types

Named types become typedefs. An enum is a typedef of its numeric type plus one #define per variant, named Type_Variant so that variants of different enums cannot clash. The index pre-processor has already given every variant a value, but the generator reads back only a literal one and counts one up from the variant before it for the rest. A variant with a computed value, such as A := 2+3, therefore gets the wrong number:

typedef int16_t Percent;
typedef Point* PointRef;
typedef int32_t Grid[2][3];
typedef char Message[81];

typedef int32_t Speed;
#define Speed_Slow ((Speed)0)
#define Speed_Fast ((Speed)10)
#define Speed_Turbo ((Speed)11)

typedef struct {
    int32_t x;
    char label[21];
} Point;

Stateful POUs

Stateful POUs follow the split the Codegen chapter describes. A function block or program is a struct with the _type suffix that holds inputs, outputs, in-outs, and locals in declaration order, and its body is a function that takes a pointer to that struct. The __vtable member the polymorphism lowerer added is rendered as uint64_t*, and an extended block embeds its parent as a member named after the base type, again with the _type suffix.

A program also gets an extern for its instance. Methods and actions take the instance pointer first and are named Parent__member, the C spelling of the qualified name:

typedef struct {
    uint64_t* __vtable;
    int16_t limit;
    int32_t count;
} Buffer_type;

typedef struct {
    Buffer_type __Buffer;
    int32_t head;
} RingBuffer_type;

typedef struct {
    int32_t i;
} main_type;

extern main_type main_instance;

void Buffer(Buffer_type* self);
bool Buffer__push(Buffer_type* self, int32_t value);
void RingBuffer(RingBuffer_type* self);
void main(main_type* self);
void Buffer__reset(Buffer_type* self);

Functions

Functions become prototypes with parameters in declaration order. Scalar inputs are values; {ref} inputs, in-outs, and outputs are pointers. Struct, array, and string parameters are also pointers. Array and string parameters include capacity comments. Sized variadics become a count and a pointer; unsized variadics become ...:

int32_t scale(int32_t value, Point* p, int32_t* total, bool* overflow);

int32_t sum(int32_t args_count, int32_t* args);

Globals

Globals are extern declarations with their C type. A variable declared as __FPOINTER f refers to a function, so the generator adds a function pointer typedef named f_ptr with the signature of f, once per header, and declares the variable with it:

typedef int32_t (*scale_ptr)(int32_t, Point*, int32_t*, bool*);

extern int32_t counter;
extern Point origin;
extern scale_ptr callback;

The generator sorts aliases by dependency, so TYPE Msgs: ARRAY[0..1] OF Msg is written after Msg even when it is declared before it, and it drops the generated aliases that nothing names. The template writes the include guard, <stdint.h>, <stdbool.h>, <math.h>, <time.h>, and <dependencies.plc.h>, a file the compiler never writes and the C side must supply, then aliases, enums, structs, globals, and functions, inside an extern "C" block.

Note

Developer note. That order is not a C declaration order. Aliases come first, and the structs keep their declaration order, so a typedef that names a struct (PointRef, scale_ptr) and a struct that names a later struct both refer to a type C has not seen yet. The header of the project above does not compile for this reason.

Combining headers

With -o, the generator appends the per-unit models in unit order and renders one header. It writes to --header-output or the directory of the first unit with declarations.

Where it lives

WhatWhere
Header generatorcompiler/plc_header_generator
Generate step, command linecompiler/plc_driver