Когда я использую Thrift для сериализации карты в C ++ на диск, а затем десериализации ее с помощью Python, я не получаю обратно тот же объект - PullRequest
0 голосов
/ 18 сентября 2018

Резюме: когда я использую Thrift для сериализации карты в C ++ на диск, а затем десериализации ее с помощью Python, я не возвращаю тот же объект.

Минимальный пример воспроизведения проблемы - Github repo https://github.com/brunorijsman/reproduce-thrift-crash

Клонируйте этот репозиторий в Ubuntu (протестирован 16.04) и следуйте инструкциям в верхней части файла размножайтесь .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 не понимает семантику структуры и, следовательно, не может угадать правильную реализацию оператора сравнения. Это обсуждается на <a href="http://mail-archives.apache.org/mod_mbox/thrift-user/201007.mbox/%3C4C4E08BD.8030407@facebook.com%3E" rel="nofollow noreferrer">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 ++. Но я не знаю, что это будет за упущенное.

Дополнительная информация добавлена ​​19 сентября 2018 г .:

Вот hexdump кодировки, созданной программой кодирования 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.

  • Следующие два байта интерпретируются как идентификатор поля. 00 00 означает идентификатор поля 0.

  • Для типа поля VOID больше ничего не читается, и мы переходим к следующему полю.

  • Следующий байт интерпретируется как тип поля. 00 означает STOP.

  • Мы читаем данные для структуры.

  • Окончательный результат - пустая структура.

Все вышеперечисленное согласуется с информацией в https://github.com/apache/thrift/blob/master/doc/specs/thrift-binary-protocol.md, которая описывает формат двоичного кодирования Thrift

На данный момент я пришел к выводу, что кодировщик C ++, по-видимому, производит «неправильную» двоичную кодировку (в кавычках я указывал неверное, потому что, безусловно, что-то столь вопиющее, как это было бы обнаружено многими другими людьми, поэтому я уверен, что я я все еще что-то упускаю).

Дополнительная информация добавлена ​​19 сентября 2018 года:

Похоже, что реализация TFileTransport на C ++ имеет концепцию "событий" при записи на диск.

Выходные данные, которые записываются на диск, делятся на последовательность «событий», где каждому «событию» предшествует 4-байтовое поле длины события, за которым следует содержимое события.

Глядя на hexdump выше, первая пара событий:

0100 0000 0d: длина события 1, значение события 0d

02 0000 0000 01: длина события 2, значение события 00 01

1103 * Etc. *

Реализация TFileTransport на Python не понимает эту концепцию событий при разборе файла.

Похоже, проблема в одном из следующих:

1) Либо код C ++ не должен вставлять эти длины событий в закодированный файл,

2) Или код Python должен понимать эти длины событий при декодировании файла.

Обратите внимание, что все эти длины событий делают кодированный файл C ++ намного больше, чем кодированный файл Python.

1 Ответ

0 голосов
/ 24 сентября 2018

К сожалению, C ++ TFileTransport не является полностью переносимым и не будет работать с TFileObjectTransport Python. Если вы переключитесь на 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>>

дает ту же утилиту, но устраняет целый класс возможных проблем (и не требует компараторов).

...