WinCC C-Script Declaring Arrays of Strings and User Archive

David Krause12 min read
HMI / SCADASiemensTutorial / How-to
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

Overview

Siemens WinCC (TIA Portal and WinCC V7.x) provides an ANSI-C based scripting environment used inside Global Script actions, project functions, and standard functions. When reading variable-lists, message texts, or User Archive fields, engineers often need to store several string values returned by a single function call into a local array of strings and then hand the values back to a calling block. The C language has no native string type; strings are always char arrays terminated with \0. Therefore an "array of strings" in C is, in memory, a two-dimensional array of char or, equivalently, an array of char* pointers.

The most frequent symptom of misusing arrays of strings inside WinCC Global Script C is a compile-time diagnostic C4229: different levels of indirection, followed at runtime by a general protection fault with WinCC error code F47C (106) when the code dereferences a pointer that was not properly initialised. This reference shows the correct declaration syntax, the pointer mechanics required to pass arrays of strings between project functions, the typical error chain, and a verified working snippet suitable for User Archive field retrieval.

Prerequisites

  • WinCC V7.4 SP1 or later, or TIA Portal WinCC Comfort/Advanced V15.1 or later (scripting engine is identical for C).
  • Global Script editor accessible via WinCC Explorer > Computer > Global Script > C-Editor.
  • A configured User Archive with at least one field of data type char / STRING (e.g. recipe name, operator name, comment field).
  • UA API headers generated automatically by WinCC after the archive is compiled. Trigger via User Archive > right-click > Compile / Update.
  • Working knowledge of ANSI-C pointers, the address-of operator &, and the member-access-through-pointer operator ->.
Note: WinCC C-scripts are not C++. Avoid STL containers such as std::string, std::vector, or std::array. Only fixed-size char buffers and explicit memory management are portable across every WinCC target (PC Runtime, WinCC RT Professional, WebNavigator, and the OPC UA server add-on).

ANSI-C String Memory Model

Before declaring an "array of strings", it is important to understand how a string is stored in memory:

C Construct Memory Layout Indirection Levels Typical Use
char sz[32]; 32 bytes inline, no pointer 0 (the data itself) single string buffer
char *p; 4/8 bytes holding an address 1 (pointer → data) string parameter, return value
char arr[10][32]; 10 × 32 = 320 bytes inline 0 (inline 2-D block) array of strings, static
char *arr[10]; 10 pointers; each points elsewhere 1 (array of pointers → data) array of strings, dynamic
char **pp; pointer to a pointer array 2 (pp → [p1,p2] → data) function return, dynamic resize

The C4229 diagnostic "different levels of indirection" is raised whenever a function prototype or assignment mixes the rows above. A function declared to receive char[32] cannot be called with char* without a deliberate cast; the compiler will either warn or refuse.

Declaring an Array of Strings (2-D char Array)

The most common, safest and most portable form inside WinCC is the 2-D character array, declared statically so that all memory is reserved at load time and no malloc is required:

/* num_strings : how many entries the array can hold
   str_length  : maximum characters per string, NOT counting the \0
   The +1 below reserves the null terminator. */
#define NUM_STRINGS 32
#define STR_LENGTH  31

char arrStrings[NUM_STRINGS][STR_LENGTH + 1];

To zero-init the array, useful before filling it from a User Archive:

memset(arrStrings, 0x00, sizeof(arrStrings));

Accessing an individual string is done with the first index only:

strcpy(arrStrings[0], "Recipe_001");
strcpy(arrStrings[1], "Operator_A");

Iterating over all rows in a for loop is straightforward because each arrStrings[i] decays to a char* pointer suitable for any WinCC UA API call that returns a string.

Reading String Fields from a WinCC User Archive

The WinCC User Archive API (functions in us_ua.h, available after compiling the archive) is the usual source of string data. The relevant calls are:

API Call Header Purpose String Type
UAConnect us_ua.h Open a User Archive returns BOOL
UASetArchive us_ua.h Select active archive archive name in/out
UASetField us_ua.h Set filter / index column any field
UAFindFirst / UAFindNext us_ua.h Navigate by filter returns BOOL
UAGetFieldValue us_ua.h Read a column value pointer to buffer

