It turns out idea to use records in ADS client instead of structs wasn’t a good idea. I expected some issues, mostly with lack of parameterless constructor, but things turned out worse than expected.

let’s consider simple struct example

[<StructLayout(LayoutKind.Sequential, Pack=1)>]
type SimpleStruct =
  struct
    val byteVar: BYTE
  end
  
[<StructLayout(LayoutKind.Sequential, Pack=1)>]
type ContainingStruct =
  struct
    [<field: MarshalAs(UnmanagedType.Struct)>]
    val nested: SimpleStruct
  end
[<StructLayout(LayoutKind.Sequential, Pack=1)>]
type ContainingStructArray =
  struct
    [<field: MarshalAs(UnmanagedType.ByValArray, SizeConst=10,ArraySubType=UnmanagedType.Struct)>]
    val nested: SimpleStruct array
  end

Assert.Equal(1, typedefof<SimpleStruct> |> Marshal.SizeOf)
Assert.Equal(1, typedefof<ContainingStruct> |> Marshal.SizeOf)
Assert.Equal(10, typedefof<ContainingStructArray> |> Marshal.SizeOf)

all passes like a charm, now let’s rewrite our structs to records

[<StructLayout(LayoutKind.Sequential, Pack=1)>]
type SimpleRecord = {
  byteVar: BYTE
}
  
[<StructLayout(LayoutKind.Sequential, Pack=1)>]
type ContainingRecord = {
  [<field: MarshalAs(UnmanagedType.Struct)>]
  nested: SimpleRecord
}
[<StructLayout(LayoutKind.Sequential, Pack=1)>]
type ContainingRecordArray = {
  
  [<field: MarshalAs(UnmanagedType.ByValArray, SizeConst=10,ArraySubType=UnmanagedType.Struct)>]
  nested: SimpleRecord array
}

Assert.Equal(1, typedefof<SimpleRecord> |> Marshal.SizeOf) //success
Assert.Equal(1, typedefof<ContainingRecord> |> Marshal.SizeOf) //success
Assert.Equal(10, typedefof<ContainingRecordArray> |> Marshal.SizeOf) //failure, actual size is 40

note that it only doesn’t work on array of structs, not sure how it works under the hood, but somehow it always defaults to 4 bytes, as if we used sizeof<SimpleRecord>, thus array of 10 items, each of them 4 bytes.

Solution?

I’m aware of none yet, beside writing custom code to serialize/deserialize records to/from byte arrays. Which itself would be that bad idea (if we ignore performance) as I could handle endianness conversions if by any chance communication with PLC would occur from big endian machine.