含位域结构体的序列化与反序列化​

需求背景

CPU 与智能网卡(DPU)交互时,需要把表项数据按协议序列化成字节流。由于 DPU 按 bit 粒度解析数据,协议对布局通常有明确约束。

  • 传输的数据必须是字节流
  • DPU 逐 bit 读取,字段需要紧密排列
  • 协议规定比特位按从高到低(MSB-first)排列

说明:CPU 主机序为小端序。但这里的协议是按 bit 流(MSB-first)定义的,不等价于“把结构体内存布局直接拷贝出去”。

带位域结构体的序列化

手动序列化:先推导出标准答案

先用一个最小例子把协议格式算清楚,后面所有实现都以这份“标准答案”做对照。

结构体定义如下。

struct Vrf {
    BYTE hitFlag : 1;
    BYTE pad1 : 7;
    WORD vrfId : 12;
    WORD pad2 : 4;
    DWORD rsv;
};

hitFlag = 1vrfId = 100 为例。

  • hitFlag 只占 1 bit,写到第一个字节最高位,因此第一个字节开头是 1000 0000(0x80)
  • vrfId 占 12 bit,100 的二进制是 0000 0110 0100,紧接着写入 bit 流
  • 其余位域填 0,rsv 也填 0
#include <bits/stdc++.h>

using namespace std;
using BYTE = unsigned char;
using WORD = unsigned short int;
using DWORD = unsigned int;
using U64 = unsigned long long;

struct Vrf {
    BYTE hitFlag : 1;
    BYTE pad1 : 7;
    WORD vrfId : 12;
    WORD pad2 : 4;
    DWORD rsv;

    Vrf() = default;

    explicit Vrf(const std::vector<BYTE>& config) {
        hitFlag = config[0] >> 7;
        pad1 = 0;
        vrfId = (config[1] << 4) | (config[2] >> 4);
        pad2 = 0;
        rsv = 0;
    }

    explicit Vrf(WORD vrfId)
        : hitFlag(1), pad1(0), vrfId(vrfId), pad2(0), rsv(0) {};

    std::vector<BYTE> serialize() const {
        std::vector<BYTE> buf(sizeof(Vrf));
        buf[0] = (hitFlag << 7) | pad1;
        buf[1] = static_cast<BYTE>(vrfId >> 4);
        buf[2] = static_cast<BYTE>(((vrfId & 0x0F) << 4) | pad2);
        buf[3] = static_cast<BYTE>((rsv >> 24) & 0xFF);  // rsv 最高字节
        buf[4] = static_cast<BYTE>((rsv >> 16) & 0xFF);
        buf[5] = static_cast<BYTE>((rsv >> 8) & 0xFF);
        buf[6] = static_cast<BYTE>(rsv & 0xFF);
        return buf;
    }

    std::vector<BYTE> serializeByMemcpy() const {
        std::vector<BYTE> buf(sizeof(Vrf));
        memcpy(buf.data(), this, sizeof(Vrf));
        return buf;
    }

    std::string toString() const {
        return "hitFlag:" + std::to_string(hitFlag) +
               ", vrfId:" + std::to_string(vrfId);
    }
} __attribute__((packed));

void dumpVector(const std::vector<BYTE>& data, size_t dumpCount) {
    cout << hex << uppercase;

    for (size_t i = 0; i < data.size(); ++i) {
        cout << setw(2) << setfill('0') << static_cast<int>(data[i]) << " ";
    }
    cout << dec << endl;
}

int main() {
    Vrf vrf(100);
    cout << vrf.toString() << endl;

    auto config = vrf.serialize();
    cout << "Serialized: " << endl;
    dumpVector(config);
    auto config2 = vrf.serializeByMemcpy();
    cout << "SerializedByMemcpy: " << endl;
    dumpVector(config2);

    return 0;
}

serialize() 输出如下:80 06 40 00 00 00 00


内存布局拷贝测试

这里用 memcpy 直接展示结构体在内存中的布局,作为对比。

std::vector<BYTE> serializeByMemcpy() const {
    std::vector<BYTE> buf(sizeof(Vrf));
    memcpy(buf.data(), this, sizeof(Vrf));
    return buf;
}
  • 测试结果如下:

  • 预期目标:80 06 40 00 00 00 00

  • serializeByMemcpy() 返回:01 64 00 00 00 00 00

  • 原因分析:

