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

The C Interface

This chapter is the contract between Structured Text and C. It says which C type a declaration has, how a parameter is passed, and which symbols a library must provide.

The compiler can write these declarations for you, see Generating Headers.

Types

Structured TextCSize in bits
BOOLbool8
BYTEuint8_t8
SINTint8_t8
USINTuint8_t8
WORDuint16_t16
INTint16_t16
UINTuint16_t16
DWORDuint32_t32
DINTint32_t32
UDINTuint32_t32
LWORDuint64_t64
LINTint64_t64
ULINTuint64_t64
REALfloat32
LREALdouble64
TIME, DATE, TIME_OF_DAY, DATE_AND_TIMEuint32_t32
LTIME, LDATE, LTIME_OF_DAY, LDATE_AND_TIMEint64_t64
CHARuint8_t8
WCHARuint16_t16
STRING[n]char[n + 1]8 * (n + 1)
WSTRING[n]uint16_t[n + 1]16 * (n + 1)
REF_TO T, POINTER TO TT*64
ARRAY[a..b] OF TT[b - a + 1]

A string holds one more element than its declared length, for the terminator. A pointer is 64 bits, which is the size of LWORD and not of DWORD.

Functions

A FUNCTION becomes a C function. The table above gives the type of a parameter, and the block that declares it decides how the parameter arrives:

  • A VAR_INPUT of an elementary type is passed by value.
  • A VAR_INPUT of a string, array, or struct type is passed as a pointer to the caller’s variable. A callee that the compiler generates copies the value on entry, so the caller sees no change. A callee that you write in C must do the same and not write through the pointer.
  • A VAR_INPUT {ref}, a VAR_IN_OUT, and a VAR_OUTPUT are passed as a pointer to the caller’s variable. A write through the pointer reaches the caller.
TYPE Point:
    STRUCT
        x: DINT;
        y: DINT;
    END_STRUCT
END_TYPE

FUNCTION F1: DINT
    VAR_INPUT
        i: DINT;
        s: STRING[10];
        p: Point;
    END_VAR
    VAR_IN_OUT
        io: DINT;
    END_VAR
    VAR_OUTPUT
        o: DINT;
    END_VAR
END_FUNCTION
typedef struct {
    int32_t x;
    int32_t y;
} Point;

int32_t F1(int32_t i, char* s, Point* p, int32_t* io, int32_t* o);

The parameters keep the order of the declaration blocks.

Return values

An elementary return type is the return value of the C function.

An aggregate return type (string, array, or struct) is returned through a pointer that the caller provides, and that pointer is the first parameter. The C function then returns void.

FUNCTION RetString: STRING[20]
    VAR_INPUT
        n: DINT;
    END_VAR
END_FUNCTION
void RetString(char* RetString, int32_t n);

Function blocks

A FUNCTION_BLOCK is a struct plus a function that takes a pointer to an instance. Every variable block becomes a member of the struct, in declaration order, and that includes the private VAR members. A VAR_TEMP block is not a member, because it lives only for the duration of one call.

FUNCTION_BLOCK FB1
    VAR_INPUT
        i: DINT;
        s: STRING[10];
    END_VAR
    VAR_IN_OUT
        io: DINT;
    END_VAR
    VAR_OUTPUT
        o: DINT;
    END_VAR
    VAR
        priv: DINT;
    END_VAR
END_FUNCTION_BLOCK
typedef struct {
    uint64_t* __vtable;
    int32_t i;
    char s[11];
    int32_t* io;
    int32_t o;
    int32_t priv;
} FB1_type;

void FB1(FB1_type* self);

Important

The first member of every function block struct is __vtable, the pointer to the method table. Structured Text has no virtual keyword, so every function block gets the member, whether it uses inheritance or not. A C struct without it has the wrong layout.

A PROGRAM has the same shape, but without the __vtable member, and the compiler creates its one instance as the global <Program>_instance. Do not use programs in a library.

Inheritance

A FUNCTION_BLOCK Derived EXTENDS Base embeds the base as its first member, named __Base, and the __vtable member stays in the root of the chain. Write the C struct the same way, nested and not flattened:

FUNCTION_BLOCK Base
    VAR
        b: SINT;
    END_VAR
END_FUNCTION_BLOCK

FUNCTION_BLOCK Derived EXTENDS Base
    VAR_INPUT
        c: DINT;
    END_VAR
END_FUNCTION_BLOCK
typedef struct {
    uint64_t* __vtable;
    int8_t b;
} Base_type;

typedef struct {
    Base_type __Base;
    int32_t c;
} Derived_type;

The nesting keeps the padding at the end of the base, so c sits at offset 16 and an instance takes 24 bytes on both sides. A flattened struct with the same three members compiles as well, but it puts c at offset 12 in 16 bytes, so the two sides read different memory and nothing reports it. The generated headers nest for you.

Struct layout

Layout and alignment follow the rules of C. In C, declare a normal struct. In another language, force the C layout, for example with #[repr(C)] in Rust:

use std::ffi::c_char;

#[repr(C)]
pub struct MyStruct {
    x: i32,
    y: *mut i32,
    z: [c_char; 256],
}

Initialization

The compiler writes a constructor function for every type and one for every source file. The constructor of a source file sets the globals of that file and calls the constructors of the types in it. It is registered in the constructor list of the binary, so it runs before the application starts and no manual call is necessary.

SymbolPurpose
<TypeName>__ctorInitializes one instance of a struct or function block
<FunctionBlock>__FB_INITThe FB_INIT method of a function block, called by the constructor of the type
__unit_<file>_<hash>__ctorInitializes the globals of one source file, and calls the constructors above

In the last symbol, <file> is the file name with every character that is not a letter, a digit, or an underscore replaced by an underscore, so fb1.st becomes fb1_st. The <hash> is eight hexadecimal characters that come from the full path, which keeps two files of the same name apart.

A function block that needs initialization in C implements the FB_INIT symbol:

void myFunctionBlock__FB_INIT(myFunctionBlock_type* self) {
    self->a = 1;
    self->b = 2;
}

The declaration on the Structured Text side only states that the method exists:

{external}
FUNCTION_BLOCK myFunctionBlock
    VAR
        a: DINT;
        b: DINT;
    END_VAR

    METHOD FB_INIT
    END_METHOD
END_FUNCTION_BLOCK

The C side must also define myFunctionBlock, the body of the function block, because the constructor of the type writes its address into the method table.

Constructors for external code

The compiler writes no constructor for an {external} unit until you ask for it, so the FB_INIT above is never called. Two options control for which units the compiler writes constructors:

OptionUse
--constructors-onlyWrite the generated constructors and no bodies. For building the constructor object of an external library. It implies --generate-external-constructors
--generate-external-constructorsWrite constructors for {external} units as well. For the application that links such a library
# 1. Build the constructor object of the library
plc --constructors-only -c -o libext_ctor.o my_lib.pli

# 2. Build and ship the shared library
gcc -shared -fPIC -o libext.so my_lib.c libext_ctor.o

# 3. Build the application, with constructors for the external declarations
plc -L. -lext -i my_lib.pli --generate-external-constructors app.st

For a library that mixes foreign code with Structured Text sources, compile the sources with constructors and archive the result:

plc iec61131-st/*.st -c --generate-external-constructors -o st.o
ar crs libst.a st.o

What’s next

You do not have to write these declarations by hand. The next chapter makes the compiler generate them.