<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0"><channel><title>Ticket search results</title><link>https://forge.codesys.com/tol/iec-snippets/snippets/</link><description>You searched for labels:"ANY"</description><language>en</language><lastBuildDate>Tue, 10 Jun 2025 21:17:41 -0000</lastBuildDate><item><title>Flexible way for text formatting - Another use of ANY type</title><link>https://forge.codesys.com/tol/iec-snippets/snippets/11/</link><description>***... 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 &gt;= iLenFormat THEN 	// Hit %, or end of format string
		IF strFormat[iPos+1] &lt;&gt; byPC THEN 					// skip %%
			IF iSegmentCnt &gt; 0 THEN 						// The first segment extends to the 2nd % char....
				strSegment := STU.MID(strFormat, iPos + 1 - iSegmentStart, iSegmentStart);
				iSegmentStart := iPos + 1;
				IF iSegmentCnt &gt; 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 &lt; 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) );
~~~


</description><dc:creator xmlns:dc="http://purl.org/dc/elements/1.1/">Strucc.c</dc:creator><pubDate>Tue, 10 Jun 2025 21:17:41 -0000</pubDate><guid isPermaLink="false">https://forge.codesys.com/tol/iec-snippets/snippets/11/</guid></item></channel></rss>