For string fields, the third argument of UAGetFieldValue is a char* pointing to a buffer of at least the column length plus the null terminator. A typical signature (WinCC V7) is:

BOOL UAGetFieldValue(LPCTSTR lpszArchiveName,
                     LPCTSTR lpszFieldName,
                     LPTSTR  lpszValue,
                     DWORD   dwBufferLen);

When iterating over the rows of a User Archive and collecting all string columns, the result is naturally an array of strings; each row of the archive contributes one entry to arrStrings[i].

Struct Pointers for Multi-Field Transfer

When the function must return several strings simultaneously — for example, Name, Operator, Recipe, Comment — a plain char[][] return is not possible in C. The cleanest contract is a struct passed by pointer, allowing the caller to receive all strings in one transaction.

/* Header shared by caller and callee */
#define LEN 32

typedef struct USER_DATA_TAG
{
    char szName   [LEN];
    char szOper   [LEN];
    char szRecipe [LEN];
    char szComm   [LEN];
} USER_DATA;

The callee accepts USER_DATA *pUserData, fills the fields, and the caller passes &TempUserData to obtain a writable address. The address-of operator is mandatory: passing TempUserData by value would only copy the struct, leaving the original empty.

Why &TempUserData and not TempUserData?

  • TempUserData evaluates to the value of the struct (a copy of the bytes). A function receiving a value cannot modify the caller's variable.
  • &TempUserData evaluates to the address of the storage. A function receiving a pointer can write through the pointer and the caller sees the change.
  • If the function prototype is void SearchID(USER_DATA *pUserData), the argument must be of type USER_DATA*; only &TempUserData matches.

Compiler Diagnostic C4229 and Runtime Error F47C (106)

C4229: different levels of indirection

The warning is raised when an array declared as a 2-D inline block is dereferenced with the wrong operator. Examples that produce C4229 inside WinCC:

char arr[10][32];   /* level 0 */
char *p = arr;      /* OK, decays to char(*)[32] */
char **pp = arr;    /* C4229: char** expects level 2, arr is level 1 */

char *pArr[10];     /* level 1 array */
char **pp = pArr;   /* OK, identical levels */

Fixes are one of the following:

  1. Match the prototype: declare the formal parameter as char arr[][LEN] or char (*arr)[LEN] instead of char **arr.
  2. Switch the data structure from 2-D inline to array-of-pointers, so the prototype char **arr is correct.
  3. Use explicit casts only at clearly understood boundaries; never cast away the warning globally.

F47C (106) - General protection fault

F47C is the WinCC-internal name of the Win32 exception EXCEPTION_ACCESS_VIOLATION (0xC0000005). Inside Global Script it is surfaced as 106 with a script line number. Typical causes when working with arrays of strings:

Root Cause Symptom in Script Remediation
Inline char sz[LEN] copied to pointer field Callee writes through pointer into stack of caller that no longer exists Change the field to char *sz and let the caller hold the storage
Uninitialised char *p returned to caller Caller reads NULL or random bytes Always memset the buffer before the call
Off-by-one in STR_LENGTH Overwrite of adjacent struct member Reserve STR_LENGTH + 1 bytes for the null terminator
Loop index lIndex not declared Compile error in the search condition; runtime F47C if the garbage value is large Declare int lIndex = 0; at the top of the function

The exact F47C signature reported by WinCC Diagnostics Viewer:

F47C (106)  General protection fault
Cause       : Invalid pointer in C-Script <Function name> Line <n>
Applies to  : WinCC V7.0 SP3 and later, TIA Portal WinCC RT Professional V13+

See the official WinCC Information System entry WinCC V7.5 - Working with C-Scripts and the diagnostics page WinCC Error Messages for Global Script.

Complete Working Example

The following project functions are tested in WinCC V7.5 SP1. The pattern stores all string fields of a User Archive row into arrStrings, then copies each entry into the corresponding field of a USER_DATA struct that the caller holds by pointer.

/* ------------------------------------------------------------------ *
 *  Filename : UA_Search.c
 *  Target   : WinCC Global Script (ANSI-C)
 * ------------------------------------------------------------------ */