BYTE hitFlag : 1:位域在内存中的具体落位由编译器/ABI 决定,常见实现会从低 bit 开始放,导致内存里看到 0000 0001(0x01)。

WORD vrfId : 12:GCC 常见实现会先分配一个 16-bit 容器;由于主机序为小端序,两个字节在内存里表现为 64 00。这与协议要求的 MSB-first bit 流顺序不是一回事。

优化一:定义工具类代替手动序列化

每次都手写“位拼装”容易出错,也不便维护。一个更可复用的做法是抽象出 BitWriter:只关心“写多少 bit、按 MSB-first 写到哪里”。

  • 接口定义:writeBits(value, n),其中value表示要序列化的值,n表示这个值要占几位

  • 功能定义:先序列化的位,放在前面的字节,一个字节内,也是从高到低位的顺序来存储

  • 算法描述:自动根据已序列化的bit数量,计算待填充的字节,每个字节内部又从高到低存储比特位

class BitWriter {
public:
    BitWriter() : bitPos_(0) {}

    // 写入 value 的低 bitCount 位(bitCount 最大 64)。
    void writeBits(U64 value, BYTE bitCount) {
        if (bitCount == 0) {
            return;
        }
        if (bitCount > 64) {
            throw invalid_argument("bitCount must be <= 64");
        }

        value &= maskLowerBits(bitCount); //去掉高位脏数据
        //从高到低写入比特位
        for (int i = bitCount - 1; i >= 0; --i) {
            appendOneBit(static_cast<BYTE>((value >> i) & 1ULL));
        }
    }

    void writeZeroBits(DWORD bitCount) {
        for (DWORD i = 0; i < bitCount; ++i) {
            appendOneBit(0);
        }
    }

    const vector<BYTE>& bytes() const {
        return data_;
    }

    size_t bitSize() const {
        return bitPos_;
    }

private:
    static U64 maskLowerBits(BYTE bitCount) {
        if (bitCount == 64) {
            return ~0ULL;
        }
        return (1ULL << bitCount) - 1ULL;
    }

    void appendOneBit(BYTE bit) {
        //计算比特位所属的字节
        size_t byteIndex = bitPos_ / 8;
        BYTE bitInByte = static_cast<BYTE>(bitPos_ % 8);

        if (byteIndex >= data_.size()) {
            data_.push_back(0);
        }

        BYTE shift = static_cast<BYTE>(7 - bitInByte);
        data_[byteIndex] |= static_cast<BYTE>(bit << shift);
        ++bitPos_;
    }

private:
    vector<BYTE> data_;
    size_t bitPos_;
};

那么在序列化的时候就可以简单调用writeBits接口即可

std::vector<BYTE> serializeByBitWriter() const {
    BitWriter w;
    w.writeBits(static_cast<U64>(hitFlag), 1);
    w.writeBits(static_cast<U64>(pad1), 7);
    w.writeBits(static_cast<U64>(vrfId), 12);
    w.writeBits(static_cast<U64>(pad2), 4);
    w.writeBits(static_cast<U64>(rsv), 32);
    //或者w.writeZeroBits(32);
    return w.bytes();
}

结果与手动序列化结果一致

优化二:只维护一处结构体位域信息

  • 痛点:结构体里已经写过“字段 + 位宽”,序列化又写一遍;字段变更时容易漏改,维护成本高
  • 难点:C++ 缺少反射,运行期拿不到字段列表与位宽
  • 方案:使用 X Macro,把“字段列表”集中维护一处,然后派生出位域声明、序列化与反序列化代码
#define VRF_FIELD_LIST(X) \
    X(BYTE, hitFlag, 1) \
    X(BYTE, pad1, 7) \
    X(WORD, vrfId, 12) \
    X(WORD, pad2, 4) \
    X(DWORD, rsv, 32)

struct Vrf {
#define VRF_DECLARE_BITFIELD(type, name, bits) type name : bits;
    VRF_FIELD_LIST(VRF_DECLARE_BITFIELD)
#undef VRF_DECLARE_BITFIELD

    Vrf() = default;

    explicit Vrf(const std::vector<BYTE>& config) {
        hitFlag = config[0] >> 7;
        pad1 = 0;
        vrfId = (config[1] << 4) | (config[2] >> 4);
        pad2 = 0;
        rsv = 0;
    }

