将C++字节结构转换/解析为Go语言。

huangapple go评论83阅读模式
英文:

Converting / Parsing C++ byte struct to Go

问题

我正在使用Go语言读取一些数据包,其中字段是C++数据类型。我尝试解析数据,但读取到的是垃圾值。

以下是一个小例子 - 特定数据类型的数据规范在C++中如下所示:

struct CarTelemetryData
{
    uint16    m_speed;                      
    uint8     m_throttle;                   
    int8      m_steer;                      
    uint8     m_brake;                     
    uint8     m_clutch;                     
    int8      m_gear;                       
    uint16    m_engineRPM;                  
    uint8     m_drs;                        
    uint8     m_revLightsPercent;           
    uint16    m_brakesTemperature[4];       
    uint16    m_tyresSurfaceTemperature[4]; 
    uint16    m_tyresInnerTemperature[4];   
    uint16    m_engineTemperature;          
    float     m_tyresPressure[4];           
};

以下是我在Go中定义的内容:

type CarTelemetryData struct {
    Speed                   uint16
    Throttle                uint8
    Steer                   int8
    Brake                   uint8
    Clutch                  uint8
    Gear                    int8
    EngineRPM               uint16
    DRS                     uint8
    RevLightsPercent        uint8
    BrakesTemperature       [4]uint16
    TyresSurfaceTemperature [4]uint16
    TyresInnerTemperature   [4]uint16
    EngineTemperature       uint16
    TyresPressure           [4]float32
}

对于实际的解包,我正在进行以下操作:

func decodePayload(dataStruct interface{}, payload []byte) {
    dataReader := bytes.NewReader(payload[:])
    binary.Read(dataReader, binary.LittleEndian, dataStruct)
}

payload := make([]byte, 2048)
s.conn.ReadFromUDP(payload[:])
telemetryData := &data.CarTelemetryData{}
s.PacketsRcvd += 1
decodePayload(telemetryData, payload)

我怀疑这是因为数据类型不匹配,在将字节读入Go数据类型时存在一些转换问题,而它们最初是作为C++打包的。我该如何处理这个问题?

注意:我无法控制发送的数据,这是由第三方服务发送的。

英文:

I am reading some data packets in Go, where the fields are C++ data types. I tried parsing the data but I am reading garbage values.

Here is a small example - the data spec sheet for a particular datatype is as follows in C++,

struct CarTelemetryData
{
    uint16    m_speed;                      
    uint8     m_throttle;                   
    int8      m_steer;                      
    uint8     m_brake;                     
    uint8     m_clutch;                     
    int8      m_gear;                       
    uint16    m_engineRPM;                  
    uint8     m_drs;                        
    uint8     m_revLightsPercent;           
    uint16    m_brakesTemperature[4];       
    uint16    m_tyresSurfaceTemperature[4]; 
    uint16    m_tyresInnerTemperature[4];   
    uint16    m_engineTemperature;          
    float     m_tyresPressure[4];           
};

And below is what I have defined in Go

type CarTelemetryData struct {
	Speed                   uint16
	Throttle                uint8
	Steer                   int8
	Brake                   uint8
	Clutch                  uint8
	Gear                    int8
	EngineRPM               uint16
	DRS                     uint8
	RevLightsPercent        uint8
	BrakesTemperature       [4]uint16
	TyresSurfaceTemperature [4]uint16
	TyresInnerTemperature   [4]uint16
	EngineTemperature       uint16
	TyresPressure           [4]float32
}

For the actual un-marshalling, I am doing this -

func decodePayload(dataStruct interface{}, payload []byte) {
	dataReader := bytes.NewReader(payload[:])
	binary.Read(dataReader, binary.LittleEndian, dataStruct)
}

payload := make([]byte, 2048)
s.conn.ReadFromUDP(payload[:])
telemetryData := &data.CarTelemetryData{}
s.PacketsRcvd += 1
decodePayload(telemetryData, payload)

