C#使用MemoryMarshal.Cast转字节数组到结构体报错解决方案
MemoryMarshal.Cast<TFrom, TTo> 仅支持完全不包含引用、指针的非托管值类型做转换。你定义的结构体里MarshalZone[]、WeatherForecastSample[]属于C#托管数组,本质是引用类型——哪怕加了[MarshalAs(UnmanagedType.ByValArray)]特性,这个特性也只在传统互操作封送场景生效,CLR层面数组字段仍然是引用,因此会抛出Only value types without pointers or references are supported异常。
另外你当前结构体设置Pack = 0是错误配置:F1 22 UDP遥测包采用1字节紧凑对齐,Pack=0会使用平台默认对齐规则(Windows环境通常为8字节对齐),会导致字段内存偏移和实际数据包不匹配,解析出的数据会完全错误,必须改为Pack=1。
方案1:零拷贝高性能方案(推荐,适配UDP高频数据包场景)
将结构体改为纯非托管值类型,无需封送、无内存拷贝,解析性能最高,支持直接使用MemoryMarshal.Cast。
.NET 7及以上版本实现
使用InlineArray特性定义定长值类型数组,不需要unsafe代码:
- 先定义对应长度的内联数组结构体:
using System.Runtime.CompilerServices; [InlineArray(21)] public struct MarshalZoneArray21 { public MarshalZone Item; } [InlineArray(56)] public struct WeatherForecastArray56 { public WeatherForecastSample Item; }
- 修改原
PacketSessionData结构体,替换托管数组字段,移除无效的MarshalAs特性,修正Pack值:
[StructLayout(LayoutKind.Sequential, Pack = 1)] public struct PacketSessionData { public PacketHeader m_header; public byte m_weather; public sbyte m_trackTemperature; public sbyte m_airTemperature; public byte m_totalLaps; public ushort m_trackLength; public byte m_sessionType; public sbyte m_trackId; public byte m_formula; public ushort m_sessionTimeLeft; public ushort m_sessionDuration; public byte m_pitSpeedLimit; public byte m_gamePaused; public byte m_isSpectating; public byte m_spectatorCarIndex; public byte m_sliProNativeSupport; public byte m_numMarshalZones; public MarshalZoneArray21 m_marshalZones; public byte m_safetyCarStatus; public byte m_networkGame; public byte m_numWeatherForecastSamples; public WeatherForecastArray56 m_weatherForecastSamples; public byte m_forecastAccuracy; public byte m_aiDifficulty; public uint m_seasonLinkIdentifier; public uint m_weekendLinkIdentifier; public uint m_sessionLinkIdentifier; public byte m_pitStopWindowIdealLap; public byte m_pitStopWindowLatestLap; public byte m_pitStopRejoinPosition; public byte m_steeringAssist; public byte m_brakingAssist; public byte m_gearboxAssist; public byte m_pitAssist; public byte m_pitReleaseAssist; public byte m_ERSAssist; public byte m_DRSAssist; public byte m_dynamicRacingLine; public byte m_dynamicRacingLineType; public byte m_gameMode; public byte m_ruleSet; public uint m_timeOfDay; public byte m_sessionLength; }
修改完成后直接调用原代码即可正常转换:
var telemetry = MemoryMarshal.Cast<byte, PacketSessionData>(bytes)[0];
内联数组可以像普通数组一样直接通过索引访问元素。
.NET 6及更低版本实现
如果使用旧版本.NET,可以用unsafe固定缓冲区实现同样的零拷贝效果:
[StructLayout(LayoutKind.Sequential, Pack = 1)] public unsafe struct PacketSessionData { // 其余字段和上面一致,仅替换两个数组字段 public byte m_numMarshalZones; public fixed byte m_marshalZones[21 * 5]; // 单MarshalZone大小为5字节 public byte m_safetyCarStatus; public byte m_networkGame; public byte m_numWeatherForecastSamples; public fixed byte m_weatherForecastSamples[56 * 8]; // 单WeatherForecastSample大小为8字节 // 其余字段和上面一致 }
读取数组元素时通过内存读取即可:
public static unsafe MarshalZone GetMarshalZone(ref PacketSessionData data, int index) { if (index < 0 || index >= 21) throw new ArgumentOutOfRangeException(nameof(index)); fixed (byte* ptr = data.m_marshalZones) { return MemoryMarshal.Read<MarshalZone>(new Span<byte>(ptr + index * 5, 5)); } }
方案2:封送转换方案(无需修改结构体,性能较低)
如果不想修改现有结构体定义,可以使用Marshal.PtrToStructure做封送转换,该方法会识别MarshalAs特性自动完成定长数组的转换,但每次转换会产生内存拷贝和封送开销,不适合高频UDP数据包场景:
PacketSessionData telemetry; fixed (byte* pBytes = bytes) { telemetry = Marshal.PtrToStructure<PacketSessionData>((IntPtr)pBytes); }
注意使用该方案同样需要把结构体的Pack值改为1,否则字段偏移错误会导致解析失败。
内容的提问来源于stack exchange,提问作者J.Meulenbeld

