Skip to content
/ pupene Public

A non-invasive, multi-format serialization library for C++17.

License

Notifications You must be signed in to change notification settings

junkdog/pupene

Repository files navigation

pupene

A non-invasive, multi-format serialization library for C++17.

Features or Caveats

This library was primarily conceived as a foundation for assisting with building tooling for gamedev projects. The approach and naming conventions are borrowed from Daniel Randle's Pupping - a method for serializing data.

  • Works with default constructible types.
    • pupene relies on direct field access to read/write.
  • Concise and simple:
    • pup() - one serializer-agnostic read/write function per type.
    • Pupper - serializer, binary and optional JSON support included.
    • Serialization format can not be customized, this makes it non-ideal for dealing with third-party formats.
    • Zero dependencies, unless built with JSON support.
  • Not limited to performing only pure serialization.

Getting started

Add this conan remote:

conan remote add junkdog https://api.bintray.com/conan/junkdog/conan

In your project's conanfile.txt/py:

[requires]
pupene/0.2.0@junkdog/stable

Links

A brief introduction

At the core, pupene is built around serializers called puppers, and pup functions - a combined read/write function per serializable type.

Include <pupene/binary.h> and/or <pupene/json.h> (requires -DPUPENE_BUILD_JSON=true) to access the appropriate puppers.

Refer to the API docs for more detailed documentation. The tests may be of some interest too.

Namespace pupene is omitted from example code, in the interest of brevity.

Pupper

Pupper is on the receiving end of pup; they only deal directly with values once pup has flattened the object into a sequence of integer, decimal and string types.

Included Puppers, and related convenience functions:

  • JsonReader: from_json()
  • JsonWriter: to_json()
  • BinaryReader: from_binary()
  • BinaryWriter: to_binary()
  • DebugPupper: to_debug()

A pup function must exist for each serializable type.

pup

An object is serializable when there's a matching pup function; these describe the structure of the type by enumerating its fields. All pup functions must be defined in the pupene::fns namespace.

A limited number of fundamental types are built-in:

  • Integer and decimal types
  • std::string
  • STL containers, such as std::vector and std::map

pup() for a type FooBar has the following signature:

template <typename P>
void pup(Pupper<P>& p,         // Pupper-agnostic - works with all  
         FooBar& value,        // must be reference
         const Meta& meta);    // name and Meta::Type

N.B. For function resolution to work, pup functions must be declared in a file which does not #include any puppers, implicitly or explicitly.

Example

You have some models:

struct Color {
    float r, g, b, a;
};

struct FontReference {
    std::string path;             
    Color inner;
    Color outer;
};

To make the types eligible, each requires its own pup function in the namespace pupene:fns.

#include <pupene/pup.h>

namespace pupene::fns {               // required namespace

    template<typename P>
    void pup(Pupper<P>& p,            // the serializer in use 
             Color& color,            // always as non-const references 
             const Meta& meta) {      // metadata for color 
        
        // idiomatic boilerplate     
        pup_object(p, color, meta, [&color](auto&& fpup) {
            fpup(color.r, "r");       // enumerate each field
            fpup(color.g, "g");
            fpup(color.b, "b");
            fpup(color.a, "a");
        });
    }
    
    template<typename P>
    void pup(Pupper<P>& p,          
             FontReference& ref,    
             const Meta& meta) {    
        
        pup_object(p, ref, meta, [&ref](auto&& fpup) {
            fpup(ref.path,  "path");  
            fpup(ref.inner, "inner"); // dispatches to pup for Color
            fpup(ref.outer, "outer");
        });
    }
}

Once the necessary pup:s are implemented, serialization between different format becomes possible e.g.:

// load binary from input stream
std::istream& in = ...;
Color color = from_binary<Color>(in);

// save to json (assumes built with PUPENE_BUILD_JSON=true)
std::string raw_json = to_json(color);

Color and FontReference can now also be combined with some STL containers:

std::unordered_map<Color, FontReference> colored_fonts = {
    {colorA, small}, {colorB, medium}, {colorC, large}
};

// complex keys - such as Color - are ok
std::string raw_json = to_json(colored_fonts);

// (this doesn't require any user-supplied pup:s)
std::vector<int> numbers = {0, 1, 6, -2, 4, 5, 7};
to_binary(numbers, std::cout);

Building

Unless already added, The conan-community remote has to be added for the Boost test dependency to work:

conan remote add conan-community https://api.bintray.com/conan/conan-community/conan 

Using Conan

First-time setup:

conan install . --build missing --install-folder _builds 

Build project:

conan build . --build-folder _builds

Using CMake:

Once conan has built the project for the first time, CMake should work as expected.

Build project:

cmake --build _builds --target install -- -j 8

Tests are run with:

cmake --build _build  --target test

... or run the tests with valgrind/memcheck:

cmake --build _build --target valgrind

CLion/CMake

To play nice with CLion, manually specify the compiler using CMake Options under Settings > Build, Execution, Deployment > CMake, e.g.:

-DCMAKE_CXX_COMPILER=/usr/bin/clang++-5.0

Overarching TODO

  • Types currently require a default constructor to be eligble for pup.
  • Benchmarking and profiling remain.

Alternative libraries:

  • MetaStuff: C++14, similar but expanded features compared to pupene
  • cereal: C++11, only binary serialization(?)