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
}
}