I suspect that this is because the datatypes are not equivalent and there is some conversion issue while reading the bytes into Go data-types, whereas they have been originally packages as C++. How can I deal with this?

Note: I don't have any control over the data that is sent, this is sent by a third party service.

答案1

得分: 2

你所面临的问题与结构成员的对齐有关。你可以在这里阅读更多相关信息,简而言之,C++编译器有时会添加填充字节,以保持体系结构所期望的自然对齐方式。如果不使用该对齐方式,可能会导致性能下降甚至访问冲突。

例如,在x86/x64上,大多数类型的对齐方式通常(但不一定保证)与其大小相同。我们可以看到:

#include <cstdint>
#include <type_traits>

std::size_t offsets[] = {
    std::alignment_of_v<std::uint8_t>,
    std::alignment_of_v<std::uint16_t>,
    std::alignment_of_v<std::uint32_t>,
    std::alignment_of_v<std::uint64_t>,
    std::alignment_of_v<__uint128_t>,
    std::alignment_of_v<std::int8_t>,
    std::alignment_of_v<std::int16_t>,
    std::alignment_of_v<std::int32_t>,
    std::alignment_of_v<std::int64_t>,
    std::alignment_of_v<__int128_t>,
    std::alignment_of_v<float>,
    std::alignment_of_v<double>,
    std::alignment_of_v<long double>,
    std::alignment_of_v<void*>,
};

编译为:

offsets:
        .quad   1
        .quad   2
        .quad   4
        .quad   8
        .quad   16
        .quad   1
        .quad   2
        .quad   4
        .quad   8
        .quad   16
        .quad   4
        .quad   8
        .quad   16
        .quad   8

由于这些(和其他)实现细节,不建议依赖内部表示。然而,在某些情况下,其他方法可能不够快(例如逐个字段序列化),或者您可能无法更改C++代码,就像OP一样。

binary.Read期望紧凑的数据,但C++会使用填充。我们需要使用特定于编译器的指令,例如#pragma pack(1),或者在Go结构体中添加填充。第一种选项对于OP来说不可行,所以我们将使用第二种选项。

我们可以使用offsetof宏来确定结构成员相对于结构本身的偏移量。我们可以这样做:

#include <array>
#include <cstddef>
#include <cstdint>

using int8 = std::int8_t;
using uint8 = std::uint8_t;
using uint16 = std::uint16_t;

struct CarTelemetryData {
    uint16 m_speed;
    uint8 m_throttle;
    int8 m_steer;
    uint8 m_brake;
    uint8 m_clutch;
    int8 m_gear;
    uint16 m_engineRPM;
    uint8 m_drs;
    uint8 m_revLightsPercent;
    uint16 m_brakesTemperature[4];
    uint16 m_tyresSurfaceTemperature[4];
    uint16 m_tyresInnerTemperature[4];
    uint16 m_engineTemperature;
    float m_tyresPressure[4];
};

// C++没有反射(尚未),所以我们需要列出每个成员
constexpr auto offsets = std::array{
    offsetof(CarTelemetryData, m_speed),
    offsetof(CarTelemetryData, m_throttle),
    offsetof(CarTelemetryData, m_steer),
    offsetof(CarTelemetryData, m_brake),
    offsetof(CarTelemetryData, m_clutch),
    offsetof(CarTelemetryData, m_gear),
    offsetof(CarTelemetryData, m_engineRPM),
    offsetof(CarTelemetryData, m_drs),
    offsetof(CarTelemetryData, m_revLightsPercent),
    offsetof(CarTelemetryData, m_brakesTemperature),
    offsetof(CarTelemetryData, m_tyresSurfaceTemperature),
    offsetof(CarTelemetryData, m_tyresInnerTemperature),
    offsetof(CarTelemetryData, m_engineTemperature),
    offsetof(CarTelemetryData, m_tyresPressure),
};

