четверг, 31 марта 2022 г.

[prog.c++] Почти что актуальное про работу с C++ объектами в разделяемой памяти

В текущем проекте для передачи данных между процессами используется разделяемая память (она же shared-memory, реализованная посредством memory-mapped files).

В основном через эту память передаются большие объемы "сырых" данных. Так что особой надобности размещать в shared-memory каких-то С++ных объектов пока не было. За исключением простого заголовка, который предшествует этим самым "сырым" данным.

Однако, на горизонте начинает маячить сценарий, при котором через shared memory может потребоваться передавать небольшие и (пока?) несложные C++ные объекты с управляющей информацией.

И тут на глаза попадается свежая статья на Хабре: Динамические структуры в shared-памяти. Любопытный подход к проблеме там описан.

В код упомянутой в статье библиотеки особо не вникал, просто просмотрел бегло по диагонали. Качеством кода не впечатлился. Но задумался о чем-то подобном.

Однако, как я понимаю, в рамках C++17 нет легальных способов взять указатель на какую-то последовательность байт в разделяемой памяти и сказать, что это вот такой-то C++ный объект. Поскольку все это сродни вот такому трюку:

struct my_object { int x; }
alignas(my_object) char buffer[sizeof(my_object)];
auto * p = reinterpret_cast<my_object *>(buffer);
p->x = 1; // Undefined behavior!

И в C++17, и (насколько я помню) в C++20, подобный код не валиден, т.к. компилятор не видит конструирования объекта, на который должен указывать p. Поэтому обращение по p являются undefined behavior. Подробности можно прочитать вот в этом обсуждении на Reddit.

Подозреваю, что сейчас большинство компиляторов допускают использование такого трюка. Но UB в коде чревато.

Поиск привел вот на такой документ: P1631R1: Object detachment and attachment. Но складывается ощущение, что там еще и конь не валялся.

Upd. Есть еще вот такой документ: P0593. Однако, как я понял, именно для работы с объектами в разделяемой памяти очень желательно иметь упомянутую в этом документе std::start_lifetime_as, которой пока в C++ нет :(

Upd2. Интересный материал был найден здесь: https://blog.panicsoftware.com/objects-their-lifetimes-and-pointers/ (но у меня именно этот URL сейчас не открывается, поэтому читал сохраненную в web.archive копию.

В общем, если кто-то из читателей может накидать ссылок на документы/обсуждения, в которых говорится о том, как же правильно и легально можно работать с C++ными объектами, размещенными в разделяемой памяти, то буду признателен.

ПРИМЕЧАНИЕ. Написанное ниже курсивом "решение" в принципе не рабочее, можно не обращать на него внимание.

У меня в качестве промежуточной идеи в голове крутится мысль о том, чтобы использовать placement new для объектов. Допустим, у нас есть что-то вроде:

struct data {
  int x_;
  int y_;
  ...
};

Эту структуру мы снабжаем двумя конструкторами. Первый полностью инициализирует data и предназначен для использования в процессе, который создает объект в разделяемой памяти:

// Конструктор для процесса-продюсера.
data::data(int x, int y) : x_{x}, y_{y} {}
...
// Получаем место в разделяемой памяти под новый объект.
void * shmem_block = alloc_block_in_shared_memory(sizeof(data));
// Конструируем объект в разделяемой памяти.
auto * d = new(shmem_block) data{0, 1}; 
...

Второй конструктор предназначен для использования в процессе, который должен использовать "чужие" объекты в разделяемой памяти. Этот конструктор ничего не делает.

Вообще ничего, никакой инициализации. В предположении, что в качестве значений для полей объекта будет взято содержимое памяти, на которую "ляжет" новый экземпляр:

// Конструктор для процесса-консюмера.
data::data() {}
...
// Как-то получаем указатель на разделяемую память.
void * shmem_block = receive_shared_memory_ptr();
// Типа конструируем объект, который уже есть в разделяемой памяти.
auto * d = new(shmem_block) data();
// Теперь используем содержимое объекта.
auto x = d.x_ + k / d.y_;

Но, подозреваю, что здесь будут свои UB: как минимум, это должны быть обращения к полям объекта, которые не были проинициализированны в конструкторе. Плюс к тому, для указателя d, который мы получили через placement new, не будет вызываться delete. Т.е. мы не вызываем деструктор для сконструированного объекта, что не должно быть правильно с точки зрения стандарта.

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

В общем, пока что самый надежный способ -- это тупая сериализация/десериализация. Чем и пользуемся. Но было бы интересно получить и какое-то другое решение.

Комментариев нет: