当我使用 Thrift 将 C++ 中的映射序列化到磁盘,然后使用 Python 反序列化它时,我没有取回相同的对象
When I use Thrift to serialize map in C++ to disk, and then de-serialize it using Python, I do not get back the same object
总结:当我使用 Thrift 将 C++ 中的映射序列化到磁盘,然后使用 Python 反序列化它时,我没有得到相同的对象。
重现问题的最小示例在 Github repo https://github.com/brunorijsman/reproduce-thrift-crash
在 Ubuntu 上克隆此 repo(在 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 得到预期结果(详见 repo)
我怀疑这与比较运算符有关。
我的直觉是我应该在 Python 中对比较运算符做类似的事情,就像我在 C++ 中所做的那样。但是我不知道会丢失什么东西。
2018 年 9 月 19 日添加的附加信息:
这是 C++ 编码程序生成的编码的 hexdump:
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++ 编码器似乎产生了 "incorrect" 二进制编码(我在引号中放错了,因为肯定有很多其他人会发现如此明显的东西,所以我我确定我仍然遗漏了一些东西)。
2018 年 9 月 19 日添加的附加信息:
似乎TFileTransport的C++实现在写入磁盘时有"events"的概念。
写入磁盘的输出分为一系列 "events",其中每个 "event" 前面是事件的 4 字节长度字段,后面是事件的内容.
查看上面的 hexdump,前几个事件是:
0100 0000 0d
:事件长度1,事件值0d
02 0000 0000 01
: 事件长度2,事件值00 01
等等
TFileTransport 的 Python 实现在解析文件时不理解事件的概念。
看来问题是以下两个问题之一:
1) C++ 代码不应该将这些事件长度插入到编码文件中,
2) 或者 Python 代码在解码文件时应该理解这些事件长度。
请注意,所有这些事件长度使 C++ 编码文件比 Python 编码文件大得多。
遗憾的是 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>>
提供相同的效用,但消除了整个 class 可能的问题(并且不需要比较器)。
总结:当我使用 Thrift 将 C++ 中的映射序列化到磁盘,然后使用 Python 反序列化它时,我没有得到相同的对象。
重现问题的最小示例在 Github repo https://github.com/brunorijsman/reproduce-thrift-crash
在 Ubuntu 上克隆此 repo(在 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 得到预期结果(详见 repo)
我怀疑这与比较运算符有关。
我的直觉是我应该在 Python 中对比较运算符做类似的事情,就像我在 C++ 中所做的那样。但是我不知道会丢失什么东西。
2018 年 9 月 19 日添加的附加信息:
这是 C++ 编码程序生成的编码的 hexdump:
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++ 编码器似乎产生了 "incorrect" 二进制编码(我在引号中放错了,因为肯定有很多其他人会发现如此明显的东西,所以我我确定我仍然遗漏了一些东西)。
2018 年 9 月 19 日添加的附加信息:
似乎TFileTransport的C++实现在写入磁盘时有"events"的概念。
写入磁盘的输出分为一系列 "events",其中每个 "event" 前面是事件的 4 字节长度字段,后面是事件的内容.
查看上面的 hexdump,前几个事件是:
0100 0000 0d
:事件长度1,事件值0d
02 0000 0000 01
: 事件长度2,事件值00 01
等等
TFileTransport 的 Python 实现在解析文件时不理解事件的概念。
看来问题是以下两个问题之一:
1) C++ 代码不应该将这些事件长度插入到编码文件中,
2) 或者 Python 代码在解码文件时应该理解这些事件长度。
请注意,所有这些事件长度使 C++ 编码文件比 Python 编码文件大得多。
遗憾的是 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>>
提供相同的效用,但消除了整个 class 可能的问题(并且不需要比较器)。