当我使用Thrift将C ++中的地图序列化到磁盘上,然后使用Python将其反序列化时,我没有得到相同的对象

时间:2018-09-18 00:42:50

标签: python c++ dictionary thrift

摘要:当我使用Thrift将C ++中的映射序列化到磁盘,然后使用Python反序列化时,我没有得到相同的对象。

Github仓库https://github.com/brunorijsman/reproduce-thrift-crash

是重现此问题的最小示例

在Ubuntu(已在16.04上测试)上克隆此仓库,并按照文件reproduce.sh顶部的说明进行操作。

我有以下Thrift模型文件,如您所见,它包含一个由结构索引的映射:

struct Coordinate {
    1: required i32 x;
    2: required i32 y;
}

struct Terrain {
    1: required map<Coordinate, i32> altitude_samples;
}

我使用以下C ++代码在地图中创建具有3个坐标的对象(有关以下所有代码段的完整代码,请参见回购):

Terrain terrain;
add_sample_to_terrain(terrain, 10, 10, 100);
add_sample_to_terrain(terrain, 20, 20, 200);
add_sample_to_terrain(terrain, 30, 30, 300);

其中:

void add_sample_to_terrain(Terrain& terrain, int32_t x, int32_t y, int32_t altitude)
{
    Coordinate coordinate;
    coordinate.x = x;
    coordinate.y = y;
    std::pair<Coordinate, int32_t> sample(coordinate, altitude);
    terrain.altitude_samples.insert(sample);
}

我使用以下C ++代码将对象序列化到磁盘:

shared_ptr<TFileTransport> transport(new TFileTransport("terrain.dat"));
shared_ptr<TBinaryProtocol> protocol(new TBinaryProtocol(transport));
terrain.write(protocol.get());

重要说明:要正常工作,我必须实现功能Coordinate :: operator <。 Thrift会为Coordinate :: operator <生成声明,但不会生成Coordinate :: operator <的实现。原因是Thrift无法理解结构的语义,因此无法猜测比较运算符的正确实现。在http://mail-archives.apache.org/mod_mbox/thrift-user/201007.mbox/%3C4C4E08BD.8030407@facebook.com%3E

中对此进行了讨论
// Thrift generates the declaration but not the implementation of operator< because it has no way
// of knowning what the criteria for the comparison are. So, provide the implementation here.
bool Coordinate::operator<(const Coordinate& other) const
{
    if (x < other.x) {
        return true;
    } else if (x > other.x) {
        return false;
    } else if (y < other.y) {
        return true;
    } else {
        return false;
    }
}

然后,最后,我使用以下Python代码从磁盘反序列化同一对象:

file = open("terrain.dat", "rb")
transport = thrift.transport.TTransport.TFileObjectTransport(file)
protocol = thrift.protocol.TBinaryProtocol.TBinaryProtocol(transport)
terrain = Terrain()
terrain.read(protocol)
print(terrain)

此Python程序输出:

Terrain(altitude_samples=None)

换句话说,反序列化的Terrain不包含terrain_samples,而是包含3个坐标的预期字典。

我100%确保文件Terrain.dat包含有效数据:我还使用C ++反序列化了相同的数据,在这种情况下,我 do 得到了预期的结果(请参见回购详细信息)

我怀疑这与比较运算符有关。

我的直觉是,就比较运算符而言,我应该在Python中完成与C ++类似的事情。但是我不知道会缺少什么。

2018年9月19日添加的其他信息:

这是C ++编码程序产生的编码的十六进制转储:

  Offset: 00 01 02 03 04 05 06 07 08 09 0A 0B 0C 0D 0E 0F   
00000000: 01 00 00 00 0D 02 00 00 00 00 01 01 00 00 00 0C    ................
00000010: 01 00 00 00 08 04 00 00 00 00 00 00 03 01 00 00    ................
00000020: 00 08 02 00 00 00 00 01 04 00 00 00 00 00 00 0A    ................
00000030: 01 00 00 00 08 02 00 00 00 00 02 04 00 00 00 00    ................
00000040: 00 00 0A 01 00 00 00 00 04 00 00 00 00 00 00 64    ...............d
00000050: 01 00 00 00 08 02 00 00 00 00 01 04 00 00 00 00    ................
00000060: 00 00 14 01 00 00 00 08 02 00 00 00 00 02 04 00    ................
00000070: 00 00 00 00 00 14 01 00 00 00 00 04 00 00 00 00    ................
00000080: 00 00 C8 01 00 00 00 08 02 00 00 00 00 01 04 00    ..H.............
00000090: 00 00 00 00 00 1E 01 00 00 00 08 02 00 00 00 00    ................
000000a0: 02 04 00 00 00 00 00 00 1E 01 00 00 00 00 04 00    ................
000000b0: 00 00 00 00 01 2C 01 00 00 00 00                   .....,.....

前4个字节为01 00 00 00

使用调试器逐步执行Python解码功能后会发现:

  • 正在将其解码为结构(预期)

  • 第一个字节01被解释为字段类型。 01表示字段类型VOID。

  • 接下来的两个字节被解释为字段ID。 00 00表示字段ID 0。

  • 对于VOID字段类型,不会读取其他任何内容,我们将继续下一个字段。

  • 下一个字节被解释为字段类型。 00表示停止。

  • 我们首先读取该结构的数据。

  • 最终结果是一个空结构。

以上所有内容与https://github.com/apache/thrift/blob/master/doc/specs/thrift-binary-protocol.md中描述Thrift二进制编码格式的信息一致

到目前为止,我的结论是C ++编码器似乎产生了“不正确的”二进制编码(我在引号中使用了不正确的内容,因为肯定有很多人会发现的那种公然的东西,因此,我相信仍然缺少一些东西。

2018年9月19日添加的其他信息:

似乎TFileTransport的C ++实现在写入磁盘时具有“事件”的概念。

写入磁盘的输出分为一系列“事件”,其中每个“事件”前面都有一个4字节长的事件字段,然后是事件的内容。

看看上面的十六进制转储,前两个事件是:

0100 0000 0d:事件长度1,事件值0d

02 0000 0000 01:事件长度2,事件值00 01

等等。

TFileTransport的Python实现在解析文件时不理解事件的这种概念。

问题似乎是以下两个问题之一:

1)C ++代码都不应该将这些事件长度插入编码文件中,

2)否则,Python代码在解码文件时应该理解这些事件长度。

请注意,所有这些事件长度使C ++编码文件比Python编码文件大得多。

1 个答案:

答案 0 :(得分:0)

遗憾的是,C ++ TFileTransport并非完全可移植,因此无法与Python的TFileObjectTransport一起使用。如果切换到C ++ TSimpleFileTransport,它将与Python TFileObjectTransport和Java TSimpleFileTransport一起正常工作。

在这里看看示例:

https://github.com/RandyAbernethy/ThriftBook/tree/master/part2/types/complex

它们几乎可以完全完成您在Java和Python中的尝试,并且可以在此处找到有关C ++,Java和Python的示例(尽管它们添加了zip压缩层):

https://github.com/RandyAbernethy/ThriftBook/tree/master/part2/types/zip

但是,另一个警告是不要使用复杂的密钥类型。复杂的键类型需要(如您所发现的)比较器,但将无法使用某些语言。我可能会建议,例如:

map<x,map<y,alt>>

提供相同的实用程序,但消除了一整类可能的问题(并且不需要比较器)。