英文:
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 <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*>,
};
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 <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++ 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<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();
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 <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 << "}";
}
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 (
"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)
}
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.
通过集体智慧和协作来改善编程学习和解决问题的方式。致力于成为全球开发者共同参与的知识库,让每个人都能够通过互相帮助和分享经验来进步。
评论