The wonders of ANY

While doing software development in TwinCAT, I have always been missing some sort of generic data type/container, to have some level of conformance to generic programming. “Generic programming… what’s that?”, you may ask. I like Ralf Hinze’s description of generic programming:

A generic program is one that the programmer writes once, but which works over many different data types.

I’ve been using generics in Ada and templates in C++, and many other languages have similar concepts. Why was there no such thing available in the world of TwinCAT/IEC 61131-3? For a long time there was a link to a type “ANY” in their data types section of TwinCAT3, but the only information available on the website was that the “ANY” type was not yet available. By coincidence I revisited their web page to check it out, and now a description is available! I think the documentation has done a good job describing the possibilities with the ANY-type, but I wanted to elaborate with this a little further.

First of all, the documentation only mentions that the only current possibility to use the ANY-type as VAR_INPUT inside (free) functions, but I’ve found them to be working fine as VAR_INPUT in function block methods as well (though not as VAR_INPUT inside function block bodies). The example in the documentation is good, though I would like to make some adjustments and show a good use case for such an example.

The following example compares two ANY-types to check whether they are the same or not. This is useful in an unit testing framework, especially so in the case of IEC61131-3, as operator overloading is not available in the IEC standard. Assume we would like to write an assert function block, which does comparisons of two values and checks whether they are the same or not. As operator overloading is not available, this would require us to have different names depending on what data type we would like to compare, such as:

  • AssertEquals_BOOL(bExpected : BOOL; bActual : BOOL)
  • AssertEquals_INT(nExpected : INT; nExpected : INT)
  • AssertEquals_WORD(nExpected : WORD; nActual : WORD)
  • AssertEquals_STRING(sExpected : STRING; sActual : STRING)

Comparing this with most other programming languages where you can have operator overloading usually makes the code much cleaner. In a unit testing framework you might also want to add more, such as a message, but we’ll keep it simple for this example. If we have an assert function block with the above mentioned methods, we might complement this with:

  • AssertEquals(Expected : ANY; Actual : ANY)

With this method we can provide any data type on the call of it, which makes it much more flexible. While it is simpler to do a simple comparison of the “basic” data types, normally by checking with the equality (=) or inequality (<>) operator for the data types, for the ANY types it requires us to do a little more work in the method body. But before trying to do any implementation, let’s look a little bit of how ANY works.

What happens in the background in TwinCAT is that when the code is compiled, TwinCAT internally replaces the any instances of ANY in functions/method with a structure which has the following contents:

TYPE AnyType :
STRUCT
// the type of the actual parameter
    typeclass : __SYSTEM.TYPE_CLASS ;
// the pointer to the actual parameter
    pvalue : POINTER TO BYTE;
// the size of the data, to which the pointer points
    diSize : DINT;
END_STRUCT
END_TYPE

That is really neat. Through this information, we can derive what data type and information lies within what the ANY-type is pointing to. The pointer basically points to the actual data that is defined as input for the method using the ANY-type as VAR_INPUT. But what is this __SYSTEM.TYPE_CLASS? If I do the classic “right-click” on the type, TwinCAT/Visual studio doesn’t give me any more hints or options to find out what it is. Let’s say I declare some variables with different types and send them into a method and store them into a variable holding the __SYSTEM.TYPE_CLASS. Doing this gives me different results for the variables, which is expected. I applied the following function on different types:

METHOD PRIVATE GetTypeClass : UDINT
VAR_INPUT
    AnyData : ANY;
END_VAR

GetTypeClass := AnyData.TypeClass;

And the result was as follows:

Type class instances

Now the question arises – “Is every data type always represented with this number, independent of what compiler, target (x86, x64, ARM) or TwinCAT version that this runs on?”. What is more relevant to ask is – “Is there an enumeration that represents the different type classes?”. Thanks to the help of the local Beckhoff support I was pointed to the library “Base interfaces”, which indeed holds an enumeration for the different type classes:

TypeClass enumeration

