Serialisasi biner C++20 modern dan pustaka RPC, hanya dengan satu file header.
Pustaka ini adalah penerus zpp::serializer. Perpustakaan mencoba untuk lebih sederhana untuk digunakan, tetapi memiliki API yang kurang lebih mirip dengan pendahulunya.
constexpr
zpp::bits
lebih mudah diketik daripada zpp::serializer
.zpp::serializer
dengan id serialisasi sha1 tetap 8 byte. Untuk banyak jenis, mengaktifkan serialisasi bersifat transparan dan tidak memerlukan baris kode tambahan. Tipe ini harus bertipe agregat, dengan anggota non array. Berikut adalah contoh kelas person
dengan nama dan umur:
struct person
{
std::string name;
int age{};
};
Contoh cara membuat serialisasi seseorang ke dalam dan dari vektor byte:
// 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);
Contoh ini hampir berhasil, kita diperingatkan bahwa kita membuang nilai kembalian. Untuk pemeriksaan kesalahan, teruslah membaca.
Kita perlu memeriksa kesalahan, perpustakaan menawarkan beberapa cara untuk melakukannya - berbasis nilai pengembalian, berbasis pengecualian, atau berbasis zpp::lemparan.
Cara berdasarkan nilai pengembalian untuk menjadi paling eksplisit, atau jika Anda lebih suka nilai pengembalian:
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.
}
Cara berbasis pengecualian menggunakan .or_throw()
(baca ini sebagai "succeed or throw" - karenanya 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 ;
});
}
Opsi lainnya adalah zpp::throwing di mana pemeriksaan kesalahan berubah menjadi dua co_await
sederhana, untuk memahami cara memeriksa kesalahan kami menyediakan fungsi main lengkap:
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 ;
});
}
Semua metode di atas, gunakan kode kesalahan berikut secara internal dan dapat diperiksa menggunakan operator perbandingan dari berdasarkan nilai pengembalian, atau dengan memeriksa kode kesalahan internal std::system_error
atau zpp::throwing
tergantung mana yang Anda gunakan:
std::errc::result_out_of_range
- mencoba menulis atau membaca dari buffer yang terlalu pendek.std::errc::no_buffer_space
- buffer yang bertambah akan melampaui batas alokasi atau meluap.std::errc::value_too_large
- pengkodean varint (integer panjang variabel) berada di luar batas representasi.std::errc::message_size
- ukuran pesan melampaui batas alokasi yang ditentukan pengguna.std::errc::not_supported
- mencoba memanggil RPC yang tidak terdaftar sebagai didukung.std::errc::bad_message
- mencoba membaca varian tipe yang tidak dikenal.std::errc::invalid_argument
- mencoba membuat serialisasi penunjuk nol atau varian tanpa nilai.std::errc::protocol_error
- mencoba membatalkan serialisasi pesan protokol yang tidak valid. Untuk sebagian besar tipe non-agregat (atau tipe agregat dengan anggota array), mengaktifkan serialisasi adalah hal yang mudah. Berikut adalah contoh kelas person
non-agregat:
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{};
};
Seringkali tipe yang kami buat bersambung dapat bekerja dengan pengikatan terstruktur, dan perpustakaan ini memanfaatkannya, namun Anda perlu memberikan jumlah anggota di kelas Anda agar ini dapat berfungsi menggunakan metode di atas.
Ini juga berfungsi dengan pencarian yang bergantung pada argumen, memungkinkan untuk tidak mengubah kelas sumber:
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
Di beberapa kompiler, SFINAE bekerja dengan requires expression
di bawah if constexpr
dan unevaluated lambda expression
. Artinya bahkan dengan tipe non agregat, jumlah anggota dapat dideteksi secara otomatis jika semua anggota berada dalam struct yang sama. Untuk ikut serta, tentukan 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{};
};
Ini berfungsi dengan clang 13
, namun portabilitasnya tidak jelas, karena di gcc
tidak berfungsi (ini adalah kesalahan besar) dan secara eksplisit menyatakan dalam standar bahwa ada niat untuk tidak mengizinkan SFINAE dalam kasus serupa, jadi ini dinonaktifkan secara default.
Jika anggota data atau konstruktor default Anda bersifat pribadi, Anda harus berteman dengan zpp::bits::access
seperti:
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{};
};
Untuk mengaktifkan penyimpanan & pemuatan objek apa pun menggunakan serialisasi eksplisit, yang berfungsi terlepas dari kompatibilitas pengikatan terstruktur, tambahkan baris berikut ke kelas Anda:
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. object_1 , self. object_2 , ...);
}
Perhatikan bahwa object_1, object_2, ...
adalah anggota data non-statis kelas Anda.
Berikut adalah contoh kelas person dengan fungsi serialisasi eksplisit:
struct person
{
constexpr static auto serialize ( auto & archive, auto & self)
{
return archive (self. name , self. age );
}
std::string name;
int age{};
};
Atau dengan pencarian yang bergantung pada argumen:
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
Membuat arsip input dan output secara bersamaan dan terpisah dari data:
// 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();
Arsip dapat dibuat dari salah satu jenis byte:
// 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);
Anda juga dapat menggunakan objek data berukuran tetap seperti array, std::array
dan tipe tampilan seperti std::span
yang mirip dengan di atas. Anda hanya perlu memastikan ukurannya cukup karena ukurannya tidak dapat diubah:
// 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);
Saat menggunakan vektor atau string, secara otomatis bertambah ke ukuran yang diinginkan, namun dengan hal di atas, data dibatasi oleh batas array atau rentang.
Saat membuat arsip dengan salah satu cara di atas, dimungkinkan untuk meneruskan sejumlah parameter variadik yang mengontrol perilaku arsip, seperti urutan byte, tipe ukuran default, menentukan perilaku penambahan, dan sebagainya. Hal ini dibahas di sisa README.
Seperti yang dikatakan di atas, perpustakaan hampir sepenuhnya constexpr, berikut adalah contoh penggunaan array sebagai objek data tetapi juga menggunakannya pada waktu kompilasi untuk membuat serialisasi dan deserialisasi tuple bilangan bulat:
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 });
Untuk kenyamanan, perpustakaan juga menyediakan beberapa fungsi serialisasi yang disederhanakan untuk waktu kompilasi:
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 });
Kueri posisi in
dan out
menggunakan position()
, dengan kata lain byte yang dibaca dan ditulis masing-masing:
std:: size_t bytes_read = in.position();
std:: size_t bytes_written = out.position();
Atur ulang posisi ke belakang atau ke depan, atau ke awal, gunakan dengan sangat hati-hati:
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.
Saat membuat serial tipe pustaka standar dengan panjang variabel, seperti vektor, string, dan tipe tampilan seperti tampilan span dan string, pustaka pertama-tama menyimpan bilangan bulat 4 byte yang mewakili ukuran, diikuti oleh elemen.
std::vector v = { 1 , 2 , 3 , 4 };
out (v);
in (v);
Alasan mengapa tipe ukuran default adalah 4 byte (yaitu std::uint32_t
) adalah untuk portabilitas antara arsitektur yang berbeda, serta sebagian besar program hampir tidak pernah mencapai kasus wadah yang lebih dari 2^32 item, dan mungkin saja tidak adil untuk membayar harga ukuran 8 byte secara default.
Untuk tipe ukuran tertentu yang bukan 4 byte, gunakan zpp::bits::sized
/ zpp::bits::sized_t
seperti:
// 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);
Pastikan tipe ukuran cukup besar untuk objek yang diserialkan, jika tidak, lebih sedikit item yang akan diserialkan, sesuai dengan aturan konversi tipe yang tidak ditandatangani.
Anda juga dapat memilih untuk tidak membuat serialisasi ukuran sama sekali, seperti:
// 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);
Untuk hal yang umum, ada deklarasi alias untuk versi tipe berukuran / tidak berukuran, misalnya di sini adalah vector
dan span
, yang lain seperti string
, string_view
, dll menggunakan pola yang sama.
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.
Serialisasi tipe ukuran tetap seperti array, std::array
s, std::tuple
s tidak menyertakan overhead apa pun kecuali elemen yang diikuti satu sama lain.
Mengubah jenis ukuran default untuk seluruh arsip dimungkinkan selama pembuatan:
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{});
Sebagian besar tipe perpustakaan mengetahui cara mengoptimalkan dan membuat serial objek sebagai byte. Namun ini dinonaktifkan saat menggunakan fungsi serialisasi eksplisit.
Jika Anda tahu tipe Anda dapat diserialkan seperti halnya byte mentah, dan Anda menggunakan serialisasi eksplisit, Anda dapat ikut serta dan mengoptimalkan serialisasinya menjadi memcpy
belaka:
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));
}
};
Hal ini juga memungkinkan untuk melakukan hal ini secara langsung dari suatu vektor atau span dari tipe-tipe yang dapat disalin dengan mudah, kali ini kita menggunakan bytes
dan bukan as_bytes
karena kita mengkonversikan isi dari vektor menjadi byte daripada objek vektor itu sendiri (data yang ditunjuk oleh vektor, bukan objek vektor):
std::vector<point> points;
out (zpp::bits::bytes(points));
in (zpp::bits::bytes(points));
Namun dalam hal ini ukurannya tidak diserialkan, hal ini dapat diperluas di masa depan untuk juga mendukung serialisasi ukuran yang mirip dengan tipe tampilan lainnya. Jika Anda perlu membuat serialisasi sebagai byte dan menginginkan ukurannya, sebagai solusinya, Anda dapat melakukan cast ke std::span<std::byte>
.
Meskipun tidak ada alat yang sempurna untuk menangani kompatibilitas mundur struktur karena serialisasi tidak memiliki overhead, Anda dapat menggunakan std::variant
sebagai cara untuk membuat versi kelas Anda atau membuat pengiriman berbasis polimorfisme yang bagus, berikut caranya:
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
Dan kemudian ke serialisasi itu sendiri:
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);
Cara varian diserialisasikan adalah dengan membuat serialisasi indeksnya (0 atau 1) sebagai std::byte
sebelum membuat serialisasi objek sebenarnya. Ini sangat efisien, namun terkadang pengguna mungkin ingin memilih id serialisasi eksplisit untuk itu, lihat poin di bawah
Untuk menyetel id serialisasi khusus, Anda perlu menambahkan baris tambahan di dalam/di luar kelas Anda masing-masing:
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>;
Perhatikan bahwa id serialisasi tipe dalam varian harus sama panjangnya, atau kesalahan kompilasi akan terjadi.
Anda juga dapat menggunakan urutan byte apa pun alih-alih string yang dapat dibaca, serta bilangan bulat atau tipe literal apa pun. Berikut adalah contoh cara menggunakan hash string sebagai id serialisasi:
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
Anda juga dapat membuat serial byte pertama dari hash, seperti:
// 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>;
Tipe tersebut kemudian diubah menjadi byte pada waktu kompilasi menggunakan (... tunggu dulu) zpp::bits::out
pada waktu kompilasi, jadi selama tipe literal Anda dapat diserialkan sesuai dengan yang di atas, Anda dapat menggunakannya sebagai id serialisasi. Id diserialkan ke std::array<std::byte, N>
namun untuk 1, 2, 4, dan 8 byte tipe dasarnya adalah std::byte
std::uint16_t
, std::uin32_t
dan std::uint64_t
masing-masing untuk kemudahan penggunaan dan efisiensi.
Jika Anda ingin membuat serialisasi varian tanpa id, atau jika Anda mengetahui bahwa suatu varian akan memiliki ID tertentu setelah deserialize, Anda dapat melakukannya menggunakan zpp::bits::known_id
untuk menggabungkan varian Anda:
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));
Deskripsi literal pembantu di perpustakaan:
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
, fungsi tersebut harus non-templat dan memiliki satu kelebihan beban: 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 ;
}
Ketika fungsi Anda tidak menerima parameter, efeknya hanya memanggil fungsi tanpa deserialisasi dan nilai yang dikembalikan adalah nilai kembalian dari fungsi Anda. Ketika fungsi mengembalikan batal, tidak ada nilai untuk tipe yang dihasilkan.
Pustaka ini juga menyediakan antarmuka RPC (panggilan prosedur jarak jauh) yang tipis untuk memungkinkan pemanggilan fungsi serialisasi dan deserialisasi:
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);
Mengenai penanganan kesalahan, mirip dengan banyak contoh di atas, Anda dapat menggunakan nilai pengembalian, pengecualian, atau cara zpp::throwing
untuk menangani kesalahan.
// 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);
ID panggilan RPC mungkin saja dilewati, misalnya ID tersebut dilewatkan di luar band, berikut cara melakukannya:
server.serve(id); // id is already known, don't deserialize it.
client.request_body<Id>(arguments...); // request without serializing id.
Fungsi anggota juga dapat didaftarkan untuk RPC, namun server perlu mendapatkan referensi ke objek kelas selama konstruksi, dan semua fungsi anggota harus berasal dari kelas yang sama (walaupun fungsi cakupan namespace boleh digabungkan):
struct a
{
int foo ( int i, std::string s);
};
std::string bar ( int i, int j);
using rpc = zpp::bits::rpc<
zpp::bits::bind<&a::foo, " a::foo " _sha256_int>,
zpp::bits::bind<bar, " bar " _sha256_int>
>;
auto [data, in, out] = zpp::bits::data_in_out();
// Our object.
a a1;
// Server and client together:
auto [client, server] = rpc::client_server(in, out, a1);
// Or separately:
rpc::client client{in, out};
rpc::server server{in, out, a1};
// Request from the client:
client.request< " a::foo " _sha256_int>( 1337 , " hello " s).or_throw();
// Serve the request from the server:
server.serve().or_throw();
// Read back the response
client.response< " a::foo " _sha256_int>().or_throw(); // == a1.foo(1337, "hello"s);
RPC juga dapat bekerja dalam mode buram dan membiarkan fungsi itu sendiri membuat serialisasi/deserialisasi data, ketika mengikat suatu fungsi sebagai buram, menggunakan 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>
>;
Urutan byte default yang digunakan adalah prosesor/OS asli yang dipilih. Anda dapat memilih urutan byte lain menggunakan zpp::bits::endian
selama konstruksi seperti:
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{});
Di pihak penerima (arsip masukan), perpustakaan mendukung tipe tampilan tipe const byte, seperti std::span<const std::byte>
untuk mendapatkan tampilan pada sebagian data tanpa menyalin. Ini perlu digunakan dengan hati-hati karena membatalkan iterator dari data yang ada dapat menyebabkan penggunaan setelah gratis. Ini disediakan untuk memungkinkan pengoptimalan bila diperlukan:
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
}
Ada juga versi tidak berukuran, yang menggunakan sisa data arsip untuk memungkinkan kasus penggunaan umum header dan jumlah data yang berubah-ubah:
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
Pustaka tidak mendukung serialisasi nilai penunjuk nol, namun secara eksplisit mendukung penunjuk kepemilikan opsional, seperti untuk membuat grafik dan struktur kompleks.
Secara teori, penggunaan std::optional<std::unique_ptr<T>>
valid, tetapi disarankan untuk menggunakan zpp::bits::optional_ptr<T>
yang dibuat khusus yang mengoptimalkan boolean yang biasanya disimpan oleh objek opsional, dan menggunakan penunjuk nol sebagai status tidak valid.
Membuat serial nilai penunjuk nol dalam hal ini akan membuat serialisasi byte nol, sedangkan nilai non-null diserialkan sebagai satu byte tunggal diikuti oleh byte objek. (yaitu, serialisasi identik dengan std::optional<T>
).
Sebagai bagian dari implementasi perpustakaan, diperlukan penerapan beberapa tipe refleksi, untuk menghitung anggota dan anggota yang berkunjung, dan perpustakaan memaparkannya kepada pengguna:
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);
Contoh di atas berfungsi dengan atau tanpa ZPP_BITS_AUTODETECT_MEMBERS_MODE=1
, bergantung pada #if
. Seperti disebutkan di atas, kita harus mengandalkan fitur kompiler tertentu untuk mendeteksi jumlah anggota yang mungkin tidak portabel.
Arsip bisa dibuat dengan opsi kontrol tambahan seperti zpp::bits::append{}
yang memerintahkan arsip keluaran untuk menyetel posisi ke akhir vektor atau sumber data lainnya. (untuk arsip masukan opsi ini tidak berpengaruh)
std::vector<std::byte> data;
zpp::bits::out out (data, zpp::bits::append{});
Dimungkinkan untuk menggunakan beberapa kontrol dan menggunakannya juga dengan 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{});
Ukuran alokasi dapat dibatasi jika arsip keluaran ke buffer yang bertambah atau saat menggunakan arsip masukan untuk membatasi berapa panjang satu pesan dengan awalan panjang untuk menghindari alokasi buffer yang sangat besar terlebih dahulu, menggunakan zpp::bits::alloc_limit<L>{}
. Tujuan penggunaan adalah untuk alasan keselamatan dan kewarasan daripada pengukuran alokasi yang akurat:
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 >{});
Untuk kebenaran yang terbaik, ketika menggunakan buffer yang bertambah untuk keluaran, jika buffer itu bertambah, buffer akan diubah ukurannya pada akhirnya untuk posisi yang tepat dari arsip keluaran, ini menimbulkan perubahan ukuran tambahan yang dalam banyak kasus dapat diterima, tetapi Anda dapat menghindari ini ubah ukuran tambahan dan kenali akhir buffer dengan menggunakan position()
. Anda bisa mencapainya dengan menggunakan zpp::bits::no_fit_size{}
:
zpp::bits::out out (data, zpp::bits::no_fit_size{});
Untuk mengontrol pembesaran vektor arsip keluaran, Anda dapat menggunakan 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.
Secara default, demi keamanan, arsip keluaran yang menggunakan buffer yang bertambah, memeriksa overflow sebelum buffer bertambah. Untuk sistem 64 bit, pemeriksaan ini meskipun murah, hampir mubazir, karena hampir tidak mungkin untuk meluapkan bilangan bulat 64 bit ketika ini mewakili ukuran memori. (yaitu, alokasi memori akan gagal sebelum memori hampir melebihi bilangan bulat ini). Jika Anda ingin menonaktifkan pemeriksaan overflow tersebut, demi kinerja, gunakan: zpp::bits::no_enlarge_overflow{}
:
zpp::bits::out out (data, zpp::bits::no_enlarge_overflow{}); // Disable overflow check when enlarging.
Saat membuat serial secara eksplisit, sering kali diperlukan untuk mengidentifikasi apakah arsip tersebut merupakan arsip input atau output, dan ini dilakukan melalui fungsi anggota statis archive.kind()
, dan dapat dilakukan dalam 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)
}
}
Pustaka menyediakan tipe untuk membuat serialisasi dan deserialisasi bilangan bulat dengan panjang variabel:
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;
Berikut adalah contoh pengkodean pada waktu kompilasi:
static_assert (zpp::bits::to_bytes<zpp::bits::varint{ 150 }>() == "9601"_decode_hex);
Templat kelas zpp::bits::varint<T, E = varint_encoding::normal>
disediakan untuk dapat mendefinisikan tipe integral varint atau tipe enumerasi apa pun, bersama dengan kemungkinan pengkodean zpp::bits::varint_encoding::normal/zig_zag
(normal adalah defaultnya).
Deklarasi alias berikut disediakan:
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.
Menggunakan varian untuk membuat serial ukuran secara default juga dimungkinkan selama pembuatan arsip:
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.
Format serialisasi perpustakaan ini tidak didasarkan pada format apa pun yang dikenal atau diterima. Tentu saja, bahasa lain tidak mendukung format ini, sehingga hampir tidak mungkin menggunakan perpustakaan untuk komunikasi lintas bahasa pemrograman.
Oleh karena itu perpustakaan mendukung format protobuf yang tersedia dalam banyak bahasa.
Perlu diketahui bahwa dukungan protobuf bersifat eksperimental, yang berarti dukungan tersebut mungkin tidak menyertakan semua fitur protobuf yang ada, dan umumnya lebih lambat (sekitar 2-5 kali lebih lambat, sebagian besar pada deserialisasi) dibandingkan format default, yang bertujuan untuk menghilangkan overhead.
Dimulai dengan pesan dasar:
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);
Untuk sintaks lengkapnya, yang nantinya akan kita gunakan untuk memberikan lebih banyak opsi, gunakan 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{}>;
Untuk memesan bidang:
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
};
Untuk secara eksplisit menentukan nomor bidang untuk setiap anggota:
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;
};
Mengakses nilai di belakang bidang sering kali transparan namun jika diperlukan secara eksplisit, gunakan pb_value(<variable>)
untuk mendapatkan atau menetapkan nilai.
Untuk memetakan anggota ke nomor bidang lain:
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.
};
Anggota tetap hanyalah anggota data C++ biasa:
struct example
{
std:: uint32_t i; // fixed unsigned integer 32, field number == 1
};
Seperti halnya zpp::bits::members
, jika diperlukan, Anda dapat menentukan jumlah anggota di bidang protokol dengan zpp::bits::pb_members<N>
:
struct example
{
using serialize = zpp::bits::pb_members< 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
Versi lengkap di atas melibatkan meneruskan jumlah anggota sebagai parameter kedua ke protokol:
struct example
{
using serialize = zpp::bits::protocol<zpp::bits::pb{}, 1 >; // 1 member.
zpp::bits:: vint32_t i; // field number == 1
};
Pesan yang tersemat hanya disarangkan di dalam kelas sebagai anggota data:
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);
Bidang berulang adalah bentuk kepemilikan wadah:
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
};
Saat ini semua kolom bersifat opsional, yang merupakan praktik yang baik, kolom yang hilang akan dihapus dan tidak digabungkan ke pesan, demi efisiensi. Nilai apa pun yang tidak disetel dalam pesan akan membiarkan anggota data target tetap utuh, sehingga memungkinkan penerapan default untuk anggota data dengan menggunakan penginisialisasi anggota data non-statis atau menginisialisasi anggota data sebelum melakukan deserialisasi pesan.
Mari kita ambil file .proto
lengkap dan terjemahkan:
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 ;
}
File yang diterjemahkan:
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;
Melakukan deserialisasi pesan yang awalnya diserialkan dengan 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
Output yang kami dapatkan untuk person
adalah:
name : "John Doe"
id : 1234
email : "[email protected]"
phones {
number : "555-4321"
type : home
}
Mari membuat serialisasinya:
person . SerializeToString ()
Hasilnya adalah:
b' n x08 John Doe x10 xd2 t x1a x10 [email protected]" x0c n x08 555-4321 x10 x01 '
Kembali ke 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
Secara default, zpp::bits
disejajarkan secara agresif, tetapi untuk mengurangi ukuran kode, zpp::bits tidak menyejajarkan decoding penuh varian (bilangan bulat dengan panjang variabel). Untuk mengonfigurasi penyebarisan decoding varian lengkap, tentukan ZPP_BITS_INLINE_DECODE_VARINT=1
.
Jika Anda mencurigai bahwa zpp::bits
terlalu banyak memasukkan ke titik yang berdampak buruk pada ukuran kode, Anda dapat menentukan ZPP_BITS_INLINE_MODE=0
, yang menonaktifkan semua force inlining dan mengamati hasilnya. Biasanya efeknya dapat diabaikan, namun diberikan sebagaimana adanya untuk kontrol tambahan.
Di beberapa kompiler, Anda mungkin selalu menemukan inline gagal dengan struktur rekursif (misalnya grafik pohon). Dalam kasus ini diperlukan untuk menghindari atribut selalu inline untuk struktur tertentu, contoh sepele adalah dengan menggunakan fungsi serialisasi eksplisit, meskipun sering kali perpustakaan mendeteksi kejadian seperti itu dan itu tidak diperlukan, tetapi contoh diberikan hanya jika:
struct node
{
constexpr static auto serialize ( auto & archive, auto & node)
{
return archive (node. value , node. nodes );
}
int value;
std::vector<node> nodes;
};
perpustakaan | kasus uji | ukuran tempat sampah | ukuran data | waktu | waktu itu |
---|---|---|---|---|---|
zpp_bits | umum | 52192B | 8413B | 733ms | 693 md |
zpp_bits | penyangga tetap | 48000B | 8413B | 620 md | 667 md |
bitsery | umum | 70904B | 6913B | 1470 md | 1524ms |
bitsery | penyangga tetap | 53648B | 6913B | 927ms | 1466ms |
mendorong | umum | 279024B | 11037B | 15126ms | 12724ms |
sereal | umum | 70560B | 10413B | 10777ms | 9088ms |
flatbuffer | umum | 70640B | 14924B | 8757ms | 3361 md |
tulisan tangan | umum | 47936B | 10413B | 1506ms | 1577ms |
tulisan tangan | tidak aman | 47944B | 10413B | 1616ms | 1392 ms |
iostream | umum | 53872B | 8413B | 11956ms | 12928ms |
paket pesan | umum | 89144B | 8857B | 2770 md | 14033ms |
protobuf | umum | 2077864B | 10018B | 19929ms | 20592ms |
protobuf | arena | 2077872B | 10018B | 10319ms | 11787ms |
ya | umum | 61072B | 10463B | 2286ms | 1770 md |
perpustakaan | kasus uji | ukuran tempat sampah | ukuran data | waktu | waktu itu |
---|---|---|---|---|---|
zpp_bits | umum | 47128B | 8413B | 790 md | 715ms |
zpp_bits | penyangga tetap | 43056B | 8413B | 605 md | 694 md |
bitsery | umum | 53728B | 6913B | 2128ms | 1832ms |
bitsery | penyangga tetap | 49248B | 6913B | 946 md | 1941 ms |
mendorong | umum | 237008B | 11037B | 16011ms | 13017ms |
sereal | umum | 61480B | 10413B | 9977ms | 8565ms |
buffer datar | umum | 62512B | 14924B | 9812ms | 3472 ms |
tulisan tangan | umum | 43112B | 10413B | 1391 md | 1321 md |
tulisan tangan | tidak aman | 43120B | 10413B | 1393ms | 1212ms |
iostream | umum | 48632B | 8413B | 10992ms | 12771ms |
paket pesan | umum | 77384B | 8857B | 3563ms | 14705ms |
protobuf | umum | 2032712B | 10018B | 18125ms | 20211ms |
protobuf | arena | 2032760B | 10018B | 9166ms | 11378ms |
ya | umum | 51000B | 10463B | 2114ms | 1558ms |
Saya berharap perpustakaan ini bermanfaat bagi Anda. Silakan menyampaikan masalah apa pun, memberikan saran untuk perbaikan, dll.