Uma moderna serialização binária C++20 e biblioteca RPC, com apenas um arquivo de cabeçalho.
Esta biblioteca é sucessora de zpp::serializer. A biblioteca tenta ser mais simples de usar, mas possui API mais ou menos semelhante à sua antecessora.
constexpr
zpp::bits
é digitado mais facilmente do que zpp::serializer
.zpp::serializer
com 8 bytes fixos de ID de serialização sha1. Para muitos tipos, a habilitação da serialização é transparente e não requer linhas de código adicionais. Esses tipos devem ser do tipo agregado, sem membros da matriz. Aqui está um exemplo de uma classe person
com nome e idade:
struct person
{
std::string name;
int age{};
};
Exemplo de como serializar a pessoa em e a partir de um vetor 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 exemplo quase funciona, estamos sendo avisados que estamos descartando o valor de retorno. Para verificação de erros, continue lendo.
Precisamos verificar se há erros, a biblioteca oferece várias maneiras de fazer isso - baseado em valor de retorno, baseado em exceção ou baseado em zpp::throwing.
A forma baseada no valor de retorno para ser mais explícita, ou se você preferir apenas 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.
}
A maneira baseada em exceções usando .or_throw()
(leia isso como "sucesso ou lançamento" - daí 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 ;
});
}
Outra opção é zpp::throwing onde a verificação de erros se transforma em dois simples co_await
s. Para entender como verificar erros, fornecemos uma função 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 os métodos acima usam os seguintes códigos de erro internamente e podem ser verificados usando o operador de comparação com base no valor de retorno ou examinando o código de erro interno de std::system_error
ou zpp::throwing
dependendo de qual você usou:
std::errc::result_out_of_range
– tentativa de escrever ou ler de um buffer muito curto.std::errc::no_buffer_space
- o buffer crescente cresceria além dos limites de alocação ou estouro.std::errc::value_too_large
- a codificação varint (inteiro de comprimento variável) está além dos limites de representação.std::errc::message_size
- o tamanho da mensagem está além dos limites de alocação definidos pelo usuário.std::errc::not_supported
– tenta chamar um RPC que não está listado como suportado.std::errc::bad_message
- tenta ler uma variante de tipo não reconhecido.std::errc::invalid_argument
- tentativa de serializar um ponteiro nulo ou uma variante sem valor.std::errc::protocol_error
- tentativa de desserializar uma mensagem de protocolo inválida. Para a maioria dos tipos não agregados (ou tipos agregados com membros da matriz), habilitar a serialização é uma linha única. Aqui está um exemplo de uma classe person
não 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{};
};
Na maioria das vezes, os tipos que serializamos podem funcionar com ligação estruturada, e esta biblioteca tira vantagem disso, mas você precisa fornecer o número de membros em sua classe para que isso funcione usando o método acima.
Isso também funciona com pesquisa dependente de argumento, permitindo não modificar a classe de origem:
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
Em alguns compiladores, SFINAE funciona com requires expression
em if constexpr
e unevaluated lambda expression
. Isso significa que mesmo com tipos não agregados o número de membros pode ser detectado automaticamente nos casos em que todos os membros estão na mesma estrutura. Para aceitar, 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{};
};
Isso funciona com clang 13
, porém a portabilidade disso não está clara, pois no gcc
não funciona (é um erro grave) e afirma explicitamente no padrão que há intenção de não permitir SFINAE em casos semelhantes, então está desativado por padrão.
Se seus membros de dados ou construtor padrão forem privados, você precisará se tornar amigo de zpp::bits::access
assim:
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 ativar o salvamento e carregamento de qualquer objeto usando serialização explícita, que funciona independentemente da compatibilidade de ligação estruturada, adicione as seguintes linhas à sua classe:
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. object_1 , self. object_2 , ...);
}
Observe que object_1, object_2, ...
são os membros de dados não estáticos da sua classe.
Aqui está o exemplo de uma classe person novamente com função de serialização explícita:
struct person
{
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. name , self. age );
}
std::string name;
int age{};
};
Ou com pesquisa dependente de argumento:
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
Criando arquivos de entrada e saída juntos e separadamente dos dados:
// 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();
Os arquivos podem ser criados a partir de qualquer um dos 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);
Você também pode usar objetos de dados de tamanho fixo, como array, std::array
e tipos de visualização, como std::span
semelhantes ao acima. Você só precisa ter certeza de que há tamanho suficiente, pois eles não são redimensionáveis:
// 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);
Ao usar um vetor ou string, ele cresce automaticamente até o tamanho certo; no entanto, com o acima, os dados são limitados aos limites das matrizes ou extensões.
Ao criar o arquivo de qualquer uma das maneiras acima, é possível passar um número variado de parâmetros que controlam o comportamento do arquivo, como ordem de bytes, tipos de tamanho padrão, especificação de comportamento de acréscimo e assim por diante. Isso é discutido no restante do README.
Como foi dito acima, a biblioteca é quase completamente constexpr, aqui está um exemplo de uso de array como objeto de dados, mas também de uso em tempo de compilação para serializar e desserializar uma tupla de inteiros:
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 });
Por conveniência, a biblioteca também fornece algumas funções de serialização simplificadas para tempo de compilação:
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 });
Consulte a posição de in
e out
usando position()
, ou seja, os bytes lidos e escritos respectivamente:
std:: size_t bytes_read = in.position();
std:: size_t bytes_written = out.position();
Redefinir a posição para trás ou para frente, ou para o início, use com 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.
Ao serializar tipos de biblioteca padrão de comprimento variável, como vetores, strings e tipos de visualização, como span e string view, a biblioteca primeiro armazena um número inteiro de 4 bytes representando o tamanho, seguido pelos elementos.
std::vector v = { 1 , 2 , 3 , 4 };
out (v);
in (v);
A razão pela qual o tipo de tamanho padrão é de 4 bytes (ou seja, std::uint32_t
) é para portabilidade entre diferentes arquiteturas, assim como a maioria dos programas quase nunca atinge o caso de um contêiner ter mais de 2 ^ 32 itens, e pode ser injusto pagar o preço do tamanho de 8 bytes por padrão.
Para tipos de tamanho específicos que não sejam 4 bytes, use zpp::bits::sized
/ zpp::bits::sized_t
assim:
// 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);
Certifique-se de que o tipo de tamanho seja grande o suficiente para o objeto serializado, caso contrário, menos itens serão serializados, de acordo com as regras de conversão de tipos não assinados.
Você também pode optar por não serializar o tamanho, assim:
// 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);
Pois onde é comum, existem declarações de alias para versões dimensionadas/não dimensionadas de tipos, por exemplo, aqui estão vector
e span
, outros como string
, string_view
, etc estão usando o mesmo padrão.
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.
A serialização de tipos de tamanho fixo, como matrizes, std::array
s, std::tuple
s não inclui nenhuma sobrecarga, exceto os elementos seguidos um pelo outro.
É possível alterar o tipo de tamanho padrão para todo o arquivo durante a criação:
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{});
A maioria dos tipos que a biblioteca sabe otimizar e serializar objetos como bytes. No entanto, é desativado ao usar funções de serialização explícitas.
Se você sabe que seu tipo é serializável apenas como bytes brutos e está usando serialização explícita, você pode optar e otimizar sua serialização para um mero 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));
}
};
Também é possível fazer isso diretamente de um vetor ou intervalo de tipos trivialmente copiáveis, desta vez usamos bytes
em vez de as_bytes
porque convertemos o conteúdo do vetor em bytes em vez do próprio objeto de vetor (os dados para os quais o vetor aponta em vez de o objeto vetorial):
std::vector<point> points;
out (zpp::bits::bytes(points));
in (zpp::bits::bytes(points));
No entanto, neste caso o tamanho não é serializado, isto pode ser estendido no futuro para também suportar a serialização do tamanho semelhante a outros tipos de visualização. Se você precisar serializar como bytes e quiser o tamanho, como solução alternativa, é possível converter para std::span<std::byte>
.
Embora não exista uma ferramenta perfeita para lidar com a compatibilidade retroativa de estruturas devido à sobrecarga zero da serialização, você pode usar std::variant
como uma forma de versionar suas classes ou criar um bom despacho baseado em polimorfismo, veja 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
E então para a serialização em si:
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);
A maneira como a variante é serializada é serializando seu índice (0 ou 1) como std::byte
antes de serializar o objeto real. Isto é muito eficiente, porém às vezes os usuários podem querer escolher um ID de serialização explícito para isso, consulte o ponto abaixo
Para definir um ID de serialização personalizado, você precisa adicionar uma linha adicional dentro/fora da sua classe, 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>;
Observe que os IDs de serialização dos tipos na variante devem corresponder em comprimento ou ocorrerá um erro de compilação.
Você também pode usar qualquer sequência de bytes em vez de uma string legível, bem como um número inteiro ou qualquer tipo literal. Aqui está um exemplo de como usar um hash de uma string como um ID de serialização:
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
Você também pode serializar apenas os primeiros bytes do hash, assim:
// 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>;
O tipo é então convertido em bytes em tempo de compilação usando (... espere) zpp::bits::out
em tempo de compilação, portanto, desde que seu tipo literal seja serializável de acordo com o acima, você pode usá-lo como um ID de serialização. O id é serializado para std::array<std::byte, N>
no entanto, para 1, 2, 4 e 8 bytes, seu tipo subjacente é std::byte
std::uint16_t
, std::uin32_t
e std::uint64_t
respectivamente para facilidade de uso e eficiência.
Se você deseja serializar a variante sem um ID, ou se sabe que uma variante terá um ID específico ao desserializar, você pode fazer isso usando zpp::bits::known_id
para encapsular sua 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));
Descrição dos literais auxiliares na 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
, a função deve ser não-modelo e ter exatamente uma 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 ;
}
Quando sua função não recebe parâmetros, o efeito é apenas chamar a função sem desserialização e o valor de retorno é o valor de retorno da sua função. Quando a função retorna void, não há valor para o tipo resultante.
A biblioteca também fornece uma interface RPC (chamada de procedimento remoto) fina para permitir serializar e desserializar chamadas de função:
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);
Em relação ao tratamento de erros, semelhante a muitos exemplos acima, você pode usar valor de retorno, exceções ou zpp::throwing
forma para tratar erros.
// 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);
É possível que os IDs das chamadas RPC sejam ignorados, por exemplo, se forem passados fora de banda, veja como fazer isso:
server.serve(id); // id is already known, don't deserialize it.
client.request_body<Id>(arguments...); // request without serializing id.
As funções-membro também podem ser registradas para RPC, no entanto, o servidor precisa obter uma referência ao objeto de classe durante a construção, e todas as funções-membro devem pertencer à mesma classe (embora as funções de escopo do namespace possam ser misturadas):
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);
O RPC também pode funcionar em modo opaco e deixar a própria função serializar/desserializar os dados, ao vincular uma função 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>
>;
A ordem de bytes padrão usada é o processador/sistema operacional nativo selecionado. Você pode escolher outra ordem de bytes usando zpp::bits::endian
durante a construção da seguinte forma:
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{});
No lado receptor (arquivo de entrada), a biblioteca suporta tipos de visualização de tipos const byte, como std::span<const std::byte>
para obter uma visualização de uma parte dos dados sem copiar. Isso precisa ser usado com cuidado porque a invalidação dos iteradores dos dados contidos pode causar um uso após a liberação. É fornecido para permitir a otimização quando necessário:
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
}
Há também uma versão sem tamanho, que consome o restante dos dados do arquivo para permitir o caso de uso comum de cabeçalho e, em seguida, uma quantidade arbitrária de dados:
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
A biblioteca não oferece suporte à serialização de valores de ponteiro nulo, mas suporta explicitamente ponteiros proprietários opcionais, como para criar gráficos e estruturas complexas.
Em teoria, é válido usar std::optional<std::unique_ptr<T>>
, mas é recomendado usar o zpp::bits::optional_ptr<T>
feito especificamente, que otimiza o booleano que o objeto opcional geralmente mantém, e usa ponteiro nulo como um estado inválido.
Nesse caso, serializar um valor de ponteiro nulo serializará um byte zero, enquanto valores não nulos seriam serializados como um único byte seguido pelos bytes do objeto. (ou seja, a serialização é idêntica a std::optional<T>
).
Como parte da implementação da biblioteca foi necessário implementar alguns tipos de reflexão, para contagem de membros e membros visitantes, e a biblioteca os expõe ao usuário:
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);
O exemplo acima funciona com ou sem ZPP_BITS_AUTODETECT_MEMBERS_MODE=1
, dependendo do #if
. Conforme observado acima, devemos contar com recursos específicos do compilador para detectar o número de membros que podem não ser portáveis.
Os arquivos podem ser construídos com opções de controle adicionais, como zpp::bits::append{}
que instrui os arquivos de saída a definir a posição até o final do vetor ou outra fonte de dados. (para arquivos de entrada esta opção não tem efeito)
std::vector<std::byte> data;
zpp::bits::out out (data, zpp::bits::append{});
É possível usar vários controles e usá-los também com 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{});
O tamanho da alocação pode ser limitado no caso de arquivo de saída para um buffer crescente ou ao usar um arquivo de entrada para limitar o tamanho de uma mensagem prefixada de comprimento único para evitar a alocação de um buffer muito grande antecipadamente, usando zpp::bits::alloc_limit<L>{}
. O uso pretendido é por razões de segurança e sanidade, e não por uma medição precisa da alocação:
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 melhor correção, ao usar buffer crescente para saída, se o buffer foi aumentado, o buffer é redimensionado no final para a posição exata do arquivo de saída, isso incorre em um redimensionamento extra que na maioria dos casos é aceitável, mas você pode evitar isso redimensionamento adicional e reconhecimento do final do buffer usando position()
. Você pode conseguir isso usando zpp::bits::no_fit_size{}
:
zpp::bits::out out (data, zpp::bits::no_fit_size{});
Para controlar a ampliação do vetor do arquivo de saída, você pode usar 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.
Por padrão, por segurança, um arquivo de saída que usa um buffer crescente verifica se há estouro antes de qualquer buffer aumentar. Para sistemas de 64 bits, esta verificação embora barata, é quase redundante, pois é quase impossível estourar um número inteiro de 64 bits quando ele representa um tamanho de memória. (ou seja, a alocação de memória falhará antes que a memória chegue perto de estourar esse número inteiro). Se você deseja desabilitar essas verificações de overflow, em favor do desempenho, use: zpp::bits::no_enlarge_overflow{}
:
zpp::bits::out out (data, zpp::bits::no_enlarge_overflow{}); // Disable overflow check when enlarging.
Ao serializar explicitamente, muitas vezes é necessário identificar se o arquivo é um arquivo de entrada ou de saída, e isso é feito por meio da função de membro estático archive.kind()
e pode ser feito em um 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)
}
}
A biblioteca fornece um tipo para serializar e desserializar inteiros de comprimento variável:
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;
Aqui está um exemplo da codificação em tempo de compilação:
static_assert (zpp::bits::to_bytes<zpp::bits::varint{ 150 }>() == "9601"_decode_hex);
O modelo de classe zpp::bits::varint<T, E = varint_encoding::normal>
é fornecido para poder definir qualquer tipo integral varint ou tipo de enumeração, junto com codificações possíveis zpp::bits::varint_encoding::normal/zig_zag
(normal é o padrão).
As seguintes declarações de alias são fornecidas:
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.
Usar varints para serializar tamanhos por padrão também é possível durante a criação do arquivo:
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.
O formato de serialização desta biblioteca não é baseado em nenhum formato conhecido ou aceito. Naturalmente, outras linguagens não suportam este formato, o que torna quase impossível o uso da biblioteca para comunicação entre linguagens de programação.
Por esta razão a biblioteca suporta o formato protobuf que está disponível em vários idiomas.
Observe que o suporte ao protobuf é meio experimental, o que significa que pode não incluir todos os recursos possíveis do protobuf e geralmente é mais lento (cerca de 2 a 5 vezes mais lento, principalmente na desserialização) do que o formato padrão, que visa ter sobrecarga zero.
Começando com a mensagem básica:
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 a sintaxe completa, que usaremos mais tarde para passar mais opções, 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 explicitamente para cada membro o número do 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;
};
Acessar o valor por trás do campo geralmente é transparente, no entanto, se for explicitamente necessário, use pb_value(<variable>)
para obter ou atribuir ao valor.
Para mapear membros para outro 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.
};
Membros fixos são simplesmente membros regulares de dados C++:
struct example
{
std:: uint32_t i; // fixed unsigned integer 32, field number == 1
};
Assim como zpp::bits::members
, quando for necessário, você pode especificar o número de membros no campo de protocolo com zpp::bits::pb_members<N>
:
struct example
{
using serialize = zpp::bits::pb_members< 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
A versão completa acima envolve passar o número de membros como o segundo parâmetro do protocolo:
struct example
{
using serialize = zpp::bits::protocol<zpp::bits::pb{}, 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
As mensagens incorporadas são simplesmente aninhadas na classe como membros de dados:
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);
Os campos repetidos têm a forma de possuir contêineres:
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
};
Atualmente todos os campos são opcionais, o que é uma boa prática, os campos faltantes são descartados e não concatenados à mensagem, para maior eficiência. Qualquer valor que não seja definido em uma mensagem deixa o membro de dados de destino intacto, o que permite implementar padrões para membros de dados usando o inicializador de membro de dados não estático ou inicializar o membro de dados antes de desserializar a mensagem.
Vamos pegar um arquivo .proto
completo e traduzi-lo:
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 ;
}
O arquivo traduzido:
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;
Desserializando uma mensagem que foi originalmente serializada com 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
A saída que obtemos para person
é:
name : "John Doe"
id : 1234
email : "[email protected]"
phones {
number : "555-4321"
type : home
}
Vamos serializá-lo:
person . SerializeToString ()
O resultado é:
b' n x08 John Doe x10 xd2 t x1a x10 [email protected]" x0c n x08 555-4321 x10 x01 '
De volta ao 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
Por padrão, zpp::bits
inline agressivamente, mas para reduzir o tamanho do código, ele não inline a decodificação completa de varints (números inteiros de comprimento variável). Para configurar o inlining da decodificação varint completa, defina ZPP_BITS_INLINE_DECODE_VARINT=1
.
Se você suspeitar que zpp::bits
está inlining demais a ponto de afetar gravemente o tamanho do código, você pode definir ZPP_BITS_INLINE_MODE=0
, que desativa todo inlining forçado e observar os resultados. Geralmente tem um efeito insignificante, mas é fornecido como está para controle adicional.
Em alguns compiladores, você pode encontrar sempre inline para falhar com estruturas recursivas (por exemplo, um gráfico de árvore). Nestes casos é necessário evitar de alguma forma o atributo always inline para a estrutura específica, um exemplo trivial seria usar uma função de serialização explícita, embora na maioria das vezes a biblioteca detecte tais ocasiões e não seja necessário, mas o exemplo é fornecido apenas em 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 teste | tamanho da caixa | tamanho dos dados | ser hora | hora |
---|---|---|---|---|---|
zpp_bits | em geral | 52192B | 8413B | 733ms | 693ms |
zpp_bits | buffer fixo | 48000B | 8413B | 620ms | 667ms |
bitéria | em geral | 70904B | 6913B | 1470ms | 1524ms |
bitéria | buffer fixo | 53648B | 6913B | 927ms | 1466ms |
impulsionar | em geral | 279024B | 11037B | 15126ms | 12724ms |
cereal | em geral | 70560B | 10413B | 10777ms | 9088ms |
buffers planos | em geral | 70640B | 14924B | 8757ms | 3361ms |
manuscrito | em geral | 47936B | 10413B | 1506ms | 1577ms |
manuscrito | inseguro | 47944B | 10413B | 1616ms | 1392ms |
iostream | em geral | 53872B | 8413B | 11956ms | 12928ms |
pacote de mensagens | em geral | 89144B | 8857B | 2770ms | 14033 ms |
protobuf | em geral | 2077864B | 10018B | 19929ms | 20592ms |
protobuf | arena | 2077872B | 10018B | 10319ms | 11787 ms |
sim | em geral | 61072B | 10463B | 2286ms | 1770ms |
biblioteca | caso de teste | tamanho da caixa | tamanho dos dados | ser hora | hora |
---|---|---|---|---|---|
zpp_bits | em geral | 47128B | 8413B | 790ms | 715ms |
zpp_bits | buffer fixo | 43056B | 8413B | 605ms | 694ms |
bitéria | em geral | 53728B | 6913B | 2128ms | 1832ms |
bitéria | buffer fixo | 49248B | 6913B | 946ms | 1941ms |
impulsionar | em geral | 237008B | 11037B | 16011ms | 13017ms |
cereal | em geral | 61480B | 10413B | 9977ms | 8565ms |
buffers planos | em geral | 62512B | 14924B | 9812ms | 3472 ms |
manuscrito | em geral | 43112B | 10413B | 1391ms | 1321ms |
manuscrito | inseguro | 43120B | 10413B | 1393ms | 1212ms |
iostream | em geral | 48632B | 8413B | 10992ms | 12771ms |
pacote de mensagens | em geral | 77384B | 8857B | 3563ms | 14705ms |
protobuf | em geral | 2032712B | 10018B | 18125ms | 20211ms |
protobuf | arena | 2032760B | 10018B | 9166ms | 11378ms |
sim | em geral | 51000B | 10463B | 2114ms | 1558ms |
Desejo que você considere esta biblioteca útil. Fique à vontade para enviar quaisquer problemas, fazer sugestões de melhorias, etc.