    explicit Vrf(WORD vrfId)
        : hitFlag(1), pad1(0), vrfId(vrfId), pad2(0), rsv(0) {};

    std::vector<BYTE> serialize() const {
        BitWriter w;
#define VRF_WRITE_FIELD(type, name, bits) w.writeBits(static_cast<U64>(name), bits);
        VRF_FIELD_LIST(VRF_WRITE_FIELD)
#undef VRF_WRITE_FIELD
        return w.bytes();
    }

    std::string toString() const {
        return "hitFlag:" + std::to_string(hitFlag) +
               ", vrfId:" + std::to_string(vrfId);
    }
} __attribute__((packed));

带位域结构体的反序列化

BitReader

延续序列化思路,实现一个 BitReader 按 MSB-first 从字节流读出 bit,并按位宽拼回字段值。

class BitReader {
public:
    explicit BitReader(const vector<BYTE>& data) : data_(data), bitPos_(0) {}

    U64 readBits(BYTE bitCount) {
        if (bitCount == 0) {
            return 0;
        }
        if (bitCount > 64) {
            throw invalid_argument("bitCount must be <= 64");
        }

        U64 value = 0;
        for (BYTE i = 0; i < bitCount; ++i) {
            value = (value << 1) | readOneBit();
        }
        return value;
    }

private:
    BYTE readOneBit() {
        size_t byteIndex = bitPos_ / 8;
        BYTE bitInByte = static_cast<BYTE>(bitPos_ % 8);
        ++bitPos_;

        if (byteIndex >= data_.size()) {
            return 0;
        }

        BYTE shift = static_cast<BYTE>(7 - bitInByte);
        return static_cast<BYTE>((data_[byteIndex] >> shift) & 1U);
    }

private:
    const vector<BYTE>& data_;
    size_t bitPos_;
};

宏驱动修改反序列化

explicit Vrf(const std::vector<BYTE>& config) {
    BitReader r(config);
#define VRF_READ_FIELD(type, name, bits) name = static_cast<type>(r.readBits(bits));
    VRF_FIELD_LIST(VRF_READ_FIELD)
#undef VRF_READ_FIELD
}

最终优化版本及测试代码

下面给出整合版代码与最小测试,目标是:序列化字节序列与“标准答案”一致,且反序列化能恢复关键字段。

#include <algorithm>
#include <iomanip>
#include <iostream>
#include <stdexcept>
#include <string>
#include <vector>

using namespace std;

// 自定义类型定义
using BYTE  = unsigned char;
using WORD  = unsigned short int;
using DWORD = unsigned int;
using U64   = unsigned long long;

// MSB-first 位流:先写入的位占据缓冲区中更早的字节、更高位(与常见网络报文一致)。
class BitWriter {
public:
    BitWriter() : bitPos_(0) {}

    // 写入 value 的低 bitCount 位(bitCount 最大 64)。
    void writeBits(U64 value, BYTE bitCount) {
        if (bitCount == 0) {
            return;
        }
        if (bitCount > 64) {
            throw invalid_argument("bitCount must be <= 64");
        }

        value &= maskLowerBits(bitCount); //去掉高位脏数据
        //从高到低写入比特位
        for (int i = bitCount - 1; i >= 0; --i) {
            appendOneBit(static_cast<BYTE>((value >> i) & 1ULL));
        }
    }

    void writeZeroBits(DWORD bitCount) {
        for (DWORD i = 0; i < bitCount; ++i) {
            appendOneBit(0);
        }
    }

    const vector<BYTE>& bytes() const {
        return data_;
    }

    size_t bitSize() const {
        return bitPos_;
    }

private:
    static U64 maskLowerBits(BYTE bitCount) {
        if (bitCount == 64) {
            return ~0ULL;
        }
        return (1ULL << bitCount) - 1ULL;
    }

    void appendOneBit(BYTE bit) {
        //计算比特位所属的字节
        size_t byteIndex = bitPos_ / 8;
        BYTE bitInByte = static_cast<BYTE>(bitPos_ % 8);

        if (byteIndex >= data_.size()) {
            data_.push_back(0);
        }

        BYTE shift = static_cast<BYTE>(7 - bitInByte);
        data_[byteIndex] |= static_cast<BYTE>(bit << shift);
        ++bitPos_;
    }

private:
    vector<BYTE> data_;
    size_t bitPos_;
};

