API guidelines

1. Introduction

  • Purpose: Provide plc library developers with information how an interface for an application written in IEC61131-3 should be designed and why.
  • Scope: This guideline applies to all developers writing libraries for use in IEC61131-3 applications.

2. API Guidelines

2.1 VAR_IN_OUT instead of pointers

If a function takes a parameter for the purpose of reading and writing to it a VAR_IN_OUT can be used instead of a pointer in the VAR_INPUT.

2.2 FUNCTION and FUNCTION_BLOCK

FUNCTION and FUNCTION_BLOCK have similar properties, but they have fundamentally different representation in the compiler. A FUNCTION is defined in a similar manner to a C function:

  • It has no backing struct
  • values defined inside it will only persist for the duration of the function call

Example:

FUNCTION myFunc : DINT
VAR_INPUT
  x  : DINT;
END_VAR
END_FUNCTION
int32_t myFunc(int32_t);

In contrast, a FUNCTION_BLOCK is backed by a struct and is globally accessible by a defined instance. To declare a FUNCTION_BLOCK, a backing struct has to be declared and passed as a reference to the function block implementation.

FUNCTION_BLOCK myFb
VAR_INPUT
  x  : DINT;
END_VAR
END_FUNCTION_BLOCK
typedef struct {
  int32_t x;
} myFunctStr;

void myFb(myFunctStr*);

2.2.1 Parameters

FUNCTION and FUNCTION_BLOCK may define input parameters. These are passed using the VAR_INPUT or VAR_IN_OUT blocks. The difference between the two blocks is how the values are passed. A VAR_INPUT variable is passed by value, while a VAR_IN_OUT variable is passed by reference. In general, it is recommended to use a VAR_IN_OUT for data that needs to be both read and written, while VAR_INPUT should be reserved to read only values.

NOTE: In FUNCTIONs complex datatypes are handled as pointers. They are however copied and changes in the function will have no effect on the actual variable.

Examples:

FUNCTION:

FUNCTION myFunc : DINT
VAR_INPUT
  myInt  : DINT;
  myString : STRING[255];
END_VAR

VAR_INPUT
  myRefStr : STRING;
END_VAR

VAR_IN_OUT
  myInOutInt : DINT;
END_VAR
END_FUNCTION
int32_t myFunc(int32_t myInt, char* myString, char* myRefStr, int32_t* myInOutInt);

FUNCTION_BLOCK:

FUNCTION_BLOCK myFb
VAR_INPUT
  myInt  : DINT;
  myString : STRING[255];
END_VAR

VAR_IN_OUT
  myInOutInt : DINT;
END_VAR
END_FUNCTION_BLOCK

typedef struct {
  int32_t myInt;
  char myString[256];
  int32_t* myInOutInt;
} myFbStruct

void myFb(myFbStruct* myFbInstance);

2.2.2 Private members

A FUNCTION_BLOCK often requires local (private) members to hold data across executions. These members have to be declared in the struct. As a side effect, these variables are visible to the users.

For example:

FUNCTION_BLOCK Count
VAR
  current : DINT;
END_VAR
END_FUNCTION_BLOCK

typedef struct {
  int32_t current;
} CountStruct;

void Count(CountStruct* countInst) {
  countInst->current = countInst->current + 1;
}

2.2.3 Return values

A FUNCTION defines a return value in the signature, while a FUNCTION_BLOCK relies on VAR_OUTPUT definitions.

Example:

FUNCTION myFunc : DINT
  VAR_INPUT
    x : DINT;
  END_VAR
  VAR_IN_OUT
    y : DINT;
  END_VAR
END_FUNCTION

The C interface would look like:

int32_t myFunc(int32_t x, int32_t* y);

The return type for a function can also include complex datatypes, such as strings, arrays and structs. Internally, complex return types are treated as reference parameters (pointers).

For complex return types, the function signature expects the return value as the first parameter.

Example:

FUNCTION myFunc : STRING
  VAR_INPUT
    x : DINT;
  END_VAR
  VAR_IN_OUT
    y : DINT;
  END_VAR
END_FUNCTION

The C interface would look like:

void myFunc(char* out, int32_t x, int32_t* y);

A FUNCTION_BLOCK should use VAR_OUTPUT for return values. Avoid using a pointer in the VAR_INPUT as a return value.