constexpr auto sizes = std::array{
    sizeof(CarTelemetryData::m_speed),
    sizeof(CarTelemetryData::m_throttle),
    sizeof(CarTelemetryData::m_steer),
    sizeof(CarTelemetryData::m_brake),
    sizeof(CarTelemetryData::m_clutch),
    sizeof(CarTelemetryData::m_gear),
    sizeof(CarTelemetryData::m_engineRPM),
    sizeof(CarTelemetryData::m_drs),
    sizeof(CarTelemetryData::m_revLightsPercent),
    sizeof(CarTelemetryData::m_brakesTemperature),
    sizeof(CarTelemetryData::m_tyresSurfaceTemperature),
    sizeof(CarTelemetryData::m_tyresInnerTemperature),
    sizeof(CarTelemetryData::m_engineTemperature),
    sizeof(CarTelemetryData::m_tyresPressure),
};

constexpr auto computePadding() {
    std::array<std::size_t, offsets.size()> result;

    std::size_t expectedOffset = 0;

    for (std::size_t i = 0; i < offsets.size(); i++) {
        result.at(i) = offsets.at(i) - expectedOffset;
        expectedOffset = offsets.at(i) + sizes.at(i);
    }

    return result;
}

auto padding = computePadding();

它编译为(constexpr FTW)

padding:
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   1
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   2

因此,在x86上,EngineRPM之前需要一个字节,在TyresPressure之前需要两个字节。

因此,让我们检查一下是否有效

C++:

#include <cstddef>
#include <cstdint>
#include <iomanip>
#include <iostream>
#include <span>

using int8 = std::int8_t;
using uint8 = std::uint8_t;
using uint16 = std::uint16_t;

struct CarTelemetryData {
    uint16 m_speed;
    uint8 m_throttle;
    int8 m_steer;
    uint8 m_brake;
    uint8 m_clutch;
    int8 m_gear;
    uint16 m_engineRPM;
    uint8 m_drs;
    uint8 m_revLightsPercent;
    uint16 m_brakesTemperature[4];
    uint16 m_tyresSurfaceTemperature[4];
    uint16 m_tyresInnerTemperature[4];
    uint16 m_engineTemperature;
    float m_tyresPressure[4];
};

int main() {
    CarTelemetryData data = {
        .m_speed = 1,
        .m_throttle = 2,
        .m_steer = 3,
        .m_brake = 4,
        .m_clutch = 5,
        .m_gear = 6,
        .m_engineRPM = 7,
        .m_drs = 8,
        .m_revLightsPercent = 9,
        .m_brakesTemperature = {10, 11, 12, 13},
        .m_tyresSurfaceTemperature = {14, 15, 16, 17},
        .m_tyresInnerTemperature = {18, 19, 20, 21},
        .m_engineTemperature = 22,
        .m_tyresPressure = {23, 24, 25, 26},
    };

    std::cout << "b := []byte{" << std::hex << std::setfill('0');

    for (auto byte : std::as_bytes(std::span(&data, 1))) {
        std::cout << "0x" << std::setw(2) << static_cast<unsigned>(byte)
                  << ", ";
    }

    std::cout << "}";
}

结果为

b := []byte{0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x07, 0x00, 0x08, 0x09, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0d, 0x00, 0x0e, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, 0x00, 0x14, 0x00, 0x15, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x41, 0x00, 0x00, 0xc0, 0x41, 0x00, 0x00, 0xc8, 0x41, 0x00, 0x00, 0xd0, 0x41, }

让我们在Go中使用它:

// Type your code here, or load an example.
// Your function name should start with a capital letter.
package main

import (
	"bytes"
	"encoding/binary"
	"fmt"
)

