Search code examples
oopcodesys

How to override a struct in a derived FB


I'm stuck while designing a basic FB for our machine using OOP principles.

A machine uses different modules like this:

module1 : BaseModuleFB;
module2 : BaseModuleFB;

A module (some part of the machine) needs data to function (like positions, delays, etc.) This data will be stored in a recipe so grouping the data in a struct makes sence.

FUNCTION_BLOCK BaseModuleFB
VAR
     Data : BaseModuleDataStruct;
END_VAR

Now I have a different machine with some extra requirement. And also some extra data is needed. So I extend the module:

FUNCTION_BLOCK ModuleWithExtraFunctionFB EXTENDS BaseModuleFB
VAR
     Data : ModuleWithExtraFunctionDataStruct;
END_VAR

The code above will not compile because the variable Data is already in use in the base class.

The Data structs look like this by the way:

TYPE BaseModuleDataStruct:
STRUCT
    position1:INT;
END_STRUCT
END_TYPE

TYPE ModuleWithExtraFunctionDataStruct EXTENDS BaseModuleDataStruct:
STRUCT
    position2:INT;
END_STRUCT
END_TYPE

Another option I thought of was creating a property called Data. This property can be overritten by the derived class.

But this approach also failed because you cannot change the type of the overridden property.

Maybe someone has some nice ideas about this? Thanks.


