yaLanTingLibs

repository·main·Indexed 24 days ago

https://github.com/alibaba/yalantinglibs

A collection of high-performance, modern C++20 utility libraries. It includes tools for serialization (struct_pack, struct_json, struct_xml, struct_yaml), logging (easylog), and asynchronous networking/RPC (coro_rpc, coro_http, async_simple). The library provides reflection-based serialization and coroutine-based frameworks, with a C++17 fallback for serialization components.

Tokens
32K
Snippets
81
Records
129
Agent score
77%

What's inside yalantinglibs

  1. Introduction to struct_pack serialization

    main

    struct_pack is a high-performance C++ serialization library designed for zero-cost abstraction. It allows for the serialization and deserialization of aggregated structures (structs where all members are public and have no custom logic) in a single line of code without requiring DSLs, macros, or manual template definitions. It uses compile-time reflection and is optimized to outperform libraries like protobuf and msgpack. For non-aggregated structs, custom reflection can be implemented using macros.

    struct person {
      int64_t id;
      std::string name;
      int age;
      double salary;
    };
    
    person person1{.id = 1, .name = "hello struct pack", .age = 20, .salary = 1024.42};
  2. Overview of yalantinglibs components

    main

    yalantinglibs is a collection of high-performance C++20 libraries designed for modern asynchronous and structured programming. Key components include:

    • struct_pack: A high-speed serialization/deserialization library that can be 2-20x faster than protobuf with minimal code.
    • coro_rpc: A high-performance, coroutine-based RPC framework capable of over 20 million QPS in echo scenarios.
    • struct_json / struct_xml / struct_yaml: Reflection-based libraries for easy conversion between C++ structs and JSON, XML, or YAML formats.
    • coro_http: A C++20 coroutine-based HTTP(S) server and client supporting GET/POST, WebSockets, multipart file uploads, and chunked/range downloads.
    • easylog: A high-performance C++17 logging library supporting cout, sprintf, and fmt::format/std::format streams.
  3. How type hash checking works in `struct_pack`

    main

    To ensure type safety during deserialization, struct_pack performs compile-time type checking using a 32-bit MD5 hash.

    The process:

    1. A type tree is generated from the types via static reflection.
    2. The tree is recursively traversed at compile time to generate a unique type string.
    3. A 32-bit MD5 hash of this string is computed at compile time.
    4. This checksum is stored in the serialized data header.

    During deserialization, the checksum in the header is compared against the hash of the target type. If they do not match, struct_pack returns struct_pack::errc::invalid_argument.

    Compatibility and Hashing: Fields of type struct_pack::compatible<T> are explicitly ignored during the generation of the type string. This ensures that adding compatible fields does not change the hash code, maintaining compatibility between different versions of the same struct.

    Hash Conflicts: While a 32-bit MD5 collision is theoretically possible (probability $\approx 2^{-31}$), it is extremely rare. In debug mode, struct_pack includes full type strings and full checksums in the serialized data to detect and prevent issues caused by hash conflicts.

  4. How string constraints work in struct_pack

    main

    String-like types (e.g., std::string, std::string_view, std::wstring) are recognized via the string constraint.

    Requirements:

    • value_type must be a character type (char, wchar_t, char16_t, char32_t, char8_t, etc.).
    • Must provide size(), begin(), end(), length(), and data().
    • value_type must be a valid struct_pack type.

    Optimizations:

    • If the memory layout is contiguous, struct_pack uses memcpy optimization.
    • When deserializing to a string_view, struct_pack enables zero-copy optimization.
    template <typename Type>
    concept is_char_t = std::is_same_v<Type, signed char> ||
        std::is_same_v<Type, char> || std::is_same_v<Type, unsigned char> ||
        std::is_same_v<Type, wchar_t> || std::is_same_v<Type, char16_t> ||
        std::is_same_v<Type, char32_t> || std::is_same_v<Type, char8_t>;
    
    template <typename Type>
    concept string =  requires(Type container) {
      requires is_char_t<typename std::remove_cvref_t<Type>::value_type>;
      container.size();
      container.begin();
      container.end();
      container.length();
      container.data();
    };
  5. How array constraints work in struct_pack

    main

    Fixed-length arrays (e.g., C built-in arrays or std::array) are recognized via the array constraint.

    Requirements:

    • Must be a C built-in array type OR have a size() member function that specializes std::tuple_size.
    • All elements must be valid struct_pack types.

    Optimization: If the memory layout is contiguous, struct_pack uses memcpy optimization.

    template <typename Type>
    concept array = std::is_array_v<T> || requires(Type arr) {
      arr.size();
      std::tuple_size<std::remove_cvref_t<Type>>{};
    };
  6. Coroutine RPC Functions: Usage and Benefits

    main

    A Coroutine RPC Function is a function that returns an async_simple::coro::Lazy<T>.

    Execution Model: When a coroutine function is called, the server starts a new coroutine on the connection's I/O thread. If the function yields (e.g., via co_await), the I/O thread is freed to handle other requests or connections. This significantly improves concurrency for I/O-bound or heavy tasks.

    Best Practice: Use coroutines to offload heavy computations to a thread pool using coro_io::post or async_simple::Promise to avoid blocking the I/O thread.

    using namespace async_simple::coro;
    
    // Offloading heavy work to a thread pool to avoid blocking I/O thread
    int heavy_calculate(int value);
    Lazy<int> calculate(int value) {
      auto val = co_await coro_io::post([value](){return heavy_calculate(value);});
      co_return val;
    }
  7. Support for complex and nested structures

    main

    struct_pack natively supports complex nested structures, including standard library containers and smart pointers. This allows for deep serialization of objects containing:

    • STL Containers: std::vector, std::list, std::deque, std::set, std::multiset, std::map, std::multimap, std::unordered_map, std::unordered_multimap, std::array.
    • Pairs and Arrays: std::pair, C-style arrays.
    • Smart Pointers: std::unique_ptr.
    • Optionality: std::optional.
    • Enums: enum class.
    enum class Color { red, black, white };
    
    struct complicated_object {
      Color color;
      int a;
      std::string b;
      std::vector<person> c;
      std::list<std::string> d;
      std::deque<int> e;
      std::map<int, person> f;
      std::multimap<int, person> g;
      std::set<std::string> h;
      std::multiset<int> i;
      std::unordered_map<int, person> j;
      std::unordered_multimap<int, int> k;
      std::array<person, 2> m;
      person n[2];
      std::pair<std::string, person> o;
      std::optional<int> p;
      std::unique_ptr<int> q;
    };
    
    struct nested_object {
      int id;
      std::string name;
      person p;
      complicated_object o;
    };
    
    nested_object nested{.id = 2, .name = "tom", .p = {20, "tom"}, .o = {}};
    auto buffer = struct_pack::serialize(nested);
    auto nested2 = struct_pack::deserialize<nested_object>(buffer.data(), buffer.size());
  8. Maintain RPC ABI compatibility with struct_pack::compatible

    main

    To ensure forward and backward compatibility when changing RPC parameters or return values, use struct_pack::compatible<T, VERSION_NUMBER>. This behaves similarly to std::optional<T>; if an older client does not provide the field, the server receives an empty value.

    Key Rules:

    • For structs: Add struct_pack::compatible<T> fields.
    • For non-struct parameters/returns: Add new parameters or return values using compatible<T> fields.
    • For void return types: Upgrade void to std::tuple<std::monostate, ...> to maintain compatibility with older clients expecting a return value.
    // server side
    int client_oldapi_server_newapi(int a, struct_pack::compatible<int> b) {
      return a + b.value_or(1);
    }
    
    // Upgrading void to support compatibility
    std::tuple<std::monostate,struct_pack::compatible<int>> client_oldapi_server_newapi_ret_void() {
        return {std::monostate{},1};
    }