Marshalling structure with array of structure in C# .NET 7.0

76 views Asked by At

I am trying to read structures from a TwinCAT3 Beckhoff PLC with C# via ADS. This works fine. But when the structures become more complex, the marshalled values are not correct. The values are completely wrong.

I have these structures. The problem is in the ProtocolTable structure. I can't read the array "Values" correctly. The array has a size of 1001 elements and this is fixed. The pack_mod is set to 1 in the PLC structures.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct TableDefinition
    {
        public short Unitpointer;
        public short Textpointer;
        public short dataType;
    }

[StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct Values
    {
        public float realValue;
        [MarshalAs(UnmanagedType.I1)]
        public bool boolValue;
    }
[StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct ProtocolTable
    {
        public TableDefinition TableDefinition;
        [MarshalAs(UnmanagedType.ByValArray, ArraySubType = UnmanagedType.Struct, SizeConst = 1001)]
        public Values[] Values;
    }

How do I configure the Marshaller for the Values field correctly?

I am reading the structures from the PLC using the Beckhoff ADS library. This works as follows:

static void Main(string[] args)
    {
        AdsClient tcClient = new ();

        tcClient.Connect(851);

        int arraySize = 2;

        uint handle = tcClient.CreateVariableHandle("MAIN.ascProtocolTable");
        ProtocolTable[] protocolTable = (ProtocolTable[])tcClient.ReadAny(handle, typeof(ProtocolTable[]), new int[] { arraySize });

        tcClient.DeleteVariableHandle(handle);
    }

I have already tried to use a SafeArray with defined SubType, but then I get an AccessViolationException that I am trying to read protected memory.

[StructLayout(LayoutKind.Sequential, Pack = 1)]
    public struct ProtocolTable
    {
        public TableDefinition TableDefinition;
        [MarshalAs(UnmanagedType.SafeArray, SafeArrayUserDefinedSubType = typeof(Values))]
        public Values[] Values;
    }

Edit:
I have now written my own Marshaller.
Everything works now perfectly and it is also possible to read a string within my Values struct.

Updated structs:

/// <summary>
/// Represents the values structure used in PLC data.
/// </summary>
public struct Values
{
    public const int STRING_SIZE = 256;

    public float RealValue;
    public bool BoolValue;
    public string StringValue;

    /// <summary>
    /// Calculates the size of the Values structure in bytes.
    /// </summary>
    /// <returns>The size of the Values structure.</returns>
    public static int SizeOf()
    {
        return Marshal.SizeOf(typeof(float)) + Marshal.SizeOf(typeof(bool)) + STRING_SIZE;
    }
}

/// <summary>
/// Represents the table definition structure in PLC data.
/// </summary>
public struct TableDefinition
{
    public short Unitpointer;
    public short Textpointer;
    public DataType DataType;

    public TableDefinition(short unitPointer, short textPointer, DataType dataType)
    {
        this.Unitpointer = unitPointer;
        this.Textpointer = textPointer;
        this.DataType = dataType;
    }
}

/// <summary>
/// Represents the PSetTable structure in PLC data.
/// </summary>
public struct PSetTable
{
    public TableDefinition TableDefinition;
    public Values PSetValues;

    /// <summary>
    /// Calculates the size of the PSetTable structure in bytes.
    /// </summary>
    /// <returns>The size of the PSetTable structure.</returns>
    public static int SizeOf()
    {
        return Marshal.SizeOf(typeof(TableDefinition)) + Values.SizeOf();
    }
}

/// <summary>
/// Represents the ProtocolTable structure in PLC data.
/// </summary>
public struct ProtocolTable
{
    public TableDefinition TableDefinition;
    public Values[] Values;

    /// <summary>
    /// Calculates the size of the ProtocolTable structure in bytes.
    /// </summary>
    /// <param name="valuesCount">The number of values in the table.</param>
    /// <returns>The size of the ProtocolTable structure.</returns>
    public static int SizeOf(int valuesCount)
    {
        return Marshal.SizeOf(typeof(TableDefinition)) + (PlcStructs.Values.SizeOf() * valuesCount);
    }
}
        

Marshaller:

/// <summary>
/// Provides methods for marshalling binary data into structured types.
/// </summary>
internal class Marshaller
{
    private const int STRING_SIZE = 256;

    /// <summary>
    /// Marshals binary data into an array of structured types.
    /// </summary>
    /// <typeparam name="T">The type of structured data to marshal.</typeparam>
    /// <param name="bytes">The binary data to marshal.</param>
    /// <param name="readStruct">A function that reads a single structured type from a BinaryReader.</param>
    /// <param name="structSizeProvider">A function that provides the size of a single structured type in bytes.</param>
    /// <returns>An array of structured types marshalled from the binary data.</returns>
    public static T[] MarshalStructs<T>(byte[] bytes, Func<BinaryReader, T> readStruct, Func<int> structSizeProvider)
    {
        int structSize = structSizeProvider();
        int arrayLength = bytes.Length / structSize;
        T[] result = new T[arrayLength];

        using (MemoryStream stream = new(bytes))
        using (BinaryReader reader = new(stream))
        {
            for (int i = 0; i < arrayLength; i++)
            {
                result[i] = readStruct(reader);
            }
        }

        return result;
    }

    /// <summary>
    /// Marshals binary data into a PSetTable structured type.
    /// </summary>
    /// <param name="reader">The BinaryReader to read the binary data from.</param>
    /// <returns>The marshalled PSetTable structured type.</returns>
    public static PlcStructs.PSetTable MarshalPSetTable(BinaryReader reader)
    {
        PlcStructs.PSetTable pSetTable = new()
        {
            TableDefinition = MarshalTableDefinition(reader)
        };

        pSetTable.PSetValues.RealValue = reader.ReadSingle();

        pSetTable.PSetValues.BoolValue = reader.ReadBoolean();

        byte[] stringBytes = reader.ReadBytes(STRING_SIZE);

        pSetTable.PSetValues.StringValue = ExtractString(stringBytes);

        return pSetTable;
    }

    /// <summary>
    /// Marshals binary data into a ProtocolTable structured type.
    /// </summary>
    /// <param name="reader">The BinaryReader to read the binary data from.</param>
    /// <param name="valuesCount">The number of values in the ProtocolTable.</param>
    /// <returns>The marshalled ProtocolTable structured type.</returns>
    public static PlcStructs.ProtocolTable MarshalProtocolTable(BinaryReader reader, int valuesCount)
    {
        PlcStructs.ProtocolTable protocolTable = new()
        {
            TableDefinition = MarshalTableDefinition(reader),

            Values = new PlcStructs.Values[valuesCount]
        };

        for (int j = 0; j < valuesCount; j++)
        {
            protocolTable.Values[j] = new PlcStructs.Values
            {
                RealValue = reader.ReadSingle(),

                BoolValue = reader.ReadBoolean()
            };

            byte[] stringBytes = reader.ReadBytes(STRING_SIZE);

            protocolTable.Values[j].StringValue = ExtractString(stringBytes);
        }

        return protocolTable;
    }

    /// <summary>
    /// Marshals the TableDefinition from the provided BinaryReader.
    /// </summary>
    /// <param name="reader">The BinaryReader containing the bytes to marshal.</param>
    /// <returns>The marshaled TableDefinition.</returns>
    private static PlcStructs.TableDefinition MarshalTableDefinition(BinaryReader reader)
    {
        // Reading the table definition bytes from the reader and pinning the byte array in memory
        // to ensure its address remains stable during operations that require an IntPtr
        byte[] tableDefinitionBytes = reader.ReadBytes(Marshal.SizeOf(typeof(PlcStructs.TableDefinition)));
        GCHandle handle = GCHandle.Alloc(tableDefinitionBytes, GCHandleType.Pinned);

        // Getting the address of the pinned object to obtain an IntPtr for further use
        IntPtr pinnedObjectPtr = handle.AddrOfPinnedObject();
        PlcStructs.TableDefinition tableDefinition;

        if (pinnedObjectPtr != IntPtr.Zero)
        {
            tableDefinition = (PlcStructs.TableDefinition)Marshal.PtrToStructure(pinnedObjectPtr, typeof(PlcStructs.TableDefinition))!;
        }
        else
        {
            throw new Exception("Failed to allocate memory for TableDefinition.");
        }

        handle.Free();

        return tableDefinition;
    }


    /// <summary>
    /// Extracts a string from a byte array.
    /// </summary>
    /// <param name="bytes">The byte array containing the string.</param>
    /// <returns>The extracted string.</returns>
    private static string ExtractString(byte[] bytes)
    {
#pragma warning disable SYSLIB0001
        // Search for the first occurrence of the null (0x00) byte in the byte array
        int nullIndex = Array.IndexOf(bytes, (byte)0);
        if (nullIndex >= 0)
        {
            byte[] relevantBytes = new byte[nullIndex];
            Array.Copy(bytes, relevantBytes, nullIndex);
            string stringValue = Encoding.UTF7.GetString(relevantBytes);
            return stringValue;
        }
        else
        {
            string stringValue = Encoding.UTF7.GetString(bytes);
            return stringValue;
        }
#pragma warning restore SYSLIB0001
    }
}
0

There are 0 answers