class BitReader {
public:
    explicit BitReader(const vector<BYTE>& data) : data_(data), bitPos_(0) {}

    U64 readBits(BYTE bitCount) {
        if (bitCount == 0) {
            return 0;
        }
        if (bitCount > 64) {
            throw invalid_argument("bitCount must be <= 64");
        }

        U64 value = 0;
        for (BYTE i = 0; i < bitCount; ++i) {
            value = (value << 1) | readOneBit();
        }
        return value;
    }

private:
    BYTE readOneBit() {
        size_t byteIndex = bitPos_ / 8;
        BYTE bitInByte = static_cast<BYTE>(bitPos_ % 8);
        ++bitPos_;

        if (byteIndex >= data_.size()) {
            return 0;
        }

        BYTE shift = static_cast<BYTE>(7 - bitInByte);
        return static_cast<BYTE>((data_[byteIndex] >> shift) & 1U);
    }

private:
    const vector<BYTE>& data_;
    size_t bitPos_;
};

#define VRF_FIELD_LIST(X) \
    X(BYTE, hitFlag, 1) \
    X(BYTE, pad1, 7) \
    X(WORD, vrfId, 12) \
    X(WORD, pad2, 4) \
    X(DWORD, rsv, 32)

struct Vrf {
#define VRF_DECLARE_BITFIELD(type, name, bits) type name : bits;
    VRF_FIELD_LIST(VRF_DECLARE_BITFIELD)
#undef VRF_DECLARE_BITFIELD

    Vrf() = default;

    explicit Vrf(const std::vector<BYTE>& config) {
        BitReader r(config);
#define VRF_READ_FIELD(type, name, bits) name = static_cast<type>(r.readBits(bits));
        VRF_FIELD_LIST(VRF_READ_FIELD)
#undef VRF_READ_FIELD
    }

    explicit Vrf(WORD vrfId)
        : hitFlag(1), pad1(0), vrfId(vrfId), pad2(0), rsv(0) {};

    std::vector<BYTE> serialize() const {
        BitWriter w;
#define VRF_WRITE_FIELD(type, name, bits) w.writeBits(static_cast<U64>(name), bits);
        VRF_FIELD_LIST(VRF_WRITE_FIELD)
#undef VRF_WRITE_FIELD
        return w.bytes();
    }

    std::string toString() const {
        return "hitFlag:" + std::to_string(hitFlag) +
               ", vrfId:" + std::to_string(vrfId);
    }
} __attribute__((packed));

void dumpVector(const std::vector<BYTE>& data) {
    cout << hex << uppercase;

    for (size_t i = 0; i < data.size(); ++i) {
        cout << setw(2) << setfill('0') << static_cast<int>(data[i]) << " ";
    }
    cout << dec << endl;
}

int main() {
    Vrf vrf(100);
    cout << "Origin: " << vrf.toString() << endl;

    auto config = vrf.serialize();
    //cout << "Serialized: " << endl;
    //dumpVector(config);
    const vector<BYTE> expected = {0x80, 0x06, 0x40, 0x00, 0x00, 0x00, 0x00};
    const bool serializeOk = (config == expected);
    cout << "Serialize bytes test: " << (serializeOk ? "PASS" : "FAIL") << endl;
    cout << "  actual  : ";
    dumpVector(config);
    cout << "  expected: ";
    dumpVector(expected);

    Vrf decoded(config);
    cout << "Decoded: " << decoded.toString() << endl;

    const bool deserializeOk = (decoded.hitFlag == vrf.hitFlag) &&
                               (decoded.vrfId == vrf.vrfId);
    cout << "Deserialize test: " << (deserializeOk ? "PASS" : "FAIL") << endl;

    return 0;
}

可复用的工程经验

  1. 协议是协议,内存是内存:位域布局不受标准保证,memcpy 只能拿到内存表示,不能当作协议序列化
  2. 先手算一个标准答案:有了可对照的期望字节序列,后续任何实现(手写、工具类、宏驱动)都能用单测快速验证
  3. 把“bit 流写入/读取”抽成工具BitWriter/BitReader 只做一件事:维护 bit 游标,按 MSB-first 写/读
  4. 把字段列表当作单一事实源:用 X Macro 集中维护 type/name/bits,再派生声明、序列化、反序列化,减少重复与漏改
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值