Solution

  • Why not just move the contents of the struct into the function block:

    FUNCTION_BLOCK BaseModuleFB
    VAR
        // instead of "Data : BaseModuleDataStruct;"
        position1: INT;
    END_VAR
    
    FUNCTION_BLOCK ModuleWithExtraFunctionFB EXTENDS BaseModuleFB
    VAR
        // instead of "Data : ModuleWithExtraFunctionDataStruct;"
        position2: INT;
        // you can access both "position1" and position2" here
    END_VAR
    

    If you need to have the data in structures, the simplest way would be to just use several structures (as pointed by Fred):

    FUNCTION_BLOCK BaseModuleFB
    VAR
        Data : BaseModuleDataStruct;
    END_VAR
    
    FUNCTION_BLOCK ModuleWithExtraFunctionFB EXTENDS BaseModuleFB
    VAR
        // Data is already available here.
        Data2 : ModuleWithExtraFunctionDataStruct;
    END_VAR
    

    If you absolutely must access a single structure inside your function block, there is no simple way to do it in a completely enclosed way, however, you could pass the responsibility of defining the struct to the user of the function block and inject it either by using interfaces or pointers:

    1. For interfaces, we will use the __QUERYINTERFACE operator.

    // An interface with a Method/Property that gives the Data structure
    // It needs to extend "__System.IQueryInterface" for us to be bale to use the "__QUERYINTERFACE" operator on it later
    INTERFACE IDataProvider EXTENDS __System.IQueryInterface
    METHOD GetModuleAData : BaseModuleDataStruct
    
    // An interface extension that adds a Method/Property that gives the extended Data structure
    INTERFACE IDataProviderB EXTENDS IDataProvider
    METHOD GetModuleBData : ModuleWithExtraFunctionDataStruct
    
    FUNCTION_BLOCK BaseModuleFB
    VAR
        _dataProvider: IDataProvider;
    END_VAR
    METHOD FB_Init: BOOL
    VAR_INPUT
        bInitRetains: BOOL; // Internal built-in hidden argument. Don't touch!
        bInCopyCode: BOOL;  // Internal built-in hidden argument. Don't touch!
        dataProvider: IDataProvider;
    END_VAR
    // Inside the FB_Init method: "THIS^._dataProvider := dataProvider;"
    
    
    FUNCTION_BLOCK ModuleWithExtraFunctionFB EXTENDS BaseModuleFB
    VAR
        _dataProviderB: IDataProviderB;
    END_VAR
    METHOD FB_Init: BOOL
    VAR_INPUT
        bInitRetains: BOOL; // Internal built-in hidden argument. Don't touch!
        bInCopyCode: BOOL;  // Internal built-in hidden argument. Don't touch!
        dataProvider: IDataProvider;
        // We will use the __QUERYINTERFACE operator to "cast" IDataProvider to IDataProviderB
    END_VAR
    VAR
        success: BOOL;
    END_VAR
    // Inside the FB_Init method:
    // success := __QUERYINTERFACE(_dataProvider, _dataProviderB);
    

    Then in the base methods you should be able to use _dataProvider.GetModuleAData and in the extended methods _dataProviderB.GetModuleBData.


    2. For raw pointers, we can just pass them:

    FUNCTION_BLOCK BaseModuleFB
    VAR
        pdata: POINTER TO BaseModuleDataStruct;
    END_VAR
    METHOD SetData: BOOL
    VAR_IN_OUT
        data: BaseModuleDataStruct;
    END_VAR
    // Inside the SetDatamethod: "THIS^.pdata:= ADR(data);"
    
    
    FUNCTION_BLOCK ModuleWithExtraFunctionFB EXTENDS BaseModuleFB
    VAR
        pdata2: POINTER TO ModuleWithExtraFunctionDataStruct;
    END_VAR
    METHOD SetData: BOOL
    VAR_IN_OUT
        data: BaseModuleDataStruct;
    END_VAR
    // Inside the SetDatamethod: "THIS^.pdata:= ADR(data);"
    //                           "THIS^.pdata2:= ADR(data);"
    // We are assuming that the user passed data of type "ModuleWithExtraFunctionDataStruct" here!!!
    // Care needs to be taken here, otherwise we may get access violations!
    

    In your Program on the first run call the SetData methods for both Function Blocks. Make sure you are passing the correct data struct to avoid any access violations!

    Then in the base methods you should be able to use pdata^.position1 and in the extended methods pdata2^position2.


    3. We could also combine the two for direct access to the structure while retaining the type safety of interfaces:

    INTERFACE IDataProvider EXTENDS __System.IQueryInterface
    METHOD GetDataAPtr : POINTER TO BaseModuleDataStruct
    
    INTERFACE IDataProviderB EXTENDS IDataProvider
    METHOD GetDataBPtr : POINTER TO ModuleWithExtraFunctionDataStruct
    
    FUNCTION_BLOCK BaseModuleFB
    VAR
        pdata: POINTER TO BaseModuleDataStruct;
    END_VAR
    METHOD FB_Init: BOOL
    VAR_INPUT
        bInitRetains: BOOL; // Internal built-in hidden argument. Don't touch!
        bInCopyCode: BOOL;  // Internal built-in hidden argument. Don't touch!
        dataProvider: IDataProvider;
    END_VAR
    // Inside the FB_Init method: "THIS^.pdata:= dataProvider.GetDataAPtr();"
    
    
    FUNCTION_BLOCK ModuleWithExtraFunctionFB EXTENDS BaseModuleFB
    VAR
        pdata2: POINTER TO ModuleWithExtraFunctionDataStruct;
    END_VAR
    METHOD FB_Init: BOOL
    VAR_INPUT
        bInitRetains: BOOL; // Internal built-in hidden argument. Don't touch!
        bInCopyCode: BOOL;  // Internal built-in hidden argument. Don't touch!
        dataProvider: IDataProvider;
    END_VAR
    VAR
        success: BOOL;
        _dataProviderB: IDataProviderB;
    END_VAR
    // Inside the FB_Init method:
    // success := __QUERYINTERFACE(_dataProvider, _dataProviderB);
    // IF (success) THEN
    //     THIS^.pdata2 := _dataProviderB.GetDataBPtr();
    // END_IF
    

    Then in the base methods you should be able to use pdata^.position1 and in the extended methods pdata2^.position2.


    4. If you'd prefer to not use pointers, you can use references instead:

    This option was proposed by Stefan Roelofs in a comment below.

    INTERFACE IDataProvider EXTENDS __System.IQueryInterface
    METHOD GetDataARef : REFERENCE TO BaseModuleDataStruct
    
    INTERFACE IDataProviderB EXTENDS IDataProvider
    METHOD GetDataBRef : REFERENCE TO ModuleWithExtraFunctionDataStruct
    
    FUNCTION_BLOCK BaseModuleFB
    VAR
        rdata: REFERENCE TO BaseModuleDataStruct;
    END_VAR
    METHOD FB_Init: BOOL
    VAR_INPUT
        bInitRetains: BOOL; // Internal built-in hidden argument. Don't touch!
        bInCopyCode: BOOL;  // Internal built-in hidden argument. Don't touch!
        dataProvider: IDataProvider;
    END_VAR
    // Inside the FB_Init method: "THIS^.rdata REF= dataProvider.GetDataARef();"
    
    
    FUNCTION_BLOCK ModuleWithExtraFunctionFB EXTENDS BaseModuleFB
    VAR
        rdata2: REFERENCE TO ModuleWithExtraFunctionDataStruct;
    END_VAR
    METHOD FB_Init: BOOL
    VAR_INPUT
        bInitRetains: BOOL; // Internal built-in hidden argument. Don't touch!
        bInCopyCode: BOOL;  // Internal built-in hidden argument. Don't touch!
        dataProvider: IDataProvider;
    END_VAR
    VAR
        success: BOOL;
        _dataProviderB: IDataProviderB;
    END_VAR
    // Inside the FB_Init method:
    // success := __QUERYINTERFACE(_dataProvider, _dataProviderB);
    // IF (success) THEN
    //     THIS^.rdata2 REF= _dataProviderB.GetDataBRef();
    // END_IF
    

    Then, inside your Function Blocks you may want to use the __ISVALIDREF() operator to validate that the cast/conversion was successful. After that in the base methods you should be able to use rdata.position1 and in the extended methods rdata2.position2.

    NOTE: You may get many C0410 warnings:

    C0410: COMPATIBILITY WARNING: A write Access to a Property of type REFERENCE calls the SET-Accessor for versions < 3.5.10.0 and writes the reference, but calls the GET-Accessor for versions >= 3.5.10.0 and writes the value! Use the operator REF= if you want to assign the reference.

    If this annoys you, you may disable the C0410 warning in your compiler (Project Settings).



    I uploaded an example PLCOpenXML file on GDrive, you can try importing it and play around with it.