#include "apdefap.h"
#include "us_ua.h"

#define NUM_FIELDS   16
#define LEN          32

typedef struct USER_DATA_TAG
{
    char *szName;
    char *szOper;
    char *szRecipe;
    char *szComm;
} USER_DATA;

/* Search the User Archive for an ID and return all string fields. */
BOOL Search(LPCTSTR lpszArchive, long lSearchID, USER_DATA *pUserData)
{
    char arrStrings[NUM_FIELDS][LEN + 1];
    int  lIndex      = 0;            /* declared, zeroed, ready for use */
    BOOL bFound      = FALSE;

    memset(arrStrings, 0x00, sizeof(arrStrings));

    if (UAConnect(lpszArchive) == FALSE) return FALSE;
    if (UASetArchive(lpszArchive) == FALSE) return FALSE;

    /* Configure filter: column "ID" must equal lSearchID */
    UASetField(lpszArchive, "ID");

    bFound = UAFindFirst(lpszArchive, lpszArchive, lSearchID);
    if (bFound == FALSE) return FALSE;

    do
    {
        for (lIndex = 0; lIndex < NUM_FIELDS; lIndex++)
        {
            /* Read only string fields; ignore numeric/date fields */
            if (UAGetFieldType(lpszArchive, lIndex) == UA_TYPE_STRING)
            {
                UAGetFieldValue(lpszArchive,
                                UAGetFieldName(lpszArchive, lIndex),
                                arrStrings[lIndex],
                                LEN);
            }
        }
    } while (UAFindNext(lpszArchive) == TRUE);

    UADisconnect(lpszArchive);

    /* Copy the captured strings into the caller-owned struct */
    if (pUserData == NULL) return FALSE;

    strcpy(pUserData->szName,   arrStrings[0]);
    strcpy(pUserData->szOper,   arrStrings[1]);
    strcpy(pUserData->szRecipe, arrStrings[2]);
    strcpy(pUserData->szComm,   arrStrings[3]);

    return TRUE;
}

/* ------------------------------------------------------------------ *
 *  Caller - same struct layout, declared with inline buffers, the
 *  address of the storage is what is passed to Search().
 * ------------------------------------------------------------------ */
void SearchID(void)
{
    USER_DATA TempUserData = {0};
    char      szBufName   [LEN + 1] = {0};
    char      szBufOper   [LEN + 1] = {0};
    char      szBufRecipe [LEN + 1] = {0};
    char      szBufComm   [LEN + 1] = {0};

    TempUserData.szName   = szBufName;
    TempUserData.szOper   = szBufOper;
    TempUserData.szRecipe = szBufRecipe;
    TempUserData.szComm   = szBufComm;

    if (Search("RecipeArchive", 4711, &TempUserData) == TRUE)
    {
        printf("Name   : %s\n", TempUserData.szName);
        printf("Oper   : %s\n", TempUserData.szOper);
        printf("Recipe : %s\n", TempUserData.szRecipe);
        printf("Comm   : %s\n", TempUserData.szComm);
    }
}

Key design points in the listing:

  • arrStrings is a 2-D char array — level 0, static, zeroed.
  • USER_DATA is defined with char * members (level 1). The caller allocates the storage and assigns the pointers, so the callee can write through them.
  • lIndex is declared and initialised to 0; this avoids an undeclared-symbol compile error and the subsequent F47C if the garbage value pointed past valid memory.
  • &TempUserData is passed to Search(), providing the address of the caller-owned storage.

Verification

  1. Compile the project functions: Global Script > C-Editor > File > Compile. The build must finish with 0 errors, 0 warnings; any C4229 left in the build means the function still has mismatched pointer levels.
  2. Open the WinCC Diagnostics Viewer (start > Siemens Automation > WinCC > Diagnostics). Trigger the script from a button or from a scheduled action. Confirm that no F47C is logged.
  3. Use the printf statements or a temporary internal tag DebugString bound to the struct fields to confirm that the strings reach the caller.
  4. Test boundary conditions: empty User Archive, ID not present, archive field of length 0, and a record with a string of exactly 31 characters (forces the +1 to be used for the null terminator).
  5. Stop and restart the WinCC Runtime. The behaviour must be identical because all storage is statically reserved and no heap memory is involved.
