Cista: A High-Performance Serialization and Reflection Library for C++

Overview

In modern software development, efficient processing and transmission of data are crucial. Cista is a high-performance, zero-copy serialization and reflection library designed for C++17, addressing the serialization and deserialization of complex data structures in a concise and efficient manner.

Simplistic and Efficient Design Philosophy

The design philosophy of Cista emphasizes simplicity and high performance. It is a single-header library with no external dependencies, meaning developers can easily integrate it into their projects. Additionally, Cista does not require preprocessor macros or source code generation mechanisms, making it more flexible and convenient to use. It directly uses native structures for data exchange without additional conversion steps, thereby minimizing processing overhead.

Powerful Features

Cista supports complex data structures, including cyclic references and recursive data structures. This means developers can easily handle nested objects without worrying about performance issues. It also provides a way to serialize data directly to the file system without intermediate buffers, which can save up to 50% of memory. Furthermore, Cista ensures the safety of data processing through continuous fuzz testing using LLVM’s LibFuzzer.

Code Examples

Basic Serialization and Deserialization

#include <iostream>
#include <vector>
#include "cista.h"

// Define a simple structure
struct Person {
    int age;
    std::string name;
    std::vector<std::string> hobbies;
};

// Register the structure for serialization
CISTA_DEFINE_STRUCT(Person, age, name, hobbies);

