#11 Flexible way for text formatting - Another use of ANY type

Unlicensed
nobody
2025-06-10
2024-11-16
Strucc.c
No

... This is just one way to do, please let me know, if you have other idea ...

It is quite a common demand - especially when developing low level function blocks, drivers - to easily and safely generate meaningful log messages. However, using the standard libraries, CONCAT, TO_STRING, the amount of code used for logging might easily exceed the actual code. It just looks silly, especially for people coming from the C, C++ or C# world... They want to do something like this:

PLCLog.DEBUG('Got param ID:%d; Type:%s Flags:%04X; Len:%d; ', dwID, strTypeClass, dwFlags, dwDataLen);

The goal here is the small footprint, flexibility and not really the performance. The problem is however, that the printf like calls available in CODESYS libraries are only handling one placeholder (I can't see a way to map **args, or va_list to IEC code...), and start to behave crazy if there are more placeholders than arguments. So, here comes a workaround:


// Just a tricky wrapper call, to allow sprintf like functionality with multiple placeholders and
// arguments of ANY type. I Mostly use it for debug log generation - it was not intended 
// for fast performance or 100% compliance. Not a very nice implementation, and in my experiance 
// can cause access violation ...
//
// The purpose is to implement building string with a very small foorptint in the code. 
// For example:
//
//  PLCLog.DEBUG( PE6(
//      'Param ID:%d; Type:%s Flags:%04X; pData:%016X; Len:%d; DrvSpec:%016X', 
//      dwID, strTypeClass, dwFlags, pData, dwDataLen, dwDriverSpec
//  ));
//
//  As illustrated - due to the limitations of ANY inputs (can't be optional), 
//  a set of helper functions is provided PE1..PE9 for the given numver of parameters.
// 
// Limitations: 
//  - It's based on CODESYS String Utilities... (STU)... I_Strings ? Ergh...
//      ... Could use IECStringutils.Printf - see in the code
//  - String length 255
//  - Placeholders are from standard implementation (not same as in visualization)
//  - Due to ANY parameters, only parameters with Read/write access can be used...
//    means : no properties (??? Should be), function call results, or any 
//    ad-hoc calculated values.
//
// Just take it easy, let me know if you have a better way for this feature    


FUNCTION _SPrintf_Segments : STRING(255);
VAR_INPUT
    strFormat       : STRING(255);
END_VAR
VAR_IN_OUT
    aArgs           : ARRAY[*] OF __SYSTEM.AnyType;
END_VAR
VAR_OUTPUT
    lBound          : DINT := LOWER_BOUND(aArgs, 1);
    uBound          : DINT := UPPER_BOUND(aArgs, 1);
END_VAR
VAR
    iLenFormat      : INT   := 0;
    iSegmentCnt     : INT   := 0;
    iSegmentStart   : INT   := 1;
    strSegment      : STRING(255);
    iPos            : INT   := 0;
    iRes            : INT;
END_VAR
VAR_STAT CONSTANT
    byPc            : BYTE := 37; // '%'
END_VAR
_SPrintf_Segments := UTF8#'';

iPos := 0;
iLenFormat := STU.LEN(strFormat);
iSegmentStart := 1;
iSegmentCnt := 0;

lBound  := LOWER_BOUND(aArgs, 1);
uBound  := UPPER_BOUND(aArgs, 1);

FOR iPos := 0 TO iLenFormat DO
    IF strFormat[iPos] = byPC OR iPos >= iLenFormat THEN    // Hit %, or end of format string
        IF strFormat[iPos+1] <> byPC THEN                   // skip %%
            IF iSegmentCnt > 0 THEN                         // The first segment extends to the 2nd % char....
                strSegment := STU.MID(strFormat, iPos + 1 - iSegmentStart, iSegmentStart);
                iSegmentStart := iPos + 1;
                IF iSegmentCnt > UPPER_BOUND(aArgs, 1) THEN
                    EXIT;
                END_IF
                iRes := STU.StuSprintf(
                    pstFormat           :=  ADR(strSegment),
                    pVarAdr             :=  aArgs[iSegmentCnt].pValue, 
                    udiVarType          :=  aArgs[iSegmentCnt].TypeClass, 
                    pBuffer             :=  ADR(_SPrintf_Segments) + TO_UDINT(LEN(_SPrintf_Segments)),
                    dwBufferSize        :=  SIZEOF(_SPrintf_Segments) - TO_UDINT(LEN(_SPrintf_Segments))
                );
            END_IF
            iSegmentCnt := iSegmentCnt + 1;
        ELSE
            iPos := iPos +1;            
        END_IF
    END_IF
END_FOR

IF iPos < iLenFormat THEN
    // If more placeholders than arguments, just add the remaining of the format string....
    _SPrintf_Segments := STU.CONCAT(_SPrintf_Segments, STU.MID(strFormat,iLenFormat,iPos+1));
END_IF

Well, this is not too bad... so far, but it still requires an array as a parameter, what can bot be done in a compact way within the code (?). So here comes a helper function:

UNCTION PE4 : STRING(255);
VAR_INPUT
    strFormat   :   STRING(255);    // Format string
    Value1      :   ANY;            // Value for 1st placeholder
    Value2      :   ANY;            // Value for 2nd placeholder
    Value3      :   ANY;            // Value for 3rd placeholder
    Value4      :   ANY;            // Value for 4th placeholder
END_VAR
VAR
    aArgs : ARRAY[1..4] OF __SYSTEM.AnyType := [
        Value1, Value2, Value3, Value4
    ];
END_VAR
PE4 := _Sprintf_Segments(strFormat, aArgs);

Yes... Unfortunately, it is not possible to make input variables of ANY type optional... So unfortunately, there is a different helper function for 2, 3, 4 ... 9 parameters. :( Have a better idea?

This way, the final call looks like:

PLCLog.DEBUG( PE4('Got param ID:%d; Type:%s Flags:%04X; Len:%d;', dwID, strTypeClass, dwFlags, dwDataLen) );

Discussion

  • Strucc.c

    Strucc.c - 2024-11-16

    Unfortunately I cannot edit this ticket... So sorry for the layout issues...
    P.

     
  • helcioburd - 2025-06-10

    Thank You! Excellent tool as is!

     

Log in to post a comment.