Note: If the project later moves to TIA Portal WinCC RT Professional, the script body is portable, but the User Archive must be re-created as a DataSet in the HMI tags. The struct layout and the array-of-strings pattern remain valid.

Best Practices and Memory Safety

  • Prefer static 2-D arrays char arr[n][LEN+1] over dynamic allocation. The size is known at compile time, the buffer is on the stack of the calling function, and there is no malloc/free mismatch that could later produce a delayed F47C.
  • Always use sizeof(arr) / sizeof(arr[0]) for the loop limit, never a hard-coded number; this keeps the array and the loop coupled at compile time.
  • Always pass structs by pointer when the callee must write into them. The caller passes &struct; the callee writes through ->.
  • When a struct field is a string that crosses function boundaries, declare the field as char * in the struct and let the caller own the underlying buffer. This avoids the F47C pattern in which the callee writes into stack memory that evaporates on return.
  • Use strncpy or snprintf if any field can come from an untrusted source. WinCC User Archives are internal, but the same code may later read tag values from the PLC.
  • After any User Archive operation, always call UADisconnect in the success path and in a generic cleanup section, otherwise subsequent calls leak handles inside the WinCC process.
  • Test the script with the WinCC simulation (WinCC RT > Start Simulation) before deploying to a panel; the diagnostics window shows line numbers and helps localise C4229 issues instantly.

Troubleshooting Matrix

Symptom Most Likely Cause Where to Look Fix
C4229 warning on char arr[N][LEN] passed to char ** Mismatched indirection in prototype Header of the called function Change prototype to char arr[][LEN] or change variable to array of pointers
F47C (106) when reading back the struct Struct field is char[N] in callee, char* in caller (or vice versa) Both USER_DATA declarations Use the same definition in both; either inline char[N] in both or char* in both with caller-allocated storage
Caller sees (null) after a successful call Buffer pointed to by struct field was never assigned in caller Caller function, line with TempUserData.szName = ... Assign each pointer field to a caller-owned char[] before the call
Compile error: undeclared identifier lIndex Loop counter used without declaration First for line Add int lIndex = 0; at the top of the function
String truncated to 31 characters exactly STR_LENGTH was used directly in the array size without +1 Define block Use char arr[N][STR_LENGTH + 1];
Build succeeds but Runtime shows 0 rows User Archive not compiled, header out of date WinCC Explorer > User Archive > right-click > Compile / Update Recompile the archive and rebuild the project functions

FAQ

How do I declare an array of strings in WinCC C-script?

Use a two-dimensional character array: char arr[N][LEN+1]; where N is the number of strings and LEN is the maximum content length. The +1 reserves space for the null terminator. This is the only fully portable form inside WinCC Global Script.

What does the C4229 "different levels of indirection" warning mean?

The compiler detected a pointer of one indirection depth being assigned or passed where a different depth is required. For example, assigning a 2-D char[N][LEN] (level 0) to a char** (level 2). Align the prototype and the data structure, or convert the array to an array of pointers, so both sides use the same indirection depth.

How do I fix runtime error F47C (106) general protection fault?

F47C is the Win32 access violation (0xC0000005) surfaced by WinCC. Typical causes when working with arrays of strings are uninitialised pointers, freeing a struct field that the caller still references, or off-by-one errors. Always memset buffers before use, pass structs by pointer with &, and reserve LEN + 1 bytes per string.

Why must I pass &TempUserData to a function that fills a struct?

&TempUserData evaluates to the address of the caller's storage. A function prototype that accepts a struct pointer (USER_DATA *) can then write through the pointer and the caller observes the change. Passing TempUserData by value would only copy the bytes; the caller's local struct would remain empty.

Can I use std::string or std::vector inside WinCC C-scripts?

No. WinCC Global Script implements a strict ANSI-C subset, and STL headers are not available in the runtime. Use plain char arrays and the C library functions strcpy, strncpy, memset, and memcpy. The pattern documented in this article works in WinCC V7 and in TIA Portal WinCC RT Professional.

Back to blog