plctwincatcodesysiec61131-3

If a VAR_INPUT is of INTERFACE type, is the value pass-by-reference or pass-by-value?


In the TwinCAT and CodeSys IEC-61131 programming environments, it's possible to declare POU VAR_INPUTs using an INTERFACE as a type specification. I believe the support for interfaces in TwinCAT and CoDeSys is an extension to the standard IEC-61131 language definition.

Question 1: When the POU is invoked, do interface VAR_INPUTs have pass-by-value (i.e. the input FB's state is copied on each execution of the called FB) or pass-by-reference semantics?

Question 2: Where is this behaviour specified or documented?


Solution

  • The interface type itself is a value, but it doesn't carry the function block it refers to. It's implemented as a pointer-to-instance's-vtable-pointer. It's used as-if it was a reference to a function block that implemented the interface, but the address returned is NOT that of the function block (that's the critical difference). That's because of the implementation:

                                        FB Instance
                                             |
    interface (PVOID) ------+        * PVOID vtable 1       +----> VTABLE 3
                            +------> * PVOID vtable 2  -----+        |
                                     *     ...                   * method 1
                                     * PVOID vtable n            *   ...
                                     * data fields               * method m
    

    So, if you read the contents of an interface, you'll get an address somewhere within the function block instance, and that address is the address of the vtable pointer within the instance. The particular vtable is the one that implements the methods of the interface (i.e. is compatible with the interface).

    We can check that this is so for some type FB_MyFB:

    INTERFACE I_Derived EXTENDS __SYSTEM.QueryInterface
    END_INTERFACE
    
    FUNCTION_BLOCK FB_MyFB IMPLEMENTS I_Derived
    ...
    END_FUNCTION_BLOCK
    
    FUNCTION F_CheckInterfaceRange(fb : REFERENCE TO FB_MyFB) : BOOL
    VAR
      ifc : I_Derived := fb;
      ifcval : POINTER TO PVOID := ADR(ifc);
    END_VAR
    ifcval := ifcval^;
    F_CheckInterfaceRange := 
      ifcval >= ADR(fb) 
      AND_THEN ifcval <= (ADR(fb) + SIZEOF(FB_MyFB) - SIZEOF(PVOID)); 
    END_FUNCTION
    

    It is seemingly impossible to get the address of the instance directly. Most likely it's an arbitrary limitation: all the vtable pointers must be valid and probably belong in a certain memory area, so you could imagine starting at whatever interface points to and walking backwards from it until you stop getting valid pointers. Those are the bounds. The instance starts with a vtable pointer, so one of those pointers you found will be it. Then examine how do the pointers look in instances of various library FB types, and then look at how the pointed-to vtables look, and I'm sure some valid heuristic would pop up that might not even be as expensive as a __QUERYINTERFACE call. CoDeSys 3 code generator is abysmal.

    The supported way, instead, is for FB to implement an interface extending SYSTEM.__QueryInterface. Then, __QUERYPOINTER is used to access that interface to get the value of THIS of the FB.

    You could imagine that __QUERYPOINTER looks a bit like:

    FUNCTION __QUERYPOINTER
    VAR_INPUT
      ifc : __SYSTEM.QueryInterface;
      ptr : REFERENCE TO PVOID;
    END_VAR
    ptr := ifc.__QUERYTHIS();
    END_FUNCTION
    

    The __SYSTEM.QueryInterface interface implements a method that casts between interfaces implemented by a FB, as long as both interfaces derive from __SYSTEM.QueryInterface, as well as a method (imagine it's called __QUERYTHIS) that returns THIS.

    The method is generated by the compiler.

    Imagine that the rest of the implementation is a bit like:

    INTERFACE __SYSTEM.QueryInterface
    PROPERTY _This_ : POINTER TO BYTE
    METHOD _This__GET : POINTER TO BYTE   // that's how CoDeSys 3 implements getters/setters
    END_PROPERTY
    ...
    END_INTERFACE
    
    FUNCTION BLOCK FB_Queryable IMPLEMENTS I_Queryable
    PROPERTY _This_ : POINTER TO BYTE
    METHOD _This__GET : POINTER TO BYTE
    _This_GET := THIS;
    END_METHOD
    END_FUNCTION_BLOCK
    

    You could similarly implement F_QueryInterface (this won't be as easy because __QUERYINTERFACE gets help from the compiler):

    FUNCTION F_QueryInterface2 : BOOL
    VAR_INPUT
      from : I_Queryable;
      to : REFERENCE TO I_Interface2;
    END_VAR
    IF from <> 0 THEN
      // the compiler would translate __QUERYINTERFACE(from, to) to something like:
      F_QueryInterface2 := from._QueryInterface_(2, ADR(to));
    END_IF
    END_FUNCTION
    
    INTERFACE I_Queryable   // cont'd
    ...
    METHOD _QueryInterface_ : BOOL
    VAR_INPUT
      typeid : INT;
      to : POINTER TO PVOID; // pointer to interface
    END_VAR
    END_INTERFACE
    
    INTERFACE I_Interface2 EXTENDS I_Queryable
    ...
    END_INTERFACE
    
    FUNCTION_BLOCK FB_MoreQueryable IMPLEMENTS I_Interface1, I_Interface2
    METHOD _QueryInterface_ : BOOL
    VAR_INPUT
      typeid : INT;
      to : POINTER TO U_Interfaces; // pointer to interface
    END_VAR
    to^.PVOID := 0;
    CASE typeId OF
      1: to^.Interface1 := THIS^;
      2: to^.Interface2 := THIS^;
    END_CASE
    _QueryInterface_ := to^.PVOID <> 0;
    END_FUNCTION_BLOCK
    
    TYPE U_Interfaces :
    UNION
       PVOID : PVOID;
       Interface1 : I_Interface1;
       Interface2 : I_Interface2;
    END_UNION
    END_TYPE