int main() {
    // Create a Person object
    Person person{30, "Alice", {"reading", "swimming", "coding"}};
    
    // Serialize to byte vector
    auto serialized = cista::serialize(person);
    
    // Deserialize
    auto deserialized = cista::deserialize<Person>(serialized);
    
    // Validate deserialization results
    std::cout << "Age: " << deserialized->age << std::endl;
    std::cout << "Name: " << deserialized->name << std::endl;
    std::cout << "Hobbies: ";
    for (const auto&& hobby : deserialized->hobbies) {
        std::cout << hobby << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

File Serialization

#include <iostream>
#include <fstream>
#include "cista.h"

struct Config {
    int port;
    std::string hostname;
    bool enable_logging;
    std::vector<std::string> allowed_users;
};

CISTA_DEFINE_STRUCT(Config, port, hostname, enable_logging, allowed_users);

int main() {
    // Create a configuration object
    Config config{8080, "example.com", true, {"user1", "user2", "admin"}};
    
    // Serialize to file
    {
        std::ofstream file("config.bin", std::ios::binary);
        cista::serialize_to(file, config);
    }
    
    // Deserialize from file
    Config loaded_config;
    {
        std::ifstream file("config.bin", std::ios::binary);
        loaded_config = cista::deserialize_from<Config>(file);
    }
    
    // Use the deserialized configuration
    std::cout << "Port: " << loaded_config.port << std::endl;
    std::cout << "Hostname: " << loaded_config.hostname << std::endl;
    std::cout << "Logging enabled: " << std::boolalpha << loaded_config.enable_logging << std::endl;
    std::cout << "Allowed users: ";
    for (const auto&& user : loaded_config.allowed_users) {
        std::cout << user << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

Zero-Copy Serialization

#include <iostream>
#include "cista.h"

// Using Cista's zero-copy feature
struct ZeroCopyData {
    cista::raw::string name;
    cista::raw::vector<int> values;
};

CISTA_DEFINE_STRUCT(ZeroCopyData, name, values);

int main() {
    // Create data
    ZeroCopyData data{cista::raw::string("Test"), {1, 2, 3, 4, 5}};
    
    // Serialize
    auto buf = cista::serialize(data);
    
    // Zero-copy deserialization - directly access data in the serialized buffer
    auto deserialized = cista::raw::deserialize<ZeroCopyData>(buf);
    
    // Note: No data copying here, directly accessing the raw buffer
    std::cout << "Name: " << deserialized->name << std::endl;
    std::cout << "Values: ";
    for (int value : deserialized->values) {
        std::cout << value << " ";
    }
    std::cout << std::endl;
    
    return 0;
}

Complex Data Structures

#include <iostream>
#include <map>
#include "cista.h"

// Define data containing nested structures
struct Address {
    cista::raw::string street;
    cista::raw::string city;
    int zip_code;
};

CISTA_DEFINE_STRUCT(Address, street, city, zip_code);

struct Employee {
    cista::raw::string name;
    int age;
    Address address;
    cista::raw::map<cista::raw::string, int> skills; // Skills and their proficiency
};

CISTA_DEFINE_STRUCT(Employee, name, age, address, skills);

int main() {
    // Create a complex object
    Address addr{"123 Main St", "Springfield", 12345};
    
    Employee emp{
        "John Doe", 
        35, 
        addr,
        {{"C++", 90}, {"Python", 80}, {"JavaScript", 70}}
    };
    
    // Serialize
    auto serialized = cista::serialize(emp);
    
    // Deserialize
    auto deserialized = cista::raw::deserialize<Employee>(serialized);
    
    // Access data
    std::cout << "Employee: " << deserialized->name << std::endl;
    std::cout << "Age: " << deserialized->age << std::endl;
    std::cout << "Address: " << deserialized->address.street << ", " 
              << deserialized->address.city << ", " 
              << deserialized->address.zip_code << std::endl;
    
    std::cout << "Skills:" << std::endl;
    for (const auto&& [skill, level] : deserialized->skills) {
        std::cout << "  " << skill << ": " << level << "%" << std::endl;
    }
    
    return 0;
}

Performance Optimization Example

#include <iostream>
#include <chrono>
#include <vector>
#include "cista.h"

// Performance testing structure
struct PerformanceData {
    int id;
    cista::raw::string name;
    double values[100]; // Fixed-size array for performance improvement
};

CISTA_DEFINE_STRUCT(PerformanceData, id, name, values);

// Test serialization performance
void performance_test() {
    const int num_objects = 10000;
    std::vector<PerformanceData> data_list;
    
    // Prepare test data
    for (int i = 0; i < num_objects; ++i) {
        PerformanceData data;
        data.id = i;
        data.name = cista::raw::string("Object_" + std::to_string(i));
        
        for (int j = 0; j < 100; ++j) {
            data.values[j] = i * 100 + j;
        }
        
        data_list.push_back(std::move(data));
    }
    
    // Measure serialization time
    auto start = std::chrono::high_resolution_clock::now();
    
    auto serialized = cista::serialize(data_list);
    
    auto end = std::chrono::high_resolution_clock::now();
    auto duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    std::cout << "Serialized " << num_objects << " objects in " 
              << duration.count() << " ms" << std::endl;
    std::cout << "Serialized size: " << serialized.size() << " bytes" << std::endl;
    
    // Measure deserialization time
    start = std::chrono::high_resolution_clock::now();
    
    auto deserialized = cista::raw::deserialize<std::vector<PerformanceData>>(serialized);
    
    end = std::chrono::high_resolution_clock::now();
    duration = std::chrono::duration_cast<std::chrono::milliseconds>(end - start);
    
    std::cout << "Deserialized " << num_objects << " objects in " 
              << duration.count() << " ms" << std::endl;
    
    // Verify data integrity
    if (deserialized->size() == num_objects) {
        std::cout << "Data integrity verified" << std::endl;
    } else {
        std::cout << "Data integrity check failed" << std::endl;
    }
}

int main() {
    performance_test();
    return 0;
}

Compilation and Usage

Cista is a single-header library that is very easy to use:

  1. Download the Cista header file:
wget https://raw.githubusercontent.com/felixguendling/cista/master/include/cista.h
  1. Include the header file in your code:
#include "cista.h"
  1. Ensure to use C++17 or higher standard during compilation:
g++ -std=c++17 -O3 -I. main.cpp -o main

Performance Advantages

The performance advantages of Cista are primarily reflected in the following aspects:

  1. Zero-Copy Serialization: Cista can operate directly on the serialized buffer, avoiding unnecessary data copying.
  2. Memory-Mapped Support: Cista supports serializing data directly to memory-mapped files, further enhancing I/O performance.
  3. Efficient Binary Format: Cista uses a compact binary format, reducing the size of serialized data.
  4. Optimized Data Layout: Cista automatically optimizes the memory layout of data structures, improving cache utilization.

Conclusion

Cista is a powerful and efficient C++ serialization library, particularly suitable for scenarios requiring high-performance data processing. Its zero-copy feature, simple API design, and excellent performance make it an ideal choice for C++ developers. Whether for simple configuration storage or complex data exchange needs, Cista provides outstanding solutions.

Through the above code examples, we can see the ease of use and powerful features of Cista. With just simple structure definitions and a few lines of code, efficient data serialization and deserialization can be achieved. For performance-driven C++ developers, Cista is definitely a library worth trying.

Leave a Comment