Example:

FUNCTION_BLOCK myFb
  VAR_INPUT
    x : DINT;
  END_VAR
  VAR_IN_OUT
    y : DINT;
  END_VAR
  VAR_OUTPUT
    myOut: DINT;
    myOut2: STRING[255];
  END_VAR
END_FUNCTION

The C interface would look like:

typedef struct {
  int32_t x;
  int32_t* y;
  int32_t myOut;
  char myOut2[256];

} myFbStruct;

void myFb(myFbStruct* myFbInst);

2.2.4 When to use a FUNCTION vs. FUNCTION_BLOCK

A FUNCTION can be well integrated into the API because of its return value which can be nested into expressions. They however don't keep data over subsequent executions. If you need to store static data use a FUNCTION_BLOCK or use VAR_IN_OUT.

NOTE: Do not use PROGRAMs in your libraries PROGRAMs have static instances. These are reserved for applications and should not be used in libraries.

2.3 Datatypes

The IEC61131-3 Standard defines several datatypes with their intended uses. To stay standard compliant, an API/Library should try and follow these guidelines.

2.3.1 Type sizes

Datatypes are generally convertable to C equivalent. With the compiler defaulting to 64bit, some sizes were also fixed to 64bit.

Below is a table of types and how they can be used from C

typec equivalentsizecomment
BOOLbool8
BYTEuint8_t8intended to be used as bit sequence and not as a number
SINTint8_t8
USINTuint8_t8
WORDuint16_t16
INTint16_t16
UINTuint16_t16
DINTint32_t32
DWORDuint32_t32
UDINTuint32_t32
LINTint64_t64
LWORDuint64_t64
ULINTuint64_t64
REALfloat_t32
LREALdouble_t64
TIMEtime_t64Note that all time and date types are 64 bit
LTIMEtime_t64
DATEtime_t64
LDATEtime_t64
DATE_AND_TIMEtime_t64
LDATE_AND_TIMEtime_t64
DTtime_t64
LDTtime_t64
TIME_OF_DAYtime_t64
LTIME_OF_DAYtime_t64
TODtime_t64
LTODtime_t64
POINTER TO type*type64The Pointer size is equivalent to LWORD and not DWORD
REF_TO type*type64Prefer this type to POINTER TO for standard compliance
STRINGuint8_t[]varUTF-8 String, null terminated. Default is 80 chars + 1 termination byte
WSTRINGuint16_t[]varUTF-16 (wide) String, null terminated. Default is 80 chars + 1 termination byte

2.3.2 Using Types in interfaces

When deciding on a type to use for a FUNCTION, FUNCTION_BLOCK, or STRUCT use a type that reflects the intention of the API:

  • A bit sequence should be in a BIT type like WORD and not in a numeric type like INT.
  • A variable representing a time should be stored in the appropriate time type and not an LINT or LWORD
  • A pointer should be stored as a REF_TO and not as an LWORD where possible.
  • (W)STRINGs and ARRAYs stored in VAR, VAR_INPUT, and VAR_OUTPUT sections of FUNCTION_BLOCKs are stored in the FUNCTION_BLOCK, and are passed by value.
    • A VAR_IN_OUT block can be used to force a type to be passed as a pointer. Note that VAR_IN_OUT is a read-write variable and changes to the parameter will change it for the caller.
    • FUNCTIONs expecting an ARRAY parameter can use the ARRAY[*] syntax (Variable sized array). The same functionality will be available for STRING. It is however not yet implemented.

2.4 Struct alignment

Struct alignment in plc follows the default behaviour of C. When developing a library in C a normal struct can be declared. In langugages other than C the struct has to be C compatible. For example in rust the #[repr(C)] can be used to make the struct C compatible.

Example:

TYPE myStruct:
STRUCT
    x      : DINT;
    y      : REF_TO DINT;
    z      : ARRAY[0..255] OF BYTE;
END_STRUCT
END_TYPE

The C struct would look like:


typedef struct {
  int32_t x;
  int32_t* y;
  char z[256];

} myStruct;

The rust struct would look like

#![allow(unused)]
fn main() {
#[repr(C)]
pub struct myStruct {
    x: i32,
    y: *mut i32,
    z: [c_char; 256],
}
}

2.5 FUNCTION_BLOCK initialization

Not yet implemented.