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.
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));
'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
- 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 attemptSTRING. - 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.
- 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. - Build the configuration. A rejected
STRINGreturn 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. - Transfer to the target and open a Watch window on the destination string plus the source
BOOL. - Force the
BOOLtoTRUEand confirm the string readstrue; force toFALSEand confirmfalse. - For the pointer variant, add an explicit negative test: call it once with a deliberately undersized buffer and confirm the return value is
FALSEand the buffer is untouched. - 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.
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.