Une bibliothèque de sérialisation binaire et RPC C++20 moderne, avec un seul fichier d'en-tête.
Cette bibliothèque est un successeur de zpp::serializer. La bibliothèque essaie d'être plus simple à utiliser, mais possède une API plus ou moins similaire à celle de son prédécesseur.
constexpr
zpp::bits
est plus facile à saisir que zpp::serializer
.zpp::serializer
avec 8 octets fixes d'identifiant de sérialisation sha1. Pour de nombreux types, l’activation de la sérialisation est transparente et ne nécessite aucune ligne de code supplémentaire. Ces types doivent être de type agrégé, avec des membres non membres du tableau. Voici un exemple de classe person
avec nom et âge :
struct person
{
std::string name;
int age{};
};
Exemple de sérialisation de la personne vers et depuis un vecteur d'octets :
// 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);
Cet exemple fonctionne presque, nous sommes avertis que nous supprimons la valeur de retour. Pour vérifier les erreurs, continuez à lire.
Nous devons vérifier les erreurs, la bibliothèque propose plusieurs façons de le faire - une valeur de retour basée sur une exception ou basée sur zpp::throwing.
La méthode basée sur les valeurs de retour pour être la plus explicite, ou si vous préférez simplement les valeurs de retour :
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 méthode basée sur les exceptions utilisant .or_throw()
(lisez ceci comme "réussir ou lancer" - d'où 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 ;
});
}
Une autre option est zpp::throwing où la vérification des erreurs se transforme en deux simples co_await
s, pour comprendre comment vérifier les erreurs, nous fournissons une fonction principale complète :
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 ;
});
}
Toutes les méthodes ci-dessus utilisent les codes d'erreur suivants en interne et peuvent être vérifiées à l'aide de l'opérateur de comparaison à partir de la valeur de retour basée sur la valeur de retour, ou en examinant le code d'erreur interne de std::system_error
ou zpp::throwing
selon celui que vous avez utilisé :
std::errc::result_out_of_range
- tentative d'écriture ou de lecture à partir d'un tampon trop court.std::errc::no_buffer_space
- la croissance du tampon augmenterait au-delà des limites d'allocation ou dépasserait.std::errc::value_too_large
- l'encodage varint (entier de longueur variable) dépasse les limites de représentation.std::errc::message_size
- la taille du message dépasse les limites d'allocation définies par l'utilisateur.std::errc::not_supported
- tente d'appeler un RPC qui n'est pas répertorié comme pris en charge.std::errc::bad_message
- tente de lire une variante de type non reconnu.std::errc::invalid_argument
- tentative de sérialisation d'un pointeur nul ou d'une variante sans valeur.std::errc::protocol_error
- tentative de désérialiser un message de protocole invalide. Pour la plupart des types non agrégés (ou des types agrégés avec des membres de tableau), l'activation de la sérialisation se fait en une seule ligne. Voici un exemple de classe person
non agrégée :
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 plupart du temps, les types que nous sérialisons peuvent fonctionner avec une liaison structurée, et cette bibliothèque en profite, mais vous devez fournir le nombre de membres de votre classe pour que cela fonctionne en utilisant la méthode ci-dessus.
Cela fonctionne également avec la recherche dépendante des arguments, permettant de ne pas modifier la classe source :
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
Dans certains compilateurs, SFINAE fonctionne avec requires expression
sous if constexpr
et unevaluated lambda expression
. Cela signifie que même avec des types non agrégés, le nombre de membres peut être détecté automatiquement dans les cas où tous les membres sont dans la même structure. Pour vous inscrire, définissez 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{};
};
Cela fonctionne avec clang 13
, mais la portabilité n'est pas claire, car dans gcc
cela ne fonctionne pas (c'est une erreur grave) et il est explicitement indiqué dans la norme qu'il y a l'intention de ne pas autoriser SFINAE dans des cas similaires, donc il est désactivé par défaut.
Si vos données membres ou votre constructeur par défaut sont privés, vous devez devenir ami avec zpp::bits::access
comme ceci :
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{};
};
Pour activer la sauvegarde et le chargement de n'importe quel objet à l'aide de la sérialisation explicite, qui fonctionne quelle que soit la compatibilité des liaisons structurées, ajoutez les lignes suivantes à votre classe :
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. object_1 , self. object_2 , ...);
}
Notez que object_1, object_2, ...
sont les données membres non statiques de votre classe.
Voici à nouveau l'exemple d'une classe personne avec une fonction de sérialisation explicite :
struct person
{
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. name , self. age );
}
std::string name;
int age{};
};
Ou avec une recherche dépendante des arguments :
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
Création d'archives d'entrée et de sortie ensemble et séparément des données :
// 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();
Les archives peuvent être créées à partir de l'un des types d'octets :
// 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);
Vous pouvez également utiliser des objets de données de taille fixe tels que array, std::array
et des types de vue tels que std::span
similaires à ceux ci-dessus. Il vous suffit de vous assurer qu'il y a suffisamment de taille puisqu'ils ne sont pas redimensionnables :
// 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);
Lorsque vous utilisez un vecteur ou une chaîne, il atteint automatiquement la bonne taille. Cependant, avec ce qui précède, les données sont limitées aux limites des tableaux ou des étendues.
Lors de la création de l'archive de l'une des manières ci-dessus, il est possible de transmettre un nombre variable de paramètres qui contrôlent le comportement de l'archive, tels que l'ordre des octets, les types de taille par défaut, la spécification du comportement d'ajout, etc. Ceci est discuté dans le reste du README.
Comme cela a été dit ci-dessus, la bibliothèque est presque entièrement constexpr, voici un exemple d'utilisation d'un tableau comme objet de données mais également de son utilisation au moment de la compilation pour sérialiser et désérialiser un tuple d'entiers :
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 });
Pour plus de commodité, la bibliothèque fournit également quelques fonctions de sérialisation simplifiées pour la compilation :
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 });
Interrogez la position de in
et out
en utilisant position()
, autrement dit les octets lus et écrits respectivement :
std:: size_t bytes_read = in.position();
std:: size_t bytes_written = out.position();
Remettre la position en arrière ou en avant, ou au début, utiliser avec une extrême prudence :
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.
Lors de la sérialisation de types de bibliothèques standard de longueur variable, tels que les vecteurs, les chaînes et les types de vue tels que la vue étendue et la vue chaîne, la bibliothèque stocke d'abord un entier de 4 octets représentant la taille, suivi des éléments.
std::vector v = { 1 , 2 , 3 , 4 };
out (v);
in (v);
La raison pour laquelle le type de taille par défaut est de 4 octets (c'est-à-dire std::uint32_t
) est pour la portabilité entre différentes architectures, et la plupart des programmes n'atteignent presque jamais le cas où un conteneur contient plus de 2 ^ 32 éléments, et cela peut être injuste de payer le prix d'une taille de 8 octets par défaut.
Pour les types de taille spécifiques qui ne font pas 4 octets, utilisez zpp::bits::sized
/ zpp::bits::sized_t
comme ceci :
// 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);
Assurez-vous que le type de taille est suffisamment grand pour l'objet sérialisé, sinon moins d'éléments seront sérialisés, conformément aux règles de conversion des types non signés.
Vous pouvez également choisir de ne pas sérialiser la taille du tout, comme ceci :
// 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);
Là où c'est courant, il existe des déclarations d'alias pour les versions de types dimensionnées/non dimensionnées, par exemple, voici vector
et span
, d'autres telles que string
, string_view
, etc. utilisent le même modèle.
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 sérialisation de types de taille fixe tels que les tableaux, std::array
s, std::tuple
s n'inclut aucune surcharge, à l'exception des éléments suivis les uns des autres.
Changer le type de taille par défaut pour l'ensemble de l'archive est possible lors de la création :
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{});
La plupart des types que la bibliothèque sait optimiser et sérialiser les objets sous forme d'octets. Il est cependant désactivé lors de l'utilisation de fonctions de sérialisation explicites.
Si vous savez que votre type est sérialisable au même titre que les octets bruts et que vous utilisez la sérialisation explicite, vous pouvez l'activer et optimiser sa sérialisation en une 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));
}
};
Il est également possible de le faire directement à partir d'un vecteur ou d'un ensemble de types trivialement copiables, cette fois nous utilisons bytes
au lieu de as_bytes
car nous convertissons le contenu du vecteur en octets plutôt qu'en objet vectoriel lui-même (les données vers lesquelles le vecteur pointe plutôt que l'objet vectoriel) :
std::vector<point> points;
out (zpp::bits::bytes(points));
in (zpp::bits::bytes(points));
Cependant, dans ce cas, la taille n'est pas sérialisée, cela pourra être étendu à l'avenir pour prendre également en charge la sérialisation de la taille similaire à d'autres types de vues. Si vous devez sérialiser en octets et souhaitez la taille, comme solution de contournement, il est possible de convertir en std::span<std::byte>
.
Bien qu'il n'existe pas d'outil parfait pour gérer la compatibilité ascendante des structures en raison de la surcharge nulle de la sérialisation, vous pouvez utiliser std::variant
comme moyen de versionner vos classes ou de créer une belle répartition basée sur le polymorphisme, voici comment :
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
Et puis à la sérialisation elle-même :
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 façon dont la variante est sérialisée consiste à sérialiser son index (0 ou 1) en tant que std::byte
avant de sérialiser l'objet réel. C'est très efficace, mais parfois les utilisateurs peuvent vouloir choisir un identifiant de sérialisation explicite pour cela, reportez-vous au point ci-dessous
Pour définir un identifiant de sérialisation personnalisé, vous devez ajouter une ligne supplémentaire respectivement à l'intérieur/à l'extérieur de votre classe :
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>;
Notez que les identifiants de sérialisation des types dans la variante doivent correspondre en longueur, sinon une erreur de compilation se produira.
Vous pouvez également utiliser n'importe quelle séquence d'octets au lieu d'une chaîne lisible, ainsi qu'un entier ou tout type littéral. Voici un exemple de la façon d'utiliser le hachage d'une chaîne comme identifiant de sérialisation :
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
Vous pouvez également sérialiser uniquement les premiers octets du hachage, comme ceci :
// 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>;
Le type est ensuite converti en octets au moment de la compilation en utilisant (... attendez) zpp::bits::out
au moment de la compilation, donc tant que votre type littéral est sérialisable selon ce qui précède, vous pouvez l'utiliser comme un identifiant de sérialisation. L'identifiant est sérialisé en std::array<std::byte, N>
mais pour 1, 2, 4 et 8 octets, son type sous-jacent est std::byte
std::uint16_t
, std::uin32_t
et std::uint64_t
respectivement pour la facilité d’utilisation et l’efficacité.
Si vous souhaitez sérialiser la variante sans identifiant, ou si vous savez qu'une variante aura un identifiant particulier lors de la désérialisation, vous pouvez le faire en utilisant zpp::bits::known_id
pour envelopper votre 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));
Description des littéraux d'assistance dans la bibliothèque :
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 fonction doit être sans modèle et avoir exactement une surcharge : 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 ;
}
Lorsque votre fonction ne reçoit aucun paramètre, l'effet consiste simplement à appeler la fonction sans désérialisation et la valeur de retour est la valeur de retour de votre fonction. Lorsque la fonction renvoie void, il n'y a aucune valeur pour le type résultant.
La bibliothèque fournit également une interface RPC (appel de procédure à distance) fine pour permettre la sérialisation et la désérialisation des appels de fonction :
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);
Concernant la gestion des erreurs, comme dans de nombreux exemples ci-dessus, vous pouvez utiliser la valeur de retour, les exceptions ou zpp::throwing
pour gérer les erreurs.
// 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);
Il est possible que les identifiants des appels RPC soient ignorés, par exemple s'ils sont transmis hors bande, voici comment y parvenir :
server.serve(id); // id is already known, don't deserialize it.
client.request_body<Id>(arguments...); // request without serializing id.
Les fonctions membres peuvent également être enregistrées pour RPC, mais le serveur doit obtenir une référence à l'objet de classe lors de la construction, et toutes les fonctions membres doivent appartenir à la même classe (bien que les fonctions de portée d'espace de noms puissent être mélangées) :
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);
Le RPC peut également fonctionner en mode opaque et laisser la fonction elle-même sérialiser/désérialiser les données, lors de la liaison d'une fonction comme opaque, en utilisant 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>
>;
L'ordre des octets par défaut utilisé est celui du processeur/système d'exploitation natif sélectionné. Vous pouvez choisir un autre ordre d'octets en utilisant zpp::bits::endian
pendant la construction comme ceci :
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{});
Du côté de la réception (archive d'entrée), la bibliothèque prend en charge les types d'affichage de types d'octets const, tels que std::span<const std::byte>
afin d'obtenir une vue sur une partie des données sans copie. Cela doit être utilisé avec précaution car l'invalidation des itérateurs des données contenues pourrait entraîner une utilisation après la libération. Il est prévu pour permettre l'optimisation en cas de besoin :
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
}
Il existe également une version non dimensionnée, qui consomme le reste des données d'archive pour permettre le cas d'utilisation courant de l'en-tête puis d'une quantité arbitraire de données :
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 bibliothèque ne prend pas en charge la sérialisation des valeurs de pointeurs nuls, mais elle prend en charge explicitement les pointeurs propriétaires facultatifs, par exemple pour créer des graphiques et des structures complexes.
En théorie, il est valable d'utiliser std::optional<std::unique_ptr<T>>
, mais il est recommandé d'utiliser le zpp::bits::optional_ptr<T>
spécialement conçu qui optimise le booléen que l'objet facultatif conserve habituellement, et utilise le pointeur nul comme état invalide.
Dans ce cas, la sérialisation d'une valeur de pointeur nulle sérialisera un octet nul, tandis que les valeurs non nulles seront sérialisées sous la forme d'un seul octet suivi des octets de l'objet. (c'est-à-dire que la sérialisation est identique à std::optional<T>
).
Dans le cadre de l'implémentation de la bibliothèque, il était nécessaire d'implémenter certains types de réflexion, pour compter les membres et les membres visiteurs, et la bibliothèque les expose à l'utilisateur :
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);
L'exemple ci-dessus fonctionne avec ou sans ZPP_BITS_AUTODETECT_MEMBERS_MODE=1
, selon le #if
. Comme indiqué ci-dessus, nous devons nous appuyer sur une fonctionnalité spécifique du compilateur pour détecter le nombre de membres qui pourraient ne pas être portables.
Les archives peuvent être construites avec des options de contrôle supplémentaires telles que zpp::bits::append{}
qui demande aux archives de sortie de définir la position à la fin du vecteur ou d'une autre source de données. (pour les archives d'entrée, cette option n'a aucun effet)
std::vector<std::byte> data;
zpp::bits::out out (data, zpp::bits::append{});
Il est possible d'utiliser plusieurs contrôles et de les utiliser également avec 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{});
La taille d'allocation peut être limitée en cas d'archive de sortie à un tampon croissant ou lors de l'utilisation d'une archive d'entrée pour limiter la longueur d'un message préfixé d'une seule longueur afin d'éviter l'allocation d'un très grand tampon à l'avance, en utilisant zpp::bits::alloc_limit<L>{}
. L'utilisation prévue est pour des raisons de sécurité et de santé mentale plutôt que pour une mesure précise de l'allocation :
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 >{});
Pour une meilleure exactitude, lors de l'utilisation d'un tampon croissant pour la sortie, si le tampon a été agrandi, le tampon est finalement redimensionné pour la position exacte de l'archive de sortie, cela entraîne un redimensionnement supplémentaire qui est acceptable dans la plupart des cas, mais vous pouvez éviter cela redimensionnez davantage et reconnaissez la fin du tampon en utilisant position()
. Vous pouvez y parvenir en utilisant zpp::bits::no_fit_size{}
:
zpp::bits::out out (data, zpp::bits::no_fit_size{});
Pour contrôler l'agrandissement du vecteur d'archive de sortie, vous pouvez utiliser 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.
Par défaut, pour des raisons de sécurité, une archive de sortie qui utilise un tampon croissant vérifie le débordement avant toute augmentation du tampon. Pour les systèmes 64 bits, cette vérification, bien que peu coûteuse, est presque redondante, car il est presque impossible de dépasser un entier de 64 bits lorsqu'il représente une taille de mémoire. (c'est-à-dire que l'allocation de mémoire échouera avant que la mémoire ne soit sur le point de déborder de cet entier). Si vous souhaitez désactiver ces contrôles de débordement, en faveur des performances, utilisez : zpp::bits::no_enlarge_overflow{}
:
zpp::bits::out out (data, zpp::bits::no_enlarge_overflow{}); // Disable overflow check when enlarging.
Lors de la sérialisation explicite, il est souvent nécessaire d'identifier si l'archive est une archive d'entrée ou de sortie, et cela se fait via la fonction membre statique archive.kind()
, et peut être fait dans 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 bibliothèque fournit un type pour sérialiser et désérialiser des entiers de longueur 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;
Voici un exemple d'encodage au moment de la compilation :
static_assert (zpp::bits::to_bytes<zpp::bits::varint{ 150 }>() == "9601"_decode_hex);
Le modèle de classe zpp::bits::varint<T, E = varint_encoding::normal>
est fourni pour pouvoir définir n'importe quel type intégral varint ou type d'énumération, ainsi que les encodages possibles zpp::bits::varint_encoding::normal/zig_zag
(normal est la valeur par défaut).
Les déclarations d'alias suivantes sont fournies :
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.
L'utilisation de variantes pour sérialiser les tailles par défaut est également possible lors de la création de l'archive :
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.
Le format de sérialisation de cette bibliothèque n'est basé sur aucun format connu ou accepté. Naturellement, d'autres langages ne prennent pas en charge ce format, ce qui rend presque impossible l'utilisation de la bibliothèque pour la communication entre langages de programmation.
Pour cette raison, la bibliothèque prend en charge le format protobuf qui est disponible dans de nombreuses langues.
Veuillez noter que la prise en charge de protobuf est plutôt expérimentale, ce qui signifie qu'elle peut ne pas inclure toutes les fonctionnalités possibles de protobuf, et qu'elle est généralement plus lente (environ 2 à 5 fois plus lente, principalement lors de la désérialisation) que le format par défaut, qui vise à n'avoir aucune surcharge.
En commençant par le message de base :
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);
Pour la syntaxe complète, que nous utiliserons plus tard pour transmettre plus d'options, utilisez 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{}>;
Pour réserver des champs :
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
};
Pour spécifier explicitement pour chaque membre le numéro de champ :
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;
};
L'accès à la valeur derrière le champ est souvent transparent, mais si cela est explicitement nécessaire, utilisez pb_value(<variable>)
pour obtenir ou attribuer à la valeur.
Pour mapper des membres à un autre numéro de champ :
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.
};
Les membres fixes sont simplement des membres de données C++ normaux :
struct example
{
std:: uint32_t i; // fixed unsigned integer 32, field number == 1
};
Comme avec zpp::bits::members
, lorsque cela est requis, vous pouvez spécifier le nombre de membres dans le champ du protocole avec 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 version complète de ce qui précède implique de transmettre le nombre de membres comme deuxième paramètre au protocole :
struct example
{
using serialize = zpp::bits::protocol<zpp::bits::pb{}, 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
Les messages incorporés sont simplement imbriqués dans la classe en tant que données membres :
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);
Les champs répétés se présentent sous la forme de conteneurs propriétaires :
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
};
Actuellement, tous les champs sont facultatifs, ce qui est une bonne pratique : les champs manquants sont supprimés et ne sont pas concaténés au message, pour des raisons d'efficacité. Toute valeur qui n'est pas définie dans un message laisse le membre de données cible intact, ce qui permet d'implémenter les valeurs par défaut pour les membres de données à l'aide d'un initialiseur de membre de données non statique ou d'initialiser le membre de données avant de désérialiser le message.
Prenons un fichier .proto
complet et traduisons-le :
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 ;
}
Le fichier traduit :
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;
Dersérialiser un message initialement sérialisé avec 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
Le résultat que nous obtenons pour person
est :
name : "John Doe"
id : 1234
email : "[email protected]"
phones {
number : "555-4321"
type : home
}
Sérialisons-le :
person . SerializeToString ()
Le résultat est :
b' n x08 John Doe x10 xd2 t x1a x10 [email protected]" x0c n x08 555-4321 x10 x01 '
Retour au 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
Par défaut, zpp::bits
est intégré de manière agressive, mais pour réduire la taille du code, il n'intègre pas le décodage complet des variantes (entiers de longueur variable). Pour configurer l'intégration du décodage complet des variantes, définissez ZPP_BITS_INLINE_DECODE_VARINT=1
.
Si vous pensez que zpp::bits
s'intègre trop au point où cela affecte gravement la taille du code, vous pouvez définir ZPP_BITS_INLINE_MODE=0
, qui désactive toute insertion forcée et observe les résultats. Son effet est généralement négligeable, mais il est fourni tel quel pour un contrôle supplémentaire.
Dans certains compilateurs, vous pouvez trouver toujours des échecs en ligne avec des structures récursives (par exemple un graphique arborescent). Dans ces cas, il est nécessaire d'éviter d'une manière ou d'une autre l'attribut toujours en ligne pour la structure spécifique, un exemple trivial serait d'utiliser une fonction de sérialisation explicite, bien que la plupart du temps la bibliothèque détecte de telles occasions et que ce ne soit pas nécessaire, mais l'exemple est fourni simplement au cas où:
struct node
{
constexpr static auto serialize ( auto & archive, auto & node)
{
return archive (node. value , node. nodes );
}
int value;
std::vector<node> nodes;
};
bibliothèque | cas de test | taille du bac | taille des données | il est temps | des temps |
---|---|---|---|---|---|
zpp_bits | général | 52192B | 8413B | 733 ms | 693 ms |
zpp_bits | tampon fixe | 48000B | 8413B | 620 ms | 667 ms |
bitserie | général | 70904B | 6913B | 1470 ms | 1524 ms |
bitserie | tampon fixe | 53648B | 6913B | 927 ms | 1466 ms |
booster | général | 279024B | 11037B | 15126 ms | 12724 ms |
céréale | général | 70560B | 10413B | 10777 ms | 9088 ms |
tampons plats | général | 70640B | 14924B | 8757 ms | 3361 ms |
manuscrit | général | 47936B | 10413B | 1506 ms | 1577 ms |
manuscrit | dangereux | 47944B | 10413B | 1616 ms | 1392 ms |
iostream | général | 53872B | 8413B | 11956 ms | 12928 ms |
pack de messages | général | 89144B | 8857B | 2770 ms | 14033 ms |
protobuf | général | 2077864B | 10018B | 19929 ms | 20592 ms |
protobuf | arène | 2077872B | 10018B | 10319ms | 11787 ms |
ouais | général | 61072B | 10463B | 2286 ms | 1770 ms |
bibliothèque | cas de test | taille du bac | taille des données | il est temps | des temps |
---|---|---|---|---|---|
zpp_bits | général | 47128B | 8413B | 790 ms | 715 ms |
zpp_bits | tampon fixe | 43056B | 8413B | 605 ms | 694 ms |
bitserie | général | 53728B | 6913B | 2128 ms | 1832 ms |
bitserie | tampon fixe | 49248B | 6913B | 946 ms | 1941 ms |
booster | général | 237008B | 11037B | 16011ms | 13017 ms |
céréale | général | 61480B | 10413B | 9977 ms | 8565ms |
tampons plats | général | 62512B | 14924B | 9812ms | 3472 ms |
manuscrit | général | 43112B | 10413B | 1391 ms | 1321 ms |
manuscrit | dangereux | 43120B | 10413B | 1393 ms | 1212 ms |
iostream | général | 48632B | 8413B | 10992 ms | 12771 ms |
pack de messages | général | 77384B | 8857B | 3563 ms | 14705 ms |
protobuf | général | 2032712B | 10018B | 18125 ms | 20211ms |
protobuf | arène | 2032760B | 10018B | 9166 ms | 11378 ms |
ouais | général | 51000B | 10463B | 2114 ms | 1558 ms |
Je souhaite que vous trouviez cette bibliothèque utile. N'hésitez pas à soumettre tout problème, à faire des suggestions d'amélioration, etc.