This is really great! With this information we can create a function that converts this enumeration into a string in case we for example want to utilize it for logging purposes.

FUNCTION F_AnyTypeClassToString : STRING
VAR_INPUT
    AnyTypeClass : __System.TYPE_CLASS;
END_VAR

CASE UDINT_TO_INT(AnyTypeClass) OF
    IBaseLibrary.TypeClass.TYPE_BOOL :
        F_AnyTypeClassToString := 'BOOL';
    IBaseLibrary.TypeClass.TYPE_BIT :
        F_AnyTypeClassToString := 'BIT';
    IBaseLibrary.TypeClass.TYPE_BYTE :
        F_AnyTypeClassToString := 'BYTE';
    IBaseLibrary.TypeClass.TYPE_WORD :
        F_AnyTypeClassToString := 'WORD';
    IBaseLibrary.TypeClass.TYPE_DWORD :
        F_AnyTypeClassToString := 'DWORD';
    IBaseLibrary.TypeClass.TYPE_LWORD :
        F_AnyTypeClassToString := 'LWORD';
    IBaseLibrary.TypeClass.TYPE_SINT :
        F_AnyTypeClassToString := 'SINT';
    IBaseLibrary.TypeClass.TYPE_INT :
        F_AnyTypeClassToString := 'INT';
    ...
    ...
    ...
    ELSE
        F_AnyTypeClassToString := 'UNKNOWN';
END_CASE

Now to get a feeling of how we could implement the AssertEquals method mentioned above, I’ll demonstrate how this piece of code could look like. First, the method header:

METHOD PUBLIC AssertEquals
VAR_INPUT
    Expected : ANY;
    Actual : ANY;
END_VAR
VAR
    nCount : DINT;
    bDataTypesNotEquals : BOOL := FALSE;
    bDataSizeNotEquals : BOOL := FALSE;
    bDataContentNotEquals : BOOL := FALSE;
    sExpectedDataString : STRING(80);
    sActualDataString : STRING(80);
END_VAR

We declare three booleans to detect the three different use cases of when the two ANY data differs:

  • When the types differ (e.g. real vs. int)
  • When the size differ (e.g. 2 vs 4 bytes)
  • When the content differ (e.g. 0x00A0 vs 0x00BD)

This can be realized with the following piece of code:

IF Expected.TypeClass <> Actual.TypeClass THEN
    bDataTypesNotEquals := TRUE;
END_IF

IF NOT bDataTypesNotEquals THEN
    IF (Expected.diSize <> Actual.diSize) THEN
        bDataSizeNotEquals := TRUE;
    END_IF
END_IF

IF NOT bDataTypesNotEquals AND NOT bDataSizeNotEquals THEN
    // Compare each byte in the ANY-types
    FOR nCount := 0 TO Expected.diSize-1 BY 1 DO
        IF Expected.pValue[nCount] <> Actual.pValue[nCount] THEN
            bDataContentNotEquals := TRUE;
            EXIT;
        END_IF
    END_FOR
END_IF

And to create an useful string that we can show a user, we need to write some additional code. What the below code basically does is that:

  • First it checks if the types are not equal. If not, we print the two types. Otherwise we:
  • Check if the size equals. If not, we print the two sizes. Otherwise we:
  • Check if the content of the data is the same. If not, we print the (byte) content of the two ANY data
IF bDataTypesNotEquals THEN
    sExpectedDataString := Tc2_Standard.CONCAT('(Type class = ', F_AnyTypeClassToString((Expected.TypeClass)));
    sExpectedDataString := Tc2_Standard.CONCAT(sExpectedDataString, ')');
    sActualDataString := Tc2_Standard.CONCAT('(Type class = ', F_AnyTypeClassToString(Actual.TypeClass));
    sActualDataString := Tc2_Standard.CONCAT(sActualDataString, ')');