type CarTelemetryData struct {
	Speed                   uint16
	Throttle                uint8
	Steer                   int8
	Brake                   uint8
	Clutch                  uint8
	Gear                    int8
	_                       uint8
	EngineRPM               uint16
	DRS                     uint8
	RevLightsPercent        uint8
	BrakesTemperature       [4]uint16
	TyresSurfaceTemperature [4]uint16
	TyresInnerTemperature   [4]uint16
	EngineTemperature       uint16
	_                       uint16
	TyresPressure           [4]float32
}

func main() {
	b := []byte{0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x07, 0x00, 0x08, 0x09, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0d, 0x00, 0x0e, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, 0x00, 0x14, 0x00, 0x15, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x41, 0x00, 0x00, 0xc0, 0x41, 0x00, 0x00, 0xc8, 0x41, 0x00, 0x00, 0xd0, 0x41}

	var dataStruct CarTelemetryData

	dataReader := bytes.NewReader(b[:])
	binary.Read(dataReader, binary.LittleEndian, &dataStruct)

	fmt.Printf("%+v", dataStruct)
}

它打印出

{Speed:1 Throttle:2 Steer:3 Brake:4 Clutch:5 Gear:6 _:0 EngineRPM:7 DRS:8 RevLightsPercent:9 BrakesTemperature:[10 11 12 13] TyresSurfaceTemperature:[14 15 16 17] TyresInnerTemperature:[18 19 20 21] EngineTemperature:22 _:0 TyresPressure:[23 24 25 26]}

如果去掉填充字节,它将失败。

英文:

The issue you're facing has to do with the alignment of struct members. You can read more about it here but, in short, the C++ compiler will sometimes add padding bytes in order to maintain the natural alignment expected by the architecture. If that alignment is not used, it may cause degraded performance or even an access violation.

For x86/x64, for example, the alignment of most types will usually (but not necessarily guaranteed to) be the same as the size. We can see that

#include &lt;cstdint&gt;
#include &lt;type_traits&gt;

std::size_t offsets[] = {
    std::alignment_of_v&lt;std::uint8_t&gt;,
    std::alignment_of_v&lt;std::uint16_t&gt;,
    std::alignment_of_v&lt;std::uint32_t&gt;,
    std::alignment_of_v&lt;std::uint64_t&gt;,
    std::alignment_of_v&lt;__uint128_t&gt;,
    std::alignment_of_v&lt;std::int8_t&gt;,
    std::alignment_of_v&lt;std::int16_t&gt;,
    std::alignment_of_v&lt;std::int32_t&gt;,
    std::alignment_of_v&lt;std::int64_t&gt;,
    std::alignment_of_v&lt;__int128_t&gt;,
    std::alignment_of_v&lt;float&gt;,
    std::alignment_of_v&lt;double&gt;,
    std::alignment_of_v&lt;long double&gt;,
    std::alignment_of_v&lt;void*&gt;,
};

compiles to

offsets:
        .quad   1
        .quad   2
        .quad   4
        .quad   8
        .quad   16
        .quad   1
        .quad   2
        .quad   4
        .quad   8
        .quad   16
        .quad   4
        .quad   8
        .quad   16
        .quad   8

Due to these (and other) implementation details, it may be advisable to not rely on the internal representation. In some cases, however, other methods may not be fast enough (such as serializing field by field), or you may not be able to change the C++ code, like OP.

binary.Read expects packed data, but C++ will use padding. We need to either use a compiler-dependent directive such as #pragma pack(1) or add padding the Go struct. The first is not an option for OP, so we'll use the second.

We can use the offsetof macro to determine the offset of a struct member relative to the struct itself. We can do something like

#include &lt;array&gt;
#include &lt;cstddef&gt;
#include &lt;cstdint&gt;

using int8 = std::int8_t;
using uint8 = std::uint8_t;
using uint16 = std::uint16_t;

