Una biblioteca RPC y serialización binaria C++20 moderna, con un solo archivo de encabezado.
Esta biblioteca es la sucesora de zpp::serializer. La biblioteca intenta ser más sencilla de usar, pero tiene una API más o menos similar a su predecesora.
constexpr
zpp::bits
se escribe más fácilmente que zpp::serializer
.zpp::serializer
con 8 bytes fijos de identificador de serialización sha1. Para muchos tipos, habilitar la serialización es transparente y no requiere líneas de código adicionales. Se requiere que estos tipos sean de tipo agregado, sin miembros que no sean de matriz. A continuación se muestra un ejemplo de una clase person
con nombre y edad:
struct person
{
std::string name;
int age{};
};
Ejemplo de cómo serializar a la persona dentro y desde un vector de bytes:
// 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);
Este ejemplo casi funciona, se nos advierte que estamos descartando el valor de retorno. Para comprobar errores, sigue leyendo.
Necesitamos verificar si hay errores, la biblioteca ofrece varias formas de hacerlo: basado en un valor de retorno, basado en excepciones o basado en zpp::throwing.
La forma basada en el valor de retorno para ser más explícita, o si simplemente prefiere los valores de retorno:
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.
}
La forma basada en excepciones usando .or_throw()
(léase esto como "tener éxito o lanzar", de ahí 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 ;
});
}
Otra opción es zpp::throwing donde la verificación de errores se convierte en dos simples co_await
s. Para comprender cómo verificar errores, proporcionamos una función principal completa:
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 ;
});
}
Todos los métodos anteriores utilizan los siguientes códigos de error internamente y se pueden verificar usando el operador de comparación basado en el valor de retorno, o examinando el código de error interno de std::system_error
o zpp::throwing
dependiendo de cuál haya utilizado:
std::errc::result_out_of_range
: intentando escribir o leer desde un búfer demasiado corto.std::errc::no_buffer_space
: el búfer en crecimiento crecería más allá de los límites de asignación o se desbordaría.std::errc::value_too_large
- la codificación varint (entero de longitud variable) está más allá de los límites de representación.std::errc::message_size
: el tamaño del mensaje supera los límites de asignación definidos por el usuario.std::errc::not_supported
: intenta llamar a un RPC que no figura como compatible.std::errc::bad_message
: intenta leer una variante de tipo no reconocido.std::errc::invalid_argument
: intentando serializar un puntero nulo o una variante sin valor.std::errc::protocol_error
: intenta deserializar un mensaje de protocolo no válido. Para la mayoría de los tipos no agregados (o tipos agregados con miembros de matriz), habilitar la serialización es una sola línea. A continuación se muestra un ejemplo de una clase person
no agregada:
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{};
};
La mayoría de las veces, los tipos que serializamos pueden funcionar con enlace estructurado, y esta biblioteca aprovecha eso, pero debe proporcionar la cantidad de miembros en su clase para que esto funcione usando el método anterior.
Esto también funciona con la búsqueda dependiente de argumentos, lo que permite no modificar la clase fuente:
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
En algunos compiladores, SFINAE funciona con requires expression
bajo if constexpr
y unevaluated lambda expression
. Significa que incluso con tipos no agregados, el número de miembros se puede detectar automáticamente en los casos en que todos los miembros estén en la misma estructura. Para participar, defina 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{};
};
Esto funciona con clang 13
, sin embargo, la portabilidad de esto no está clara, ya que en gcc
no funciona (es un error grave) y establece explícitamente en el estándar que existe la intención de no permitir SFINAE en casos similares, por lo que está desactivado de forma predeterminada.
Si los miembros de tus datos o el constructor predeterminado son privados, debes hacerte amigo de zpp::bits::access
de esta manera:
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{};
};
Para habilitar guardar y cargar cualquier objeto mediante serialización explícita, que funciona independientemente de la compatibilidad del enlace estructurado, agregue las siguientes líneas a su clase:
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. object_1 , self. object_2 , ...);
}
Tenga en cuenta que object_1, object_2, ...
son los miembros de datos no estáticos de su clase.
Aquí está nuevamente el ejemplo de una clase de persona con función de serialización explícita:
struct person
{
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. name , self. age );
}
std::string name;
int age{};
};
O con búsqueda dependiente de argumentos:
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
Crear archivos de entrada y salida juntos y por separado de los datos:
// 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();
Los archivos se pueden crear a partir de cualquiera de los tipos de bytes:
// 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);
También puede utilizar objetos de datos de tamaño fijo como array, std::array
y tipos de vista como std::span
similar al anterior. Sólo necesitas asegurarte de que haya suficiente tamaño ya que no se pueden cambiar de tamaño:
// 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);
Cuando se utiliza un vector o una cadena, automáticamente crece hasta el tamaño correcto; sin embargo, con lo anterior los datos se limitan a los límites de las matrices o tramos.
Al crear el archivo de cualquiera de las formas anteriores, es posible pasar una cantidad variada de parámetros que controlan el comportamiento del archivo, como el orden de bytes, los tipos de tamaño predeterminados, la especificación del comportamiento de adición, etc. Esto se analiza en el resto del README.
Como se dijo anteriormente, la biblioteca es casi completamente constexpr, aquí hay un ejemplo del uso de una matriz como objeto de datos pero también de su uso en tiempo de compilación para serializar y deserializar una tupla de números enteros:
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 });
Para mayor comodidad, la biblioteca también proporciona algunas funciones de serialización simplificadas para el tiempo de compilación:
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 });
Consulta la posición de in
y out
usando position()
, en otras palabras, los bytes leídos y escritos respectivamente:
std:: size_t bytes_read = in.position();
std:: size_t bytes_written = out.position();
Restablecer la posición hacia atrás o hacia delante, o al principio, utilizar con extremo cuidado:
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.
Al serializar tipos de biblioteca estándar de longitud variable, como vectores, cadenas y tipos de vista como vista de intervalo y cadena, la biblioteca primero almacena un entero de 4 bytes que representa el tamaño, seguido de los elementos.
std::vector v = { 1 , 2 , 3 , 4 };
out (v);
in (v);
La razón por la cual el tipo de tamaño predeterminado es de 4 bytes (es decir, std::uint32_t
) es por portabilidad entre diferentes arquitecturas, además la mayoría de los programas casi nunca llegan al caso en que un contenedor tenga más de 2^32 elementos, y puede ser Es injusto pagar el precio de un tamaño de 8 bytes por defecto.
Para tipos de tamaño específicos que no sean 4 bytes, use zpp::bits::sized
/ zpp::bits::sized_t
así:
// 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);
Asegúrese de que el tipo de tamaño sea lo suficientemente grande para el objeto serializado; de lo contrario, se serializarán menos elementos, de acuerdo con las reglas de conversión de tipos sin firmar.
También puedes optar por no serializar el tamaño en absoluto, así:
// 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);
Donde es común, hay declaraciones de alias para versiones de tipos con tamaño o sin tamaño, por ejemplo, aquí están vector
y span
, otros como string
, string_view
, etc., usan el mismo patrón.
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.
La serialización de tipos de tamaño fijo como matrices, std::array
s, std::tuple
s no incluye ninguna sobrecarga excepto los elementos seguidos entre sí.
Es posible cambiar el tipo de tamaño predeterminado para todo el archivo durante la creación:
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{});
Para la mayoría de los tipos, la biblioteca sabe cómo optimizar y serializar objetos como bytes. Sin embargo, está deshabilitado cuando se utilizan funciones de serialización explícitas.
Si sabe que su tipo es serializable al igual que los bytes sin formato y está utilizando la serialización explícita, puede optar por participar y optimizar su serialización a una simple 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));
}
};
También es posible hacer esto directamente desde un vector o un conjunto de tipos trivialmente copiables, esta vez usamos bytes
en lugar de as_bytes
porque convertimos el contenido del vector en bytes en lugar del objeto vectorial en sí (los datos a los que apunta el vector en lugar de el objeto vectorial):
std::vector<point> points;
out (zpp::bits::bytes(points));
in (zpp::bits::bytes(points));
Sin embargo, en este caso el tamaño no está serializado; esto puede ampliarse en el futuro para admitir también la serialización del tamaño de manera similar a otros tipos de vista. Si necesita serializar como bytes y desea el tamaño, como solución alternativa es posible convertir a std::span<std::byte>
.
Si bien no existe una herramienta perfecta para manejar la compatibilidad con versiones anteriores de estructuras debido a la sobrecarga cero de la serialización, puedes usar std::variant
como una forma de versionar tus clases o crear un buen envío basado en polimorfismo, así es como:
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
Y luego a la serialización misma:
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);
La forma en que se serializa la variante es serializando su índice (0 o 1) como std::byte
antes de serializar el objeto real. Esto es muy eficiente, sin embargo, a veces los usuarios pueden querer elegir una identificación de serialización explícita para eso; consulte el punto siguiente
Para establecer una identificación de serialización personalizada, debe agregar una línea adicional dentro/fuera de su clase respectivamente:
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>;
Tenga en cuenta que los identificadores de serialización de los tipos de la variante deben coincidir en longitud o se producirá un error de compilación.
También puede usar cualquier secuencia de bytes en lugar de una cadena legible, así como un número entero o cualquier tipo literal. Aquí hay un ejemplo de cómo usar un hash de una cadena como identificación de serialización:
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
También puedes serializar solo los primeros bytes del hash, así:
// 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>;
Luego, el tipo se convierte a bytes en tiempo de compilación usando (... espere) zpp::bits::out
en tiempo de compilación, de modo que siempre que su tipo literal sea serializable de acuerdo con lo anterior, puede usarlo como identificación de serialización. La identificación se serializa en std::array<std::byte, N>
sin embargo, para 1, 2, 4 y 8 bytes su tipo subyacente es std::byte
std::uint16_t
, std::uin32_t
y std::uint64_t
respectivamente para facilidad de uso y eficiencia.
Si desea serializar la variante sin una identificación, o si sabe que una variante tendrá una identificación particular al deserializarla, puede hacerlo usando zpp::bits::known_id
para empaquetar su variante:
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));
Descripción de literales auxiliares en la biblioteca:
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
, la función no debe ser una plantilla y tener exactamente una sobrecarga: 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 ;
}
Cuando su función no recibe parámetros, el efecto es simplemente llamar a la función sin deserialización y el valor de retorno es el valor de retorno de su función. Cuando la función devuelve void, no hay ningún valor para el tipo resultante.
La biblioteca también proporciona una interfaz RPC (llamada a procedimiento remoto) delgada para permitir serializar y deserializar llamadas a funciones:
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);
Con respecto al manejo de errores, de manera similar a muchos ejemplos anteriores, puede usar el valor de retorno, excepciones o zpp::throwing
para manejar errores.
// 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);
Es posible que se omitan los ID de las llamadas RPC, por ejemplo, si se pasan fuera de banda; aquí se explica cómo lograrlo:
server.serve(id); // id is already known, don't deserialize it.
client.request_body<Id>(arguments...); // request without serializing id.
Las funciones miembro también se pueden registrar para RPC, sin embargo, el servidor necesita obtener una referencia al objeto de clase durante la construcción, y todas las funciones miembro deben pertenecer a la misma clase (aunque se pueden mezclar funciones de alcance de espacio de nombres):
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);
El RPC también puede funcionar en modo opaco y permitir que la función serialice/deserialice los datos, al vincular una función como opaca, usando 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>
>;
El orden de bytes predeterminado utilizado es el procesador/SO nativo seleccionado. Puede elegir otro orden de bytes usando zpp::bits::endian
durante la construcción, así:
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{});
En el extremo receptor (archivo de entrada), la biblioteca admite tipos de vista de tipos de bytes constantes, como std::span<const std::byte>
para obtener una vista de una parte de los datos sin copiarlos. Esto debe usarse con cuidado porque invalidar los iteradores de los datos contenidos podría causar un uso posterior a la liberación. Se proporciona para permitir la optimización cuando sea necesario:
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
}
También hay una versión sin tamaño, que consume el resto de los datos del archivo para permitir el caso de uso común de encabezado y luego una cantidad arbitraria de datos:
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
La biblioteca no admite la serialización de valores de puntero nulos, pero sí admite explícitamente punteros de propiedad opcionales, como para crear gráficos y estructuras complejas.
En teoría, es válido usar std::optional<std::unique_ptr<T>>
, pero se recomienda usar zpp::bits::optional_ptr<T>
creado específicamente, que optimiza el valor booleano que generalmente mantiene el objeto opcional. y utiliza un puntero nulo como estado no válido.
En ese caso, serializar un valor de puntero nulo serializará un byte cero, mientras que los valores no nulos se serializarán como un solo byte seguido de los bytes del objeto. (es decir, la serialización es idéntica a std::optional<T>
).
Como parte de la implementación de la biblioteca, fue necesario implementar algunos tipos de reflexión, para contar miembros y miembros visitantes, y la biblioteca los expone al usuario:
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);
El ejemplo anterior funciona con o sin ZPP_BITS_AUTODETECT_MEMBERS_MODE=1
, dependiendo de #if
. Como se señaló anteriormente, debemos confiar en una característica específica del compilador para detectar la cantidad de miembros que pueden no ser portátiles.
Los archivos se pueden construir con opciones de control adicionales como zpp::bits::append{}
que indica a los archivos de salida que establezcan la posición al final del vector u otra fuente de datos. (para archivos de entrada esta opción no tiene efecto)
std::vector<std::byte> data;
zpp::bits::out out (data, zpp::bits::append{});
Es posible utilizar múltiples controles y usarlos también con 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{});
El tamaño de asignación se puede limitar en el caso de un archivo de salida a un búfer en crecimiento o cuando se utiliza un archivo de entrada para limitar la longitud de un mensaje con prefijo de longitud única para evitar la asignación de un búfer muy grande por adelantado, usando zpp::bits::alloc_limit<L>{}
. El uso previsto es por razones de seguridad y cordura más que por una medición precisa de la asignación:
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 >{});
Para una mayor corrección, cuando se utiliza un búfer en crecimiento para la salida, si el búfer aumentó, el tamaño del búfer se cambia al final para la posición exacta del archivo de salida, esto genera un cambio de tamaño adicional que en la mayoría de los casos es aceptable, pero puede evitarlo. cambie el tamaño adicional y reconozca el final del búfer usando position()
. Puedes lograr esto usando zpp::bits::no_fit_size{}
:
zpp::bits::out out (data, zpp::bits::no_fit_size{});
Para controlar la ampliación del vector de archivo de salida, puede utilizar 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.
De forma predeterminada, por seguridad, un archivo de salida que utiliza un búfer en crecimiento comprueba si hay desbordamiento antes de que crezca cualquier búfer. Para sistemas de 64 bits, esta comprobación, aunque barata, es casi redundante, ya que es casi imposible desbordar un entero de 64 bits cuando representa un tamaño de memoria. (es decir, la asignación de memoria fallará antes de que la memoria esté a punto de desbordar este número entero). Si desea desactivar esas comprobaciones de desbordamiento, a favor del rendimiento, utilice: zpp::bits::no_enlarge_overflow{}
:
zpp::bits::out out (data, zpp::bits::no_enlarge_overflow{}); // Disable overflow check when enlarging.
Al serializar explícitamente, a menudo es necesario identificar si el archivo es de entrada o de salida, y se realiza a través de la función miembro estática archive.kind()
, y se puede realizar en un 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)
}
}
La biblioteca proporciona un tipo para serializar y deserializar enteros de longitud variable:
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;
A continuación se muestra un ejemplo de la codificación en tiempo de compilación:
static_assert (zpp::bits::to_bytes<zpp::bits::varint{ 150 }>() == "9601"_decode_hex);
La plantilla de clase zpp::bits::varint<T, E = varint_encoding::normal>
se proporciona para poder definir cualquier tipo integral o tipo de enumeración varint, junto con posibles codificaciones zpp::bits::varint_encoding::normal/zig_zag
(normal es el valor predeterminado).
Se proporcionan las siguientes declaraciones de alias:
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.
También es posible usar variantes para serializar tamaños de forma predeterminada durante la creación del archivo:
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.
El formato de serialización de esta biblioteca no se basa en ningún formato conocido o aceptado. Naturalmente, otros lenguajes no admiten este formato, lo que hace que sea casi imposible utilizar la biblioteca para la comunicación entre lenguajes de programación.
Por este motivo, la biblioteca admite el formato protobuf, que está disponible en muchos idiomas.
Tenga en cuenta que el soporte de protobuf es algo experimental, lo que significa que puede no incluir todas las características posibles de protobuf y generalmente es más lento (alrededor de 2 a 5 veces más lento, principalmente en la deserialización) que el formato predeterminado, que apunta a tener cero gastos generales.
Comenzando con el mensaje básico:
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);
Para obtener la sintaxis completa, que usaremos más adelante para pasar más opciones, use 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{}>;
Para reservar campos:
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
};
Para especificar explícitamente para cada miembro el número de campo:
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;
};
Acceder al valor detrás del campo suele ser transparente; sin embargo, si es necesario explícitamente, use pb_value(<variable>)
para obtener o asignar el valor.
Para asignar miembros a otro número de campo:
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.
};
Los miembros fijos son simplemente miembros de datos normales de C++:
struct example
{
std:: uint32_t i; // fixed unsigned integer 32, field number == 1
};
Al igual que con zpp::bits::members
, para cuando sea necesario, puede especificar el número de miembros en el campo de protocolo con zpp::bits::pb_members<N>
:
struct example
{
using serialize = zpp::bits::pb_members< 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
La versión completa de lo anterior implica pasar el número de miembros como segundo parámetro del protocolo:
struct example
{
using serialize = zpp::bits::protocol<zpp::bits::pb{}, 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
Los mensajes incrustados simplemente se anidan dentro de la clase como miembros de datos:
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);
Los campos repetidos tienen la forma de poseer contenedores:
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
};
Actualmente, todos los campos son opcionales, lo cual es una buena práctica; los campos faltantes se eliminan y no se concatenan con el mensaje para mayor eficiencia. Cualquier valor que no esté establecido en un mensaje deja intacto el miembro de datos de destino, lo que permite implementar valores predeterminados para los miembros de datos mediante el uso de un inicializador de miembro de datos no estático o inicializar el miembro de datos antes de deserializar el mensaje.
Tomemos un archivo .proto
completo y tradujémoslo:
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 ;
}
El archivo traducido:
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;
Deserializar un mensaje que fue serializado originalmente con 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
El resultado que obtenemos por person
es:
name : "John Doe"
id : 1234
email : "[email protected]"
phones {
number : "555-4321"
type : home
}
serialicémoslo:
person . SerializeToString ()
El resultado es:
b' n x08 John Doe x10 xd2 t x1a x10 [email protected]" x0c n x08 555-4321 x10 x01 '
Volver a C++:
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
De forma predeterminada, zpp::bits
inserta líneas agresivamente, pero para reducir el tamaño del código, no integra la decodificación completa de varints (enteros de longitud variable). Para configurar la inserción en línea de la decodificación variante completa, defina ZPP_BITS_INLINE_DECODE_VARINT=1
.
Si sospecha que zpp::bits
está insertando demasiado hasta el punto de afectar gravemente el tamaño del código, puede definir ZPP_BITS_INLINE_MODE=0
, que deshabilita toda inserción forzada y observa los resultados. Generalmente tiene un efecto insignificante, pero se proporciona tal cual para un control adicional.
En algunos compiladores, es posible que siempre en línea falle con estructuras recursivas (por ejemplo, un gráfico de árbol). En estos casos se requiere evitar de alguna manera el atributo siempre en línea para la estructura específica, un ejemplo trivial sería usar una función de serialización explícita, aunque la mayoría de las veces la biblioteca detecta tales ocasiones y no es necesario, pero el ejemplo se proporciona solo En caso:
struct node
{
constexpr static auto serialize ( auto & archive, auto & node)
{
return archive (node. value , node. nodes );
}
int value;
std::vector<node> nodes;
};
biblioteca | caso de prueba | tamaño del contenedor | tamaño de datos | tiempo de servicio | des tiempo |
---|---|---|---|---|---|
zpp_bits | general | 52192B | 8413B | 733ms | 693ms |
zpp_bits | buffer fijo | 48000B | 8413B | 620 ms | 667ms |
bitsery | general | 70904B | 6913B | 1470ms | 1524ms |
bitsery | buffer fijo | 53648B | 6913B | 927ms | 1466ms |
aumentar | general | 279024B | 11037B | 15126ms | 12724ms |
cereal | general | 70560B | 10413B | 10777ms | 9088ms |
buffers planos | general | 70640B | 14924B | 8757 ms | 3361 ms |
escrito | general | 47936B | 10413B | 1506ms | 1577ms |
escrito | inseguro | 47944B | 10413B | 1616ms | 1392ms |
iostream | general | 53872B | 8413B | 11956ms | 12928ms |
paquete de mensajes | general | 89144B | 8857B | 2770 ms | 14033ms |
protobuf | general | 2077864B | 10018B | 19929ms | 20592 ms |
protobuf | arena | 2077872B | 10018B | 10319 ms | 11787ms |
sí | general | 61072B | 10463B | 2286ms | 1770ms |
biblioteca | caso de prueba | tamaño del contenedor | tamaño de datos | tiempo de servicio | des tiempo |
---|---|---|---|---|---|
zpp_bits | general | 47128B | 8413B | 790 ms | 715ms |
zpp_bits | buffer fijo | 43056B | 8413B | 605ms | 694ms |
bitsery | general | 53728B | 6913B | 2128ms | 1832ms |
bitsery | buffer fijo | 49248B | 6913B | 946ms | 1941ms |
aumentar | general | 237008B | 11037B | 16011ms | 13017ms |
cereal | general | 61480B | 10413B | 9977ms | 8565ms |
buffers planos | general | 62512B | 14924B | 9812ms | 3472ms |
escrito | general | 43112B | 10413B | 1391ms | 1321ms |
escrito | inseguro | 43120B | 10413B | 1393ms | 1212ms |
iostream | general | 48632B | 8413B | 10992ms | 12771ms |
paquete de mensajes | general | 77384B | 8857B | 3563ms | 14705ms |
protobuf | general | 2032712B | 10018B | 18125ms | 20211ms |
protobuf | arena | 2032760B | 10018B | 9166ms | 11378ms |
sí | general | 51000B | 10463B | 2114ms | 1558ms |
Deseo que encuentres útil esta biblioteca. No dude en enviar cualquier problema, hacer sugerencias para mejorar, etc.