B&R Automation Studio: STRING Return Type in Functions Fix

Jason IP7 min read
B&R AutomationOther TopicPLC Programming
Licensed PE Working through this on a live machine? A Maine-licensed engineer can take it from here — included with IMD hardware, by the hour for everything else. Book an engineer

Problem Details

You want a one-line call in Structured Text such as myString := BOOL_TO_STRING(myBool); inside a B&R Automation Studio user library. The declaration editor and the compiler will not accept a user-defined function whose return type is STRING. Numeric and boolean return types (BOOL, USINT, DINT, REAL, LREAL) are accepted; the string return is rejected.

The confusing part is that B&R's own runtime libraries expose conversion helpers that appear to hand back a string in an expression. That leads engineers to assume the same construct is available to user code. It is not, and the workaround is structural rather than a setting you can toggle.

Scope note: This applies to user functions declared in a user library (IEC or ANSI C) in Automation Studio. Function blocks are not affected — they have no return value at all, so the restriction never applies to them.

Root Cause

A function in the IEC 61131-3 model returns a single value by value. Value-returning semantics require the compiler to know the exact size of the returned object and to allocate temporary storage for it in the caller's expression evaluation. A STRING in B&R is a fixed-length character array whose length is part of the declaration (STRING[80], STRING[255], ...), not a managed object with a uniform footprint. There is no single, size-agnostic string type the compiler can return by value into an arbitrary expression.

Several of the built-in "look-alike" conversion helpers are not ordinary compiled functions at all. They are resolved by the language front end and expanded during parsing/code generation, closer to a macro or built-in operator than to a library function that user code can imitate. Because they are privileged constructs of the toolchain, they can produce a string result in an expression while user libraries cannot.

Practical consequence: the restriction is a property of the compiler front end, not a project option, a library attribute, or a runtime version issue. There is no supported way to declare a user function with a STRING return type. Design around it.

Solution 1 — Function With a Pointer Output

Keep the callable as a FUNCTION, but pass the destination string in by address. The function writes into caller-owned memory and returns a status value instead of the text.

Declaration (.fun in the user library):

FUNCTION fcBoolToString : BOOL
	VAR_INPUT
		bValue      : BOOL;
		pDest       : UDINT;      (* address of target STRING *)
		nDestSize   : UINT;       (* usable bytes incl. terminator *)
	END_VAR
END_FUNCTION

Implementation (Structured Text):

FUNCTION fcBoolToString
	IF (pDest = 0) OR (nDestSize < 6) THEN
		fcBoolToString := FALSE;
		RETURN;
	END_IF

	IF bValue THEN
		brsstrcpy(pDest, ADR('true'));
	ELSE
		brsstrcpy(pDest, ADR('false'));
	END_IF

	fcBoolToString := TRUE;
END_FUNCTION

Call site:

VAR
	sFlag : STRING[10];
	bOk   : BOOL;
END_VAR

bOk := fcBoolToString(bMotorRun, ADR(sFlag), SIZEOF(sFlag));
Buffer discipline: The literal 'false' needs 5 characters plus the terminator. Declare the destination as STRING[5] or larger and always pass SIZEOF() so the function can reject undersized buffers instead of writing past the end. Writing past a STRING declared in a global data module corrupts adjacent variables and is one of the hardest faults to trace at runtime.

Solution 2 — Function Block With VAR_IN_OUT

The cleaner IEC-conformant option. VAR_IN_OUT passes the string by reference and the compiler enforces the type, so no manual address arithmetic and no size parameter are required.

Declaration (.typ/.fun in the user library):

FUNCTION_BLOCK fbBoolToString
	VAR_INPUT
		bValue : BOOL;
	END_VAR
	VAR_IN_OUT
		sDest  : STRING[80];
	END_VAR
	VAR_OUTPUT
		bDone  : BOOL;
	END_VAR
END_FUNCTION_BLOCK

Implementation:

FUNCTION_BLOCK fbBoolToString
	IF bValue THEN
		sDest := 'true';
	ELSE
		sDest := 'false';
	END_IF
	bDone := TRUE;
END_FUNCTION_BLOCK

Call site (cyclic program):

VAR
	BtoS  : fbBoolToString;
	sFlag : STRING[80];
END_VAR

BtoS.bValue := bMotorRun;
BtoS(sDest := sFlag);

Solution 3 — Inline the Conditional

For a single conversion in one place, the library round-trip is not worth it. Three lines of ST are self-documenting and generate less code than a function block instance:

IF bMotorRun THEN
	sFlag := 'true';
ELSE
	sFlag := 'false';
END_IF

Use this when the mapping is local. Move to a library only when the same conversion appears in multiple tasks, or when the text form must be centrally controlled (for example 'TRUE'/'FALSE' for a JSON or CSV payload versus 'ON'/'OFF' for an HMI label). Centralizing avoids the classic defect where one task emits 'true' and another emits 'True' into the same interface.

Option Comparison

Criterion Function + pointer Function block + VAR_IN_OUT Inline IF
Usable inside an expression No (returns status) No N/A
Lines at call site 1 2–3 (assign inputs, invoke) 3–5
Instance memory required None One instance per call site None
Compile-time type checking of destination None — raw UDINT address Full — compiler checks STRING Full
Buffer-overrun risk High if size not validated Low None
Callable from Init/Exit as well as Cyclic Yes Yes Yes
Reusable across projects Yes (library) Yes (library) Copy/paste only

Implementation and Verification

  1. Create a user library in the Logical View. Add the function or function block declaration to the library's declaration file — the return type field must stay BOOL/UDINT/void; do not attempt STRING.
  2. Write the implementation in the matching ST source file. Build the library alone first to confirm the declaration is accepted before wiring it into application code.
  3. Add the library reference to the task and declare the destination string as a local or global STRING[n], sized for the longest literal plus terminator.
  4. Build the configuration. A rejected STRING return type surfaces as a declaration/parse error on the function header, not as a link error — if you see a link error instead, the library reference is missing from the task, not the return type.
  5. Transfer to the target and open a Watch window on the destination string plus the source BOOL.
  6. Force the BOOL to TRUE and confirm the string reads true; force to FALSE and confirm false.
  7. For the pointer variant, add an explicit negative test: call it once with a deliberately undersized buffer and confirm the return value is FALSE and the buffer is untouched.
  8. Watch the variables declared immediately after the destination string in the same data module for the first few cycles. Unexpected changes there indicate the copy overran the buffer.
Pointer validity: With the ADR() approach, never store the address across cycles in a variable whose target may be reallocated. Recompute ADR() at each call. A stale pointer into freed or moved memory produces a page fault on the target rather than a clean error.

Extending the Pattern

The same two mechanisms cover every user-defined "returns text" helper — case conversion, enum-to-label lookup, fixed-point formatting, and building JSON or CSV fragments from process variables. Standardize on one style across the library so call sites look consistent:

Helper Recommended form Reason
Bool to text Function block, VAR_IN_OUT Trivial body, type safety matters more than call brevity
Enum/state to label Function block, VAR_IN_OUT Lookup table lives in the FB, destination checked by compiler
Concatenating a JSON payload Function block, VAR_IN_OUT plus length output Caller needs the produced length to append further fields
Reused inside existing pointer-based C code Function with pointer + size Matches the surrounding calling convention

Where the destination length varies between call sites, prefer the pointer form with an explicit size argument, or declare the VAR_IN_OUT at the largest length used in the project and document that minimum in the library header.

FAQ

Why can't a B&R user function return a STRING?

Functions return a value by value, and a B&R STRING is a fixed-length array whose size is part of its declaration, so the compiler has no size-agnostic string type to return into an expression. The restriction lives in the Automation Studio compiler front end and cannot be enabled by any project setting.

How do I return a string from a function block in Automation Studio?

Declare the string in VAR_IN_OUT instead of VAR_OUTPUT. The caller passes its own STRING variable, the block writes into it directly, and the compiler still type-checks the argument.

Can I use a pointer to get a string out of a function?

Yes. Pass ADR(myString) as a UDINT input plus SIZEOF(myString), write into that memory inside the function, and use the return value for a success/failure status. Always validate the size argument before copying to avoid overrunning adjacent variables.

Why do built-in conversion helpers seem to return strings when mine can't?

Some of those constructs are resolved by the language front end during parsing and code generation rather than compiled as ordinary library functions, so they behave more like built-in operators. User libraries have no access to that mechanism.

Is a function block overkill for converting one BOOL to text?

For a single local conversion, an inline IF bValue THEN s := 'true'; ELSE s := 'false'; END_IF is smaller and needs no instance memory. Move to a library block only when the same conversion is reused across tasks or when the exact text form must be consistent across an interface.

Back to blog