Современная библиотека двоичной сериализации и RPC C++20 с одним заголовочным файлом.
Эта библиотека является преемницей zpp::serializer. Библиотека старается быть проще в использовании, но имеет более-менее схожий API с предшественницей.
constexpr
zpp::bits
легче вводить, чем zpp::serializer
.zpp::serializer
с фиксированными 8 байтами идентификатора сериализации sha1. Для многих типов включение сериализации прозрачно и не требует дополнительных строк кода. Эти типы должны быть агрегатными и не содержать членов массива. Вот пример класса person
с именем и возрастом:
struct person
{
std::string name;
int age{};
};
Пример сериализации человека в вектор байтов и обратно:
// The `data_in_out` utility function creates a vector of bytes, the input and output archives
// and returns them so we can decompose them easily in one line using structured binding like so:
auto [data, in, out] = zpp::bits::data_in_out();
// Serialize a few people:
out (person{ " Person1 " , 25 }, person{ " Person2 " , 35 });
// Define our people.
person p1, p2;
// We can now deserialize them either one by one `in(p1)` `in(p2)`, or together, here
// we chose to do it together in one line:
in (p1, p2);
Этот пример почти работает, нас предупреждают, что мы отбрасываем возвращаемое значение. Для проверки ошибок продолжайте читать.
Нам нужно проверить наличие ошибок, библиотека предлагает несколько способов сделать это - на основе возвращаемого значения, на основе исключений или на основе zpp::throwing.
Способ, основанный на возвращаемом значении, для наиболее явного представления или если вы просто предпочитаете возвращаемые значения:
auto [data, in, out] = zpp::bits::data_in_out();
auto result = out(person{ " Person1 " , 25 }, person{ " Person2 " , 35 });
if (failure(result)) {
// `result` is implicitly convertible to `std::errc`.
// handle the error or return/throw exception.
}
person p1, p2;
result = in(p1, p2);
if (failure(result)) {
// `result` is implicitly convertible to `std::errc`.
// handle the error or return/throw exception.
}
Способ, основанный на исключениях, с использованием .or_throw()
(читайте это как «успех или бросок» - следовательно, or_throw()
):
int main ()
{
try {
auto [data, in, out] = zpp::bits::data_in_out ();
// Check error using `or_throw()` which throws an exception.
out (person{ " Person1 " , 25 }, person{ " Person2 " , 35 }). or_throw ();
person p1, p2;
// Check error using `or_throw()` which throws an exception.
in (p1, p2). or_throw ();
return 0 ;
} catch ( const std:: exception & error) {
std::cout << " Failed with error: " << error. what () << ' n ' ;
return 1 ;
} catch (...) {
std::cout << " Unknown error n " ;
return 1 ;
});
}
Другой вариант — zpp::throwing, где проверка ошибок превращается в две простые функции co_await
. Чтобы понять, как проверять наличие ошибок, мы предоставляем полную функцию main:
int main ()
{
return zpp::try_catch ([]() -> zpp::throwing< int > {
auto [data, in, out] = zpp::bits::data_in_out ();
// Check error using `co_await`, which suspends the coroutine.
co_await out (person{ " Person1 " , 25 }, person{ " Person2 " , 35 });
person p1, p2;
// Check error using `co_await`, which suspends the coroutine.
co_await in (p1, p2);
co_return 0 ;
}, [](zpp::error error) {
std::cout << " Failed with error: " << error. message () << ' n ' ;
return 1 ;
}, []( /* catch all */ ) {
std::cout << " Unknown error n " ;
return 1 ;
});
}
Все вышеперечисленные методы внутренне используют следующие коды ошибок, и их можно проверить с помощью оператора сравнения на основе возвращаемого значения или путем проверки внутреннего кода ошибки std::system_error
или zpp::throwing
в зависимости от того, какой из них вы использовали:
std::errc::result_out_of_range
— попытка записи или чтения из слишком короткого буфера.std::errc::no_buffer_space
— растущий буфер выйдет за пределы выделения или переполнится.std::errc::value_too_large
— кодировка varint (целое число переменной длины) выходит за пределы представления.std::errc::message_size
— размер сообщения превышает определенные пользователем пределы выделения.std::errc::not_supported
— попытка вызвать RPC, который не указан как поддерживаемый.std::errc::bad_message
— попытка прочитать вариант неизвестного типа.std::errc::invalid_argument
— попытка сериализовать нулевой указатель или его вариант без значения.std::errc::protocol_error
— попытка десериализации неверного сообщения протокола. Для большинства неагрегированных типов (или агрегатных типов с элементами массива) включение сериализации выполняется в одну строку. Вот пример неагрегированного класса person
:
struct person
{
// Add this line to your class with the number of members:
using serialize = zpp::bits::members< 2 >; // Two members
person ( auto && ...){ /* ... */ } // Make non-aggregate.
std::string name;
int age{};
};
В большинстве случаев типы, которые мы сериализуем, могут работать со структурированной привязкой, и эта библиотека использует это преимущество, но вам необходимо указать количество членов в вашем классе, чтобы это работало, используя метод, описанный выше.
Это также работает с поиском по аргументам, позволяя не изменять исходный класс:
namespace my_namespace
{
struct person
{
person ( auto && ...){ /* ... */ } // Make non-aggregate.
std::string name;
int age{};
};
// Add this line somewhere before the actual serialization happens.
auto serialize ( const person & person) -> zpp::bits::members<2>;
} // namespace my_namespace
В некоторых компиляторах SFINAE работает с requires expression
в if constexpr
и unevaluated lambda expression
. Это означает, что даже при использовании неагрегированных типов количество членов может определяться автоматически в тех случаях, когда все члены находятся в одной структуре. Чтобы подписаться, определите ZPP_BITS_AUTODETECT_MEMBERS_MODE=1
.
// Members are detected automatically, no additional change needed.
struct person
{
person ( auto && ...){ /* ... */ } // Make non-aggregate.
std::string name;
int age{};
};
Это работает с clang 13
, однако переносимость этого не ясна, поскольку в gcc
это не работает (это серьезная ошибка), и в стандарте явно указано, что существует намерение не разрешать SFINAE в подобных случаях, поэтому по умолчанию отключен.
Если ваши элементы данных или конструктор по умолчанию являются частными, вам нужно подружиться с zpp::bits::access
следующим образом:
struct private_person
{
// Add this line to your class.
friend zpp::bits::access;
using serialize = zpp::bits::members< 2 >;
private:
std::string name;
int age{};
};
Чтобы включить сохранение и загрузку любого объекта с использованием явной сериализации, которая работает независимо от совместимости структурированной привязки, добавьте в свой класс следующие строки:
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. object_1 , self. object_2 , ...);
}
Обратите внимание, что object_1, object_2, ...
являются нестатическими членами данных вашего класса.
Вот еще раз пример класса person с явной функцией сериализации:
struct person
{
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. name , self. age );
}
std::string name;
int age{};
};
Или с поиском, зависящим от аргумента:
namespace my_namespace
{
struct person
{
std::string name;
int age{};
};
constexpr auto serialize ( auto & archive, person & person)
{
return archive (person. name , person. age );
}
constexpr auto serialize ( auto & archive, const person & person)
{
return archive (person. name , person. age );
}
} // namespace my_namespace
Создание входных и выходных архивов вместе и отдельно из данных:
// Create both a vector of bytes, input and output archives.
auto [data, in, out] = zpp::bits::data_in_out();
// Create just the input and output archives, and bind them to the
// existing vector of bytes.
std::vector<std::byte> data;
auto [in, out] = zpp::bits::in_out(data);
// Create all of them separately
std::vector<std::byte> data;
zpp::bits::in in (data);
zpp::bits::out out (data);
// When you need just data and in/out
auto [data, in] = zpp::bits::data_in();
auto [data, out] = zpp::bits::data_out();
Архивы могут быть созданы из любого из типов байтов:
// Either one of these work with the below.
std::vector<std::byte> data;
std::vector< char > data;
std::vector< unsigned char > data;
std::string data;
// Automatically works with either `std::byte`, `char`, `unsigned char`.
zpp::bits::in in (data);
zpp::bits::out out (data);
Вы также можете использовать объекты данных фиксированного размера, такие как массив, std::array
и типы представлений, такие как std::span
аналогично приведенному выше. Вам просто нужно убедиться, что размера достаточно, поскольку их размер нельзя изменить:
// Either one of these work with the below.
std::byte data[ 0x1000 ];
char data[ 0x1000 ];
unsigned char data[ 0x1000 ];
std::array<std::byte, 0x1000 > data;
std::array< char , 0x1000 > data;
std::array< unsigned char , 0x1000 > data;
std::span<std::byte> data = /* ... */ ;
std::span< char > data = /* ... */ ;
std::span< unsigned char > data = /* ... */ ;
// Automatically works with either `std::byte`, `char`, `unsigned char`.
zpp::bits::in in (data);
zpp::bits::out out (data);
При использовании вектора или строки он автоматически увеличивается до нужного размера, однако при этом данные ограничиваются границами массивов или диапазонов.
При создании архива любым из описанных выше способов можно передать переменное количество параметров, которые управляют поведением архива, например, порядок байтов, типы размеров по умолчанию, указание поведения добавления и т. д. Это обсуждается в остальной части README.
Как было сказано выше, библиотека почти полностью является constexpr, вот пример использования массива в качестве объекта данных, а также его использования во время компиляции для сериализации и десериализации кортежа целых чисел:
constexpr auto tuple_integers ()
{
std::array<std::byte, 0x1000 > data{};
auto [in, out] = zpp::bits::in_out (data);
out (std::tuple{ 1 , 2 , 3 , 4 , 5 }). or_throw ();
std::tuple t{ 0 , 0 , 0 , 0 , 0 };
in (t). or_throw ();
return t;
}
// Compile time check.
static_assert (tuple_integers() == std::tuple{ 1 , 2 , 3 , 4 , 5 });
Для удобства библиотека также предоставляет некоторые упрощенные функции сериализации во время компиляции:
using namespace zpp ::bits::literals ;
// Returns an array
// where the first bytes are those of the hello world string and then
// the 1337 as 4 byte integer.
constexpr std::array data =
zpp::bits::to_bytes< " Hello World! " _s, 1337 >();
static_assert (
zpp::bits::from_bytes<data,
zpp::bits::string_literal< char , 12 >,
int >() == std::tuple{ " Hello World! " _s, 1337 });
Запросите позицию in
и out
с помощью position()
, другими словами, читаемые и записываемые байты соответственно:
std:: size_t bytes_read = in.position();
std:: size_t bytes_written = out.position();
Сбросьте положение назад или вперед или в начало, используйте с особой осторожностью:
in.reset(); // reset to beginning.
in.reset(position); // reset to position.
in.position() -= sizeof ( int ); // Go back an integer.
in.position() += sizeof ( int ); // Go forward an integer.
out.reset(); // reset to beginning.
out.reset(position); // reset to position.
out.position() -= sizeof ( int ); // Go back an integer.
out.position() += sizeof ( int ); // Go forward an integer.
При сериализации типов стандартной библиотеки переменной длины, таких как векторы, строки и типы представлений, такие как представление диапазона и строки, библиотека сначала сохраняет 4-байтовое целое число, представляющее размер, а затем элементы.
std::vector v = { 1 , 2 , 3 , 4 };
out (v);
in (v);
Причина, по которой тип размера по умолчанию составляет 4 байта (т.е. std::uint32_t
), заключается в переносимости между различными архитектурами, а также большинство программ почти никогда не достигают случая, когда контейнер содержит более 2 ^ 32 элементов, и это может быть несправедливо платить цену за размер 8 байт по умолчанию.
Для определенных типов размеров, отличных от 4 байтов, используйте zpp::bits::sized
/ zpp::bits::sized_t
следующим образом:
// Using `sized` function:
std::vector< int > v = { 1 , 2 , 3 , 4 };
out (zpp::bits::sized<std:: uint16_t >(v));
in (zpp::bits::sized<std:: uint16_t >(v));
// Using `sized_t` type:
zpp::bits:: sized_t <std::vector< int >, std:: uint16_t > v = { 1 , 2 , 3 , 4 };
out (v);
in (v);
Убедитесь, что тип размера достаточно велик для сериализуемого объекта, иначе будет сериализовано меньше элементов в соответствии с правилами преобразования беззнаковых типов.
Вы также можете вообще не сериализовать размер, например:
// Using `unsized` function:
std::vector< int > v = { 1 , 2 , 3 , 4 };
out (zpp::bits::unsized(v));
in (zpp::bits::unsized(v));
// Using `unsized_t` type:
zpp::bits:: unsized_t <std::vector< int >> v = { 1 , 2 , 3 , 4 };
out (v);
in (v);
Там, где это распространено, существуют объявления псевдонимов для размерных/неразмерных версий типов, например, здесь vector
и span
, другие, такие как string
, string_view
и т. д., используют тот же шаблон.
zpp::bits::vector1b<T>; // vector with 1 byte size.
zpp::bits::vector2b<T>; // vector with 2 byte size.
zpp::bits::vector4b<T>; // vector with 4 byte size == default std::vector configuration
zpp::bits::vector8b<T>; // vector with 8 byte size.
zpp::bits::static_vector<T>; // unsized vector
zpp::bits::native_vector<T>; // vector with native (size_type) byte size.
zpp::bits::span1b<T>; // span with 1 byte size.
zpp::bits::span2b<T>; // span with 2 byte size.
zpp::bits::span4b<T>; // span with 4 byte size == default std::span configuration
zpp::bits::span8b<T>; // span with 8 byte size.
zpp::bits::static_span<T>; // unsized span
zpp::bits::native_span<T>; // span with native (size_type) byte size.
Сериализация типов фиксированного размера, таких как массивы, std::array
s, std::tuple
s, не включает в себя никаких накладных расходов, за исключением элементов, следующих друг за другом.
Изменение типа размера по умолчанию для всего архива возможно во время создания:
zpp::bits::in in (data, zpp::bits::size1b{}); // Use 1 byte for size.
zpp::bits::out out (data, zpp::bits::size1b{}); // Use 1 byte for size.
zpp::bits::in in (data, zpp::bits::size2b{}); // Use 2 bytes for size.
zpp::bits::out out (data, zpp::bits::size2b{}); // Use 2 bytes for size.
zpp::bits::in in (data, zpp::bits::size4b{}); // Use 4 bytes for size.
zpp::bits::out out (data, zpp::bits::size4b{}); // Use 4 bytes for size.
zpp::bits::in in (data, zpp::bits::size8b{}); // Use 8 bytes for size.
zpp::bits::out out (data, zpp::bits::size8b{}); // Use 8 bytes for size.
zpp::bits::in in (data, zpp::bits::size_native{}); // Use std::size_t for size.
zpp::bits::out out (data, zpp::bits::size_native{}); // Use std::size_t for size.
zpp::bits::in in (data, zpp::bits::no_size{}); // Don't use size, for very special cases, since it is very limiting.
zpp::bits::out out (data, zpp::bits::no_size{}); // Don't use size, for very special cases, since it is very limiting.
// Can also do it together, for example for 2 bytes size:
auto [data, in, out] = data_in_out(zpp::bits::size2b{});
auto [data, out] = data_out(zpp::bits::size2b{});
auto [data, in] = data_in(zpp::bits::size2b{});
Большинство типов библиотека умеет оптимизировать и сериализовать объекты как байты. Однако он отключен при использовании явных функций сериализации.
Если вы знаете, что ваш тип сериализуем так же, как необработанные байты, и вы используете явную сериализацию, вы можете выбрать и оптимизировать его сериализацию до простого memcpy
:
struct point
{
int x;
int y;
constexpr static auto serialize ( auto & archive, auto & self)
{
// Serialize as bytes, instead of serializing each
// member separately. The overall result is the same, but this may be
// faster sometimes.
return archive ( zpp::bits::as_bytes (self));
}
};
Это также возможно сделать непосредственно из вектора или диапазона тривиально копируемых типов, на этот раз мы используем bytes
вместо as_bytes
, потому что мы преобразуем содержимое вектора в байты, а не сам векторный объект (данные, на которые указывает вектор, а не векторный объект):
std::vector<point> points;
out (zpp::bits::bytes(points));
in (zpp::bits::bytes(points));
Однако в этом случае размер не сериализуется, в будущем это может быть расширено, чтобы поддерживать сериализацию размера, аналогично другим типам представлений. Если вам нужно сериализовать в байтах и вам нужен размер, в качестве обходного пути можно выполнить приведение к std::span<std::byte>
.
Хотя не существует идеального инструмента для обеспечения обратной совместимости структур из-за отсутствия накладных расходов на сериализацию, вы можете использовать std::variant
как способ создания версий ваших классов или создания хорошей диспетчеризации на основе полиморфизма, вот как:
namespace v1
{
struct person
{
using serialize = zpp::bits::members< 2 >;
auto get_hobby () const
{
return " <none> " sv;
}
std::string name;
int age;
};
} // namespace v1
namespace v2
{
struct person
{
using serialize = zpp::bits::members< 3 >;
auto get_hobby () const
{
return std::string_view (hobby);
}
std::string name;
int age;
std::string hobby;
};
} // namespace v2
И затем к самой сериализации:
auto [data, in, out] = zpp::bits::data_in_out();
out (std::variant<v1::person, v2::person>(v1::person{ " Person1 " , 25 }))
.or_throw();
std::variant<v1::person, v2::person> v;
in (v).or_throw();
std::visit ([]( auto && person) {
( void ) person. name == " Person1 " ;
( void ) person. age == 25 ;
( void ) person. get_hobby () == " <none> " ;
}, v);
out (std::variant<v1::person, v2::person>(
v2::person{ " Person2 " , 35 , " Basketball " }))
.or_throw();
in (v).or_throw();
std::visit ([]( auto && person) {
( void ) person. name == " Person2 " ;
( void ) person. age == 35 ;
( void ) person. get_hobby () == " Basketball " ;
}, v);
Вариант сериализуется путем сериализации его индекса (0 или 1) как std::byte
перед сериализацией фактического объекта. Это очень эффективно, однако иногда пользователи могут захотеть выбрать для этого явный идентификатор сериализации, см. пункт ниже.
Чтобы установить собственный идентификатор сериализации, вам необходимо добавить дополнительную строку внутри/вне вашего класса соответственно:
using namespace zpp ::bits::literals ;
// Inside the class, this serializes the full string "v1::person" before you serialize
// the person.
using serialize_id = zpp::bits::id< " v1::person " _s>;
// Outside the class, this serializes the full string "v1::person" before you serialize
// the person.
auto serialize_id ( const person &) -> zpp::bits::id<"v1::person"_s>;
Обратите внимание, что идентификаторы сериализации типов в варианте должны совпадать по длине, иначе возникнет ошибка компиляции.
Вы также можете использовать любую последовательность байтов вместо читаемой строки, а также целое число или любой литеральный тип. Вот пример того, как использовать хеш строки в качестве идентификатора сериализации:
using namespace zpp ::bits::literals ;
// Inside:
using serialize_id = zpp::bits::id< " v1::person " _sha1>; // Sha1
using serialize_id = zpp::bits::id< " v1::person " _sha256>; // Sha256
// Outside:
auto serialize_id ( const person &) -> zpp::bits::id<"v1::person"_sha1>; // Sha1
auto serialize_id ( const person &) -> zpp::bits::id<"v1::person"_sha256>; // Sha256
Вы также можете сериализовать только первые байты хеша, например:
// First 4 bytes of hash:
using serialize_id = zpp::bits::id< " v1::person " _sha256, 4 >;
// First sizeof(int) bytes of hash:
using serialize_id = zpp::bits::id< " v1::person " _sha256_int>;
Затем тип преобразуется в байты во время компиляции с использованием (... подождите) zpp::bits::out
во время компиляции, поэтому, пока ваш литеральный тип сериализуем в соответствии с вышеизложенным, вы можете использовать его как идентификатор сериализации. Идентификатор сериализуется в std::array<std::byte, N>
однако для 1, 2, 4 и 8 байтов его базовым типом является std::byte
std::uint16_t
, std::uin32_t
и std::uint64_t
соответственно, для простоты использования и эффективности.
Если вы хотите сериализовать вариант без идентификатора или знаете, что вариант будет иметь определенный идентификатор при десериализации, вы можете сделать это, используя zpp::bits::known_id
для переноса вашего варианта:
std::variant<v1::person, v2::person> v;
// Id assumed to be v2::person, and is not serialized / deserialized.
out (zpp::bits::known_id< " v2::person " _sha256_int>(v));
in (zpp::bits::known_id< " v2::person " _sha256_int>(v));
// When deserializing you can pass the id as function parameter, to be able
// to use outside of compile time context. `id_v` stands for "id value".
// In our case 4 bytes translates to a plain std::uint32_t, so any dynamic
// integer could fit as the first parameter to `known_id` below.
in (zpp::bits::known_id(zpp::bits::id_v< " v2::person " _sha256_int>, v));
Описание вспомогательных литералов в библиотеке:
using namespace zpp ::bits::literals ;
" hello " _s // Make a string literal.
" hello " _b // Make a binary data literal.
" hello " _sha1 // Make a sha1 binary data literal.
" hello " _sha256 // Make a sha256 binary data literal.
" hello " _sha1_int // Make a sha1 integer from the first hash bytes.
" hello " _sha256_int // Make a sha256 integer from the first hash bytes.
" 01020304 " _decode_hex // Decode a hex string into bytes literal.
zpp::bits::apply
, функция не должна быть шаблонной и иметь ровно одну перегрузку: int foo (std::string s, int i)
{
// s == "hello"s;
// i == 1337;
return 1338 ;
}
auto [data, in, out] = zpp::bits::data_in_out();
out ( " hello " s, 1337 ).or_throw();
// Call the foo in one of the following ways:
// Exception based:
zpp::bits::apply (foo, in).or_throw() == 1338;
// zpp::throwing based:
co_await zpp::bits::apply (foo, in) == 1338;
// Return value based:
if ( auto result = zpp::bits::apply(foo, in);
failure (result)) {
// Failure...
} else {
result. value () == 1338 ;
}
Когда ваша функция не получает параметров, эффектом является просто вызов функции без десериализации, а возвращаемое значение — это возвращаемое значение вашей функции. Когда функция возвращает void, для результирующего типа нет значения.
Библиотека также предоставляет тонкий интерфейс RPC (удаленный вызов процедур), позволяющий сериализовать и десериализовать вызовы функций:
using namespace std ::literals ;
using namespace zpp ::bits::literals ;
int foo ( int i, std::string s);
std::string bar ( int i, int j);
using rpc = zpp::bits::rpc<
zpp::bits::bind<foo, " foo " _sha256_int>,
zpp::bits::bind<bar, " bar " _sha256_int>
>;
auto [data, in, out] = zpp::bits::data_in_out();
// Server and client together:
auto [client, server] = rpc::client_server(in, out);
// Or separately:
rpc::client client{in, out};
rpc::server server{in, out};
// Request from the client:
client.request< " foo " _sha256_int>( 1337 , " hello " s).or_throw();
// Serve the request from the server:
server.serve().or_throw();
// Read back the response
client.response< " foo " _sha256_int>().or_throw(); // == foo(1337, "hello"s);
Что касается обработки ошибок, как и во многих приведенных выше примерах, вы можете использовать возвращаемое значение, исключения или zpp::throwing
для обработки ошибок.
// Return value based.
if ( auto result = client.request< " foo " _sha256_int>( 1337 , " hello " s); failure(result)) {
// Handle the failure.
}
if ( auto result = server.serve(); failure(result)) {
// Handle the failure.
}
if ( auto result = client.response< " foo " _sha256_int>(); failure(result)) {
// Handle the failure.
} else {
// Use response.value();
}
// Throwing based.
co_await client.request< " foo " _sha256_int>( 1337 , " hello " s); failure(result));
co_await server.serve();
co_await client.response< " foo " _sha256_int>(); // == foo(1337, "hello"s);
Идентификаторы вызовов RPC могут быть пропущены, например, если они передаются вне диапазона. Вот как этого добиться:
server.serve(id); // id is already known, don't deserialize it.
client.request_body<Id>(arguments...); // request without serializing id.
Функции-члены также могут быть зарегистрированы для RPC, однако серверу необходимо получить ссылку на объект класса во время создания, и все функции-члены должны принадлежать к одному и тому же классу (хотя функции области имен можно смешивать):
struct a
{
int foo ( int i, std::string s);
};
std::string bar ( int i, int j);
using rpc = zpp::bits::rpc<
zpp::bits::bind<&a::foo, " a::foo " _sha256_int>,
zpp::bits::bind<bar, " bar " _sha256_int>
>;
auto [data, in, out] = zpp::bits::data_in_out();
// Our object.
a a1;
// Server and client together:
auto [client, server] = rpc::client_server(in, out, a1);
// Or separately:
rpc::client client{in, out};
rpc::server server{in, out, a1};
// Request from the client:
client.request< " a::foo " _sha256_int>( 1337 , " hello " s).or_throw();
// Serve the request from the server:
server.serve().or_throw();
// Read back the response
client.response< " a::foo " _sha256_int>().or_throw(); // == a1.foo(1337, "hello"s);
RPC также может работать в непрозрачном режиме и позволять самой функции сериализовать/десериализовать данные при привязке функции как непрозрачной, используя bind_opaque
:
// Each of the following signatures of `foo()` are valid for opaque rpc call:
auto foo (zpp::bits::in<> &, zpp::bits::out<> &);
auto foo (zpp::bits::in<> &);
auto foo (zpp::bits::out<> &);
auto foo (std::span<std::byte> input); // assumes all data is consumed from archive.
auto foo (std::span<std::byte> & input); // resize input in the function to signal how much was consumed.
using rpc = zpp::bits::rpc<
zpp::bits::bind_opaque<foo, " a::foo " _sha256_int>,
zpp::bits::bind<bar, " bar " _sha256_int>
>;
По умолчанию используется порядок байтов, выбранный собственным процессором/ОС. Вы можете выбрать другой порядок байтов, используя zpp::bits::endian
во время построения, например:
zpp::bits::in in (data, zpp::bits::endian::big{}); // Use big endian
zpp::bits::out out (data, zpp::bits::endian::big{}); // Use big endian
zpp::bits::in in (data, zpp::bits::endian::network{}); // Use big endian (provided for convenience)
zpp::bits::out out (data, zpp::bits::endian::network{}); // Use big endian (provided for convenience)
zpp::bits::in in (data, zpp::bits::endian::little{}); // Use little endian
zpp::bits::out out (data, zpp::bits::endian::little{}); // Use little endian
zpp::bits::in in (data, zpp::bits::endian::swapped{}); // If little use big otherwise little.
zpp::bits::out out (data, zpp::bits::endian::swapped{}); // If little use big otherwise little.
zpp::bits::in in (data, zpp::bits::endian::native{}); // Use the native one (default).
zpp::bits::out out (data, zpp::bits::endian::native{}); // Use the native one (default).
// Can also do it together, for example big endian:
auto [data, in, out] = data_in_out(zpp::bits::endian::big{});
auto [data, out] = data_out(zpp::bits::endian::big{});
auto [data, in] = data_in(zpp::bits::endian::big{});
На принимающей стороне (входной архив) библиотека поддерживает типы представлений константных байтовых типов, например std::span<const std::byte>
чтобы получить представление части данных без копирования. Это необходимо использовать осторожно, поскольку признание недействительными итераторов содержащихся данных может привести к использованию после освобождения. Это сделано для того, чтобы при необходимости можно было оптимизировать:
using namespace std ::literals ;
auto [data, in, out] = zpp::bits::data_in_out();
out ( " hello " sv).or_throw();
std::span< const std::byte> s;
in (s).or_throw();
// s.size() == "hello"sv.size()
// std::memcmp("hello"sv.data(), s.data(), "hello"sv.size()) == 0
}
Существует также неразмерная версия, которая потребляет остальную часть архивных данных, чтобы обеспечить общий вариант использования заголовка, а затем произвольного объема данных:
auto [data, in, out] = zpp::bits::data_in_out();
out (zpp::bits::unsized( " hello " sv)).or_throw();
std::span< const std::byte> s;
in (zpp::bits::unsized(s)).or_throw();
// s.size() == "hello"sv.size()
// std::memcmp("hello"sv.data(), s.data(), "hello"sv.size()) == 0
Библиотека не поддерживает сериализацию значений нулевых указателей, однако явно поддерживает необязательные указатели владения, например, для создания графиков и сложных структур.
Теоретически допустимо использовать std::optional<std::unique_ptr<T>>
, но рекомендуется использовать специально созданный zpp::bits::optional_ptr<T>
, который оптимизирует логическое значение, которое обычно сохраняет необязательный объект, и использует нулевой указатель как недопустимое состояние.
Сериализация значения нулевого указателя в этом случае приведет к сериализации нулевого байта, тогда как ненулевые значения сериализуются как один байт, за которым следуют байты объекта. (т. е. сериализация идентична std::optional<T>
).
В рамках реализации библиотеки требовалось реализовать некоторые типы отражения для подсчета участников и посещения участников, и библиотека предоставляет их пользователю:
struct point
{
int x;
int y;
};
# if !ZPP_BITS_AUTODETECT_MEMBERS_MODE
auto serialize (point) -> zpp::bits::members<2>;
# endif
static_assert (zpp::bits::number_of_members<point>() == 2);
constexpr auto sum = zpp::bits::visit_members(
point{ 1 , 2 }, []( auto x, auto y) { return x + y; });
static_assert (sum == 3 );
constexpr auto generic_sum = zpp::bits::visit_members(
point{ 1 , 2 }, []( auto ... members) { return ( 0 + ... + members); });
static_assert (generic_sum == 3 );
constexpr auto is_two_integers =
zpp::bits::visit_members_types<point>([]< typename ... Types>() {
if constexpr (std::same_as<std::tuple<Types...>,
std::tuple< int , int >>) {
return std::true_type{};
} else {
return std::false_type{};
}
})();
static_assert (is_two_integers);
Приведенный выше пример работает с ZPP_BITS_AUTODETECT_MEMBERS_MODE=1
или без него, в зависимости от #if
. Как отмечалось выше, мы должны полагаться на конкретную функцию компилятора, чтобы определить количество членов, которые могут быть непереносимыми.
Архивы могут быть созданы с дополнительными параметрами управления, такими как zpp::bits::append{}
, который предписывает выходным архивам устанавливать позицию конца вектора или другого источника данных. (для входных архивов эта опция не действует)
std::vector<std::byte> data;
zpp::bits::out out (data, zpp::bits::append{});
Можно использовать несколько элементов управления, а также использовать их с data_in_out/data_in/data_out/in_out
:
zpp::bits::out out (data, zpp::bits::append{}, zpp::bits::endian::big{});
auto [in, out] = in_out(data, zpp::bits::append{}, zpp::bits::endian::big{});
auto [data, in, out] = data_in_out(zpp::bits::size2b{}, zpp::bits::endian::big{});
Размер выделения может быть ограничен в случае вывода архива растущим буфером или при использовании входного архива, чтобы ограничить длину сообщения с префиксом одной длины, чтобы избежать предварительного выделения очень большого буфера, используя zpp::bits::alloc_limit<L>{}
. Предполагаемое использование предназначено для соображений безопасности и здравомыслия, а не для точного измерения распределения:
zpp::bits::out out (data, zpp::bits::alloc_limit< 0x10000 >{});
zpp::bits::in in (data, zpp::bits::alloc_limit< 0x10000 >{});
auto [in, out] = in_out(data, zpp::bits::alloc_limit< 0x10000 >{});
auto [data, in, out] = data_in_out(zpp::bits::alloc_limit< 0x10000 >{});
Для большей корректности при использовании растущего буфера для вывода, если буфер был увеличен, размер буфера в конце изменяется в соответствии с точным положением выходного архива, это влечет за собой дополнительное изменение размера, что в большинстве случаев приемлемо, но вы можете этого избежать. дополнительное изменение размера и распознавание конца буфера с помощью position()
. Вы можете добиться этого, используя zpp::bits::no_fit_size{}
:
zpp::bits::out out (data, zpp::bits::no_fit_size{});
Чтобы контролировать увеличение вектора выходного архива, вы можете использовать zpp::bits::enlarger<Mul, Div = 1>
:
zpp::bits::out out (data, zpp::bits::enlarger< 2 >{}); // Grow by multiplying size by 2.
zpp::bits::out out (data, zpp::bits::enlarger< 3 , 2 >{}); // Default - Grow by multiplying size by 3 and divide by 2 (enlarge by 1.5).
zpp::bits::out out (data, zpp::bits::exact_enlarger{}); // Grow to exact size every time.
По умолчанию, в целях безопасности, выходной архив, использующий растущий буфер, проверяет наличие переполнения перед увеличением любого буфера. Для 64-битных систем эта проверка, хотя и дешевая, почти избыточна, поскольку практически невозможно переполнить 64-битное целое число, когда оно представляет размер памяти. (т. е. выделение памяти завершится неудачей до того, как память приблизится к переполнению этого целого числа). Если вы хотите отключить эти проверки переполнения в пользу производительности, используйте: zpp::bits::no_enlarge_overflow{}
:
zpp::bits::out out (data, zpp::bits::no_enlarge_overflow{}); // Disable overflow check when enlarging.
При явной сериализации часто требуется определить, является ли архив входным или выходным архивом, и это делается с помощью статической функции-члена archive.kind()
и может быть выполнено в if constexpr
:
static constexpr auto serialize ( auto & archive, auto & self)
{
using archive_type = std:: remove_cvref_t < decltype (archive)>;
if constexpr ( archive_type::kind () == zpp::bits::kind::in) {
// Input archive
} else if constexpr ( archive_type::kind () == zpp::bits::kind::out) {
// Output archive
} else {
// No such archive (no need to check for this)
}
}
Библиотека предоставляет тип для сериализации и десериализации целых чисел переменной длины:
auto [data, in, out] = zpp::bits::data_in_out();
out (zpp::bits::varint{ 150 }).or_throw();
zpp::bits::varint i{ 0 };
in (i).or_throw();
// i == 150;
Вот пример кодировки во время компиляции:
static_assert (zpp::bits::to_bytes<zpp::bits::varint{ 150 }>() == "9601"_decode_hex);
Шаблон класса zpp::bits::varint<T, E = varint_encoding::normal>
предоставляется для определения любого целочисленного типа varint или типа перечисления, а также возможных кодировок zpp::bits::varint_encoding::normal/zig_zag
(по умолчанию — нормальный).
Предоставляются следующие объявления псевдонимов:
using vint32_t = varint<std:: int32_t >; // varint of int32 types.
using vint64_t = varint<std:: int64_t >; // varint of int64 types.
using vuint32_t = varint<std:: uint32_t >; // varint of unsigned int32 types.
using vuint64_t = varint<std:: uint64_t >; // varint of unsigned int64 types.
using vsint32_t = varint<std:: int32_t , varint_encoding::zig_zag>; // zig zag encoded varint of int32 types.
using vsint64_t = varint<std:: int64_t , varint_encoding::zig_zag>; // zig zag encoded varint of int64 types.
using vsize_t = varint<std:: size_t >; // varint of std::size_t types.
Использование varints для сериализации размеров по умолчанию также возможно во время создания архива:
auto [data, in, out] = data_in_out(zpp::bits::size_varint{});
zpp::bits::in in (data, zpp::bits::size_varint{}); // Uses varint to encode size.
zpp::bits::out out (data, zpp::bits::size_varint{}); // Uses varint to encode size.
Формат сериализации этой библиотеки не основан ни на каком известном или принятом формате. Естественно, другие языки не поддерживают этот формат, что делает практически невозможным использование библиотеки для межязыкового взаимодействия.
По этой причине библиотека поддерживает формат protobuf, доступный на многих языках.
Обратите внимание, что поддержка protobuf является своего рода экспериментальной, что означает, что она может не включать в себя все возможные функции protobuf и, как правило, медленнее (примерно в 2-5 раз медленнее, в основном при десериализации), чем формат по умолчанию, который стремится к нулевым издержкам.
Начнем с основного сообщения:
struct example
{
zpp::bits:: vint32_t i; // varint of 32 bit, field number is implicitly set to 1,
// next field is implicitly 2, and so on
};
// Serialize as protobuf protocol (as usual, can also define this inside the class
// with `using serialize = zpp::bits::pb_protocol;`)
auto serialize ( const example &) -> zpp::bits::pb_protocol;
// Use archives as usual, specify what kind of size to prefix the message with.
// We chose no size to demonstrate the actual encoding of the message, but in general
// it is recommended to size prefix protobuf messages since they are not self terminating.
auto [data, in, out] = data_in_out(zpp::bits::no_size{});
out (example{. i = 150 }).or_throw();
example e;
in (e).or_throw();
// e.i == 150
// Serialize the message without any size prefix, and check the encoding at compile time:
static_assert (
zpp::bits::to_bytes<zpp::bits:: unsized_t <example>{{. i = 150 }}>() ==
"089601"_decode_hex);
Для полного синтаксиса, который мы позже будем использовать для передачи дополнительных параметров, используйте zpp::bits::protocol
:
// Serialize as protobuf protocol (as usual, can also define this inside the class
// with `using serialize = zpp::bits::protocol<zpp::bits::pb{}>;`)
auto serialize ( const example &) -> zpp::bits::protocol<zpp::bits::pb{}>;
Чтобы зарезервировать поля:
struct example
{
[[no_unique_address]] zpp::bits::pb_reserved _1; // field number 1 is reserved.
zpp::bits:: vint32_t i; // field number == 2
zpp::bits:: vsint32_t j; // field number == 3
};
Чтобы явно указать для каждого элемента номер поля:
struct example
{
zpp::bits::pb_field<zpp::bits:: vint32_t , 20 > i; // field number == 20
zpp::bits::pb_field<zpp::bits:: vsint32_t , 30 > j; // field number == 30
using serialize = zpp::bits::pb_protocol;
};
Доступ к значению поля часто прозрачен, однако, если это явно необходимо, используйте pb_value(<variable>)
для получения или присвоения значения.
Чтобы сопоставить элементы с другим номером поля:
struct example
{
zpp::bits:: vint32_t i; // field number == 20
zpp::bits:: vsint32_t j; // field number == 30
using serialize = zpp::bits::protocol<
zpp::bits::pb{
zpp::bits::pb_map< 1 , 20 >{}, // Map first member to field number 20.
zpp::bits::pb_map< 2 , 30 >{}}>; // Map second member to field number 30.
};
Фиксированные члены — это обычные члены данных C++:
struct example
{
std:: uint32_t i; // fixed unsigned integer 32, field number == 1
};
Как и в случае с zpp::bits::members
, когда это необходимо, вы можете указать количество участников в поле протокола с помощью zpp::bits::pb_members<N>
:
struct example
{
using serialize = zpp::bits::pb_members< 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
Полная версия вышеизложенного включает передачу количества членов в качестве второго параметра протокола:
struct example
{
using serialize = zpp::bits::protocol<zpp::bits::pb{}, 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
Встроенные сообщения просто вложены в класс как члены данных:
struct nested_example
{
example nested; // field number == 1
};
auto serialize ( const nested_example &) -> zpp::bits::pb_protocol;
static_assert (zpp::bits::to_bytes<zpp::bits:: unsized_t <nested_example>{
{. nested = example{ 150 }}}>() == "0a03089601"_decode_hex);
Повторяющиеся поля имеют форму владеющих контейнеров:
struct repeating
{
using serialize = zpp::bits::pb_protocol;
std::vector<zpp::bits:: vint32_t > integers; // field number == 1
std::string characters; // field number == 2
std::vector<example> examples; // repeating examples, field number == 3
};
В настоящее время все поля являются необязательными, что является хорошей практикой. В целях эффективности отсутствующие поля удаляются и не объединяются с сообщением. Любое значение, которое не установлено в сообщении, оставляет целевой элемент данных нетронутым, что позволяет реализовать значения по умолчанию для элементов данных с помощью нестатического инициализатора элемента данных или инициализировать элемент данных перед десериализацией сообщения.
Давайте возьмем полный файл .proto
и переведем его:
syntax = "proto3" ;
package tutorial ;
message person {
string name = 1 ;
int32 id = 2 ;
string email = 3 ;
enum phone_type {
mobile = 0 ;
home = 1 ;
work = 2 ;
}
message phone_number {
string number = 1 ;
phone_type type = 2 ;
}
repeated phone_number phones = 4 ;
}
message address_book {
repeated person people = 1 ;
}
Переведенный файл:
struct person
{
std::string name; // = 1
zpp::bits:: vint32_t id; // = 2
std::string email; // = 3
enum phone_type
{
mobile = 0 ,
home = 1 ,
work = 2 ,
};
struct phone_number
{
std::string number; // = 1
phone_type type; // = 2
};
std::vector<phone_number> phones; // = 4
};
struct address_book
{
std::vector<person> people; // = 1
};
auto serialize ( const person &) -> zpp::bits::pb_protocol;
auto serialize ( const person::phone_number &) -> zpp::bits::pb_protocol;
auto serialize ( const address_book &) -> zpp::bits::pb_protocol;
Десериализация сообщения, которое изначально было сериализовано с помощью Python:
import addressbook_pb2
person = addressbook_pb2 . person ()
person . id = 1234
person . name = "John Doe"
person . email = "[email protected]"
phone = person . phones . add ()
phone . number = "555-4321"
phone . type = addressbook_pb2 . person . home
Результат, который мы получаем для person
:
name : "John Doe"
id : 1234
email : "[email protected]"
phones {
number : "555-4321"
type : home
}
Давайте сериализуем это:
person . SerializeToString ()
Результат:
b' n x08 John Doe x10 xd2 t x1a x10 [email protected]" x0c n x08 555-4321 x10 x01 '
Вернемся к С++:
using namespace zpp ::bits::literals ;
constexpr auto data =
" nx08 John Doe x10xd2tx1ax10 [email protected] "x0cnx08 "
" 555-4321 x10x01 " _b;
static_assert (data.size() == 45);
person p;
zpp::bits::in{data, zpp::bits::no_size{}}(p).or_throw();
// p.name == "John Doe"
// p.id == 1234
// p.email == "[email protected]"
// p.phones.size() == 1
// p.phones[0].number == "555-4321"
// p.phones[0].type == person::home
По умолчанию zpp::bits
встраивается агрессивно, но для уменьшения размера кода он не встраивает полное декодирование варинтов (целых чисел переменной длины). Чтобы настроить встраивание полного декодирования varint, определите ZPP_BITS_INLINE_DECODE_VARINT=1
.
Если вы подозреваете, что zpp::bits
встраивает слишком много данных до такой степени, что это плохо влияет на размер кода, вы можете определить ZPP_BITS_INLINE_MODE=0
, который отключает всю принудительную встраивание и наблюдать за результатами. Обычно он дает незначительный эффект, но предусмотрен как есть для дополнительного контроля.
В некоторых компиляторах вы всегда можете обнаружить встроенную ошибку с рекурсивными структурами (например, древовидным графом). В этих случаях необходимо каким-то образом избежать атрибута Always Inline для конкретной структуры. Тривиальным примером может быть использование явной функции сериализации, хотя в большинстве случаев библиотека обнаруживает такие случаи, и в этом нет необходимости, но пример предоставляется просто в случае:
struct node
{
constexpr static auto serialize ( auto & archive, auto & node)
{
return archive (node. value , node. nodes );
}
int value;
std::vector<node> nodes;
};
библиотека | тестовый пример | размер бункера | размер данных | время службы | время |
---|---|---|---|---|---|
zpp_bits | общий | 52192Б | 8413Б | 733 мс | 693 мс |
zpp_bits | фиксированный буфер | 48000Б | 8413Б | 620 мс | 667 мс |
горький | общий | 70904Б | 6913Б | 1470 мс | 1524 мс |
горький | фиксированный буфер | 53648Б | 6913Б | 927 мс | 1466 мс |
способствовать росту | общий | 279024Б | 11037Б | 15126 мс | 12724 мс |
злак | общий | 70560Б | 10413Б | 10777 мс | 9088 мс |
плоские буферы | общий | 70640Б | 14924Б | 8757 мс | 3361 мс |
рукописный | общий | 47936Б | 10413Б | 1506 мс | 1577 мс |
рукописный | небезопасный | 47944Б | 10413Б | 1616 мс | 1392 мс |
iostream | общий | 53872Б | 8413Б | 11956 мс | 12928 мс |
пакет сообщений | общий | 89144Б | 8857Б | 2770 мс | 14033 мс |
протобуф | общий | 2077864Б | 10018Б | 19929 мс | 20592 мс |
протобуф | арена | 2077872Б | 10018Б | 10319 мс | 11787 мс |
да | общий | 61072Б | 10463Б | 2286 мс | 1770 мс |
библиотека | тестовый пример | размер бункера | размер данных | время службы | время |
---|---|---|---|---|---|
zpp_bits | общий | 47128Б | 8413Б | 790 мс | 715 мс |
zpp_bits | фиксированный буфер | 43056Б | 8413Б | 605 мс | 694 мс |
горький | общий | 53728Б | 6913Б | 2128 мс | 1832 мс |
горький | фиксированный буфер | 49248Б | 6913Б | 946 мс | 1941 мс |
способствовать росту | общий | 237008Б | 11037Б | 16011 мс | 13017 мс |
злак | общий | 61480Б | 10413Б | 9977 мс | 8565 мс |
плоские буферы | общий | 62512Б | 14924Б | 9812 мс | 3472 мс |
рукописный | общий | 43112Б | 10413Б | 1391 мс | 1321 мс |
рукописный | небезопасный | 43120Б | 10413Б | 1393 мс | 1212 мс |
iostream | общий | 48632Б | 8413Б | 10992 мс | 12771 мс |
пакет сообщений | общий | 77384Б | 8857Б | 3563 мс | 14705 мс |
протобуф | общий | 2032712Б | 10018Б | 18125 мс | 20211 мс |
протобуф | арена | 2032760Б | 10018Б | 9166 мс | 11378 мс |
да | общий | 51000Б | 10463Б | 2114 мс | 1558 мс |
Желаю вам найти эту библиотеку полезной. Пожалуйста, не стесняйтесь сообщать о любых проблемах, вносить предложения по улучшению и т. д.