ELSIF bDataSizeNotEquals THEN
    sExpectedDataString := Tc2_Standard.CONCAT('Data size = ', DINT_TO_STRING(Expected.diSize));
    sExpectedDataString := Tc2_Standard.CONCAT(sExpectedDataString, ')');
    sActualDataString := Tc2_Standard.CONCAT('Data size = ', DINT_TO_STRING(Actual.diSize));
    sActualDataString := Tc2_Standard.CONCAT(sActualDataString, ')');
ELSIF bDataContentNotEquals THEN
    FOR nCount := 0 TO MIN(Expected.diSize-1, 38) BY 1 DO // One byte will equal two characters (example: 255 = 0xff, 1 = 0x01)
        sExpectedDataString := Tc2_Standard.CONCAT(STR1 := Tc2_Utilities.BYTE_TO_HEXSTR(in := Expected.pValue[nCount],
                                                                                        iPrecision := 2,
                                                                                        bLoCase := FALSE),
                                                   STR2 := sExpectedDataString);
    END_FOR
    sExpectedDataString := Tc2_Standard.CONCAT(STR1 := '0x', STR2 := sExpectedDataString);

    FOR nCount := 0 TO MIN(Actual.diSize-1, 38) BY 1 DO // One byte will equal two characters (example: 255 = 0xff, 1 = 0x01)
        sActualDataString := Tc2_Standard.CONCAT(STR1 := Tc2_Utilities.BYTE_TO_HEXSTR(in := Actual.pValue[nCount],
                                                                                      iPrecision := 2,
                                                                                      bLoCase := FALSE),
                                                 STR2 := sActualDataString);
    END_FOR
    sActualDataString := Tc2_Standard.CONCAT(STR1 := '0x', STR2 := sActualDataString);
END_IF

IF bDataTypesNotEquals OR bDataSizeNotEquals OR bDataContentNotEquals THEN
    // Send the 'sActualDataString' to a logger...
END_IF

Now I’ll demonstrate all three use cases with examples.

Use case #1 – Different types

With example one I declare two different data types, INT and WORD, both with the same length (2 bytes).

VAR
    ValueOne : INT := 15000;
    ValueTwo : WORD := 120;
END_VAR

Running the AssertEquals-code with the two variables above results in:

Not same type

Use case #2 – Different lengths

How can we accomplish having two variables using the same data type but different sizes? Arrays! Remember, the ANY-type can literally take anything! We’ll declare:

VAR
    ValueOne : ARRAY[1..2] OF INT;
    ValueTwo : ARRAY[1..3] OF INT;
END_VAR

And then we get:

Not same data size

The first array consists of two integers (each 2 bytes, total of 4 bytes) while the other one consists of three integers (totalling 6 bytes), which is exactly what the message tells us.

Use case #3 – Different data content

To demonstrate this one it’s enough to create two variables with the same type and length, but where the content differs. One example of this could be:

VAR
    ValueOne : DWORD := 16#01234567;
    ValueTwo : DWORD := 16#89ABCDEF;
END_VAR

Which gives the runresult:

Not same data content

Which proves our code works really well, and also gives us a feeling of the capabilities of the ANY-type. I think it was really fun experimenting and playing around with the ANY-type. Do you know any use-case where you would find the ANY-type particularly useful? Please comment below!

  • Share on:

15 Comments, RSS

    • Jakob Sagatowski

      Hi Stefan! Thanks for your feedback. I was not aware of the T_ARG. I’m happy to see that you’ve also evaluated the usage of a [*] ARRAY, but I see that the advantage of T_ARG to ANY is that it’s possible to use in the context of VAR_IN_OUT, and thus together with [*]-arrays, which is great! Thanks for this invaluable information.

  1. Chiranjeevi

    Hi, I am new to Beckhoff plc!
    I would like to convert string to array of bytes…. My string length is 1000..

    Maxstring to byte array converts untill 255 characters only….
    Can any one please suggest….?

  2. Siam

    Hi Jakob,

    I found that this function fails in some structs.
    The example I use contains:

    BOOL;
    string(255);
    BOOL;
    REAL;
    REAL;
    REAL;
    REAL;
    REAL;
    REAL;

    It goes through the loop and fails at byte 82. The comparison happens on an unit test “Assert” where I am checking if my program updates the structure correctly. In the program, the actual value is changed from blank, to containing values. I have a suspicion this discrepancy relates to the string.

    • Jakob Sagatowski

      I’m not sure whether this relates to this actual post, or about the TcUnit framework? If it’s the TcUnit framework, could you file an issue with the example/description to reproduce the error?

      • Siam Chowdhury

        Sure, although I’m not exactly using TcUnit, I can try and make an example of this later (a bit of a time crunch here).

        I actually used some inspiration here to make a unit testing framework which uses OPC UA to find the ‘test methods’ and uses OPC UA method calls to execute the tests.
        It came from a desire to try and make the PLC unit testing framework a bit more universal.
        I did make use of this blog post (I have a line giving credit to your cleverness in the code 🙂 ) to create my Assert.Equal method in the framework.

        I found some odd cases where objects I expected to be equal were not. When putting in a break point, it showed that the byte comparison failed… I didn’t have enough time to debug further so I changed from the generic Assert.Equal to Assert.String (which does simple string comparison) to get around the issue. This first issue was with comparing two structures.

        Now I’m back here with a bit more information 🙂 The Assert.Equal method was working well for function blocks (even though it had a string in there) but just found an issue where it failed. The objects I am comparing share a common abstract base class. (in fact, it used to be the structure above, but after some refactoring, they are now function blocks)

        ex://
        FUNCTION_BLOCK ABSTRACT ObjectDetails
        FUNCTION_BLOCK PartDetails EXTENDS ObjectDetails

        I’ll get some more details when I finish my current deliverable and have some ‘breathing room’.
        Would you still like to continue this discussion on the bug report?

  3. Stefano Gronchi

    I think that you made a copy-paste error in

    IF NOT bDataTypesNotEquals AND NOT bDataTypesNotEquals THEN

    since the two conditions are equal.

    Should not it be

    IF NOT bDataTypesNotEquals AND NOT bDataSizeNotEquals THEN

    ?

  4. Stefan

    The sad thing with Any is that it requires you to have a SET for Properties… TwinCat is sometimes pretty stupid :(… Why is TwinCat forcing me to implement a Set Property when I just have a value to log….

    And no, adding a Set is not always needed, I have for example an Wrapper FB accessing some information in the backend where I dont even need a private set Method for the Property…

  5. Miguel

    Hello Jakob

    First of all, thank you for your posts, they are always very interesting to read, and you provide very useful information.

    I was recently working with Any type with a generic function an i just found a possible bug, maybe you are aware of this, or maybe i have just used incorrectly:

    Let’s say you have a simple method defined as follows

    METHOD TestingAnyType : BOOL
    VAR_INPUT
    anyValue : ANY;
    intValue : INT;
    END_VAR

    TestingAnyType := anyValue.pValue^= intValue;

    In this case i want to verify that the value provided by anyValue is the same value as initValue,
    this method will work perfectly with values between 0-255 (1 Byte) and after that it will just give the modulo as a result, is it correct? i think is wrong, i would appreciate any infos regarding this topic.

    I tested with the Build 4024.32 and with 4024.22

    Best Regards

    Miguel

  6. Rob de Kluijver

    All the functions/function blocks/methods with the name of the type in there name could do with an extension with any.
    Examples:
    AddBool, AddDint, AddLint, … in the Tc3_JsonXml library
    BYTE_TO_HEXSTR, DWORD_TO_HEXSTR, LWORD_TO_DECSTR … in the Tc2_Utilities
    Wrapper functions.
    Example:
    Using a wrapper around F_GetMappingStatus to check if a variable is linked/mapped just like the __ISVALIDREF operator

Your email address will not be published. Required fields are marked *

*

This site uses Akismet to reduce spam. Learn how your comment data is processed.