struct CarTelemetryData {
    uint16 m_speed;
    uint8 m_throttle;
    int8 m_steer;
    uint8 m_brake;
    uint8 m_clutch;
    int8 m_gear;
    uint16 m_engineRPM;
    uint8 m_drs;
    uint8 m_revLightsPercent;
    uint16 m_brakesTemperature[4];
    uint16 m_tyresSurfaceTemperature[4];
    uint16 m_tyresInnerTemperature[4];
    uint16 m_engineTemperature;
    float m_tyresPressure[4];
};

// C++ has no reflection (yet) so we need to list every member
constexpr auto offsets = std::array{
    offsetof(CarTelemetryData, m_speed),
    offsetof(CarTelemetryData, m_throttle),
    offsetof(CarTelemetryData, m_steer),
    offsetof(CarTelemetryData, m_brake),
    offsetof(CarTelemetryData, m_clutch),
    offsetof(CarTelemetryData, m_gear),
    offsetof(CarTelemetryData, m_engineRPM),
    offsetof(CarTelemetryData, m_drs),
    offsetof(CarTelemetryData, m_revLightsPercent),
    offsetof(CarTelemetryData, m_brakesTemperature),
    offsetof(CarTelemetryData, m_tyresSurfaceTemperature),
    offsetof(CarTelemetryData, m_tyresInnerTemperature),
    offsetof(CarTelemetryData, m_engineTemperature),
    offsetof(CarTelemetryData, m_tyresPressure),
};

constexpr auto sizes = std::array{
    sizeof(CarTelemetryData::m_speed),
    sizeof(CarTelemetryData::m_throttle),
    sizeof(CarTelemetryData::m_steer),
    sizeof(CarTelemetryData::m_brake),
    sizeof(CarTelemetryData::m_clutch),
    sizeof(CarTelemetryData::m_gear),
    sizeof(CarTelemetryData::m_engineRPM),
    sizeof(CarTelemetryData::m_drs),
    sizeof(CarTelemetryData::m_revLightsPercent),
    sizeof(CarTelemetryData::m_brakesTemperature),
    sizeof(CarTelemetryData::m_tyresSurfaceTemperature),
    sizeof(CarTelemetryData::m_tyresInnerTemperature),
    sizeof(CarTelemetryData::m_engineTemperature),
    sizeof(CarTelemetryData::m_tyresPressure),
};

constexpr auto computePadding() {
    std::array&lt;std::size_t, offsets.size()&gt; result;

    std::size_t expectedOffset = 0;

    for (std::size_t i = 0; i &lt; offsets.size(); i++) {
        result.at(i) = offsets.at(i) - expectedOffset;
        expectedOffset = offsets.at(i) + sizes.at(i);
    }

    return result;
}

auto padding = computePadding();

which compiles to (constexpr FTW)

padding:
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   1
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   0
        .quad   2

So, on x86, we need one byte before EngineRPM and two bytes before TyresPressure.

So, let's check if that works.

C++:

#include &lt;cstddef&gt;
#include &lt;cstdint&gt;
#include &lt;iomanip&gt;
#include &lt;iostream&gt;
#include &lt;span&gt;

using int8 = std::int8_t;
using uint8 = std::uint8_t;
using uint16 = std::uint16_t;

struct CarTelemetryData {
    uint16 m_speed;
    uint8 m_throttle;
    int8 m_steer;
    uint8 m_brake;
    uint8 m_clutch;
    int8 m_gear;
    uint16 m_engineRPM;
    uint8 m_drs;
    uint8 m_revLightsPercent;
    uint16 m_brakesTemperature[4];
    uint16 m_tyresSurfaceTemperature[4];
    uint16 m_tyresInnerTemperature[4];
    uint16 m_engineTemperature;
    float m_tyresPressure[4];
};

int main() {
    CarTelemetryData data = {
        .m_speed = 1,
        .m_throttle = 2,
        .m_steer = 3,
        .m_brake = 4,
        .m_clutch = 5,
        .m_gear = 6,
        .m_engineRPM = 7,
        .m_drs = 8,
        .m_revLightsPercent = 9,
        .m_brakesTemperature = {10, 11, 12, 13},
        .m_tyresSurfaceTemperature = {14, 15, 16, 17},
        .m_tyresInnerTemperature = {18, 19, 20, 21},
        .m_engineTemperature = 22,
        .m_tyresPressure = {23, 24, 25, 26},
    };

    std::cout &lt;&lt; &quot;b := []byte{&quot; &lt;&lt; std::hex &lt;&lt; std::setfill(&#39;0&#39;);

    for (auto byte : std::as_bytes(std::span(&amp;data, 1))) {
        std::cout &lt;&lt; &quot;0x&quot; &lt;&lt; std::setw(2) &lt;&lt; static_cast&lt;unsigned&gt;(byte)
                  &lt;&lt; &quot;, &quot;;
    }

    std::cout &lt;&lt; &quot;}&quot;;
}

results in

b := []byte{0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x07, 0x00, 0x08, 0x09, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0d, 0x00, 0x0e, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, 0x00, 0x14, 0x00, 0x15, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x41, 0x00, 0x00, 0xc0, 0x41, 0x00, 0x00, 0xc8, 0x41, 0x00, 0x00, 0xd0, 0x41, }

Let's use that in Go:

// Type your code here, or load an example.
// Your function name should start with a capital letter.
package main

import (
	&quot;bytes&quot;
	&quot;encoding/binary&quot;
	&quot;fmt&quot;
)

type CarTelemetryData struct {
	Speed                   uint16
	Throttle                uint8
	Steer                   int8
	Brake                   uint8
	Clutch                  uint8
	Gear                    int8
	_                       uint8
	EngineRPM               uint16
	DRS                     uint8
	RevLightsPercent        uint8
	BrakesTemperature       [4]uint16
	TyresSurfaceTemperature [4]uint16
	TyresInnerTemperature   [4]uint16
	EngineTemperature       uint16
	_                       uint16
	TyresPressure           [4]float32
}

func main() {
	b := []byte{0x01, 0x00, 0x02, 0x03, 0x04, 0x05, 0x06, 0x00, 0x07, 0x00, 0x08, 0x09, 0x0a, 0x00, 0x0b, 0x00, 0x0c, 0x00, 0x0d, 0x00, 0x0e, 0x00, 0x0f, 0x00, 0x10, 0x00, 0x11, 0x00, 0x12, 0x00, 0x13, 0x00, 0x14, 0x00, 0x15, 0x00, 0x16, 0x00, 0x00, 0x00, 0x00, 0x00, 0xb8, 0x41, 0x00, 0x00, 0xc0, 0x41, 0x00, 0x00, 0xc8, 0x41, 0x00, 0x00, 0xd0, 0x41}

	var dataStruct CarTelemetryData

	dataReader := bytes.NewReader(b[:])
	binary.Read(dataReader, binary.LittleEndian, &amp;dataStruct)

	fmt.Printf(&quot;%+v&quot;, dataStruct)
}

which prints

{Speed:1 Throttle:2 Steer:3 Brake:4 Clutch:5 Gear:6 _:0 EngineRPM:7 DRS:8 RevLightsPercent:9 BrakesTemperature:[10 11 12 13] TyresSurfaceTemperature:[14 15 16 17] TyresInnerTemperature:[18 19 20 21] EngineTemperature:22 _:0 TyresPressure:[23 24 25 26]}

Take the padding bytes out and it fails.

huangapple
  • 本文由 发表于 2022年2月20日 06:54:45
  • 转载请务必保留本文链接:https://go.coder-hub.com/71189898.html
匿名

发表评论

匿名网友

:?: :razz: :sad: :evil: :!: :smile: :oops: :grin: :eek: :shock: :???: :cool: :lol: :mad: :twisted: :roll: :wink: :idea: :arrow: :neutral: :cry: :mrgreen:

确定