Serialization with context

In some situation, serializing or deserializing C++ objects require some context, such as a factory object, some parameters, etc. Thallium allows passing a context to its serialization mechanism whenever needed.

Client

The following client example sends a “process” RPC that takes two point objects. By calling with_serialization_context() when creating the callable, we can pass any variable we want. By default these variables will be copied into an internal context. If a reference is needed, std::ref() and std::cref() can be used.

Similarly, calling with_serialization_context on the response will allow the use of a context when deserializing the response.

#include <iostream>
#include <thallium.hpp>
#include "point.hpp"

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <address>" << std::endl;
        exit(0);
    }

    tl::engine myEngine("tcp", THALLIUM_CLIENT_MODE);
    tl::remote_procedure process = myEngine.define("process");
    tl::endpoint server = myEngine.lookup(argv[1]);

    point q{1.0, 2.0, 3.0};
    double d = 2.0;

    point p1{4.0, 5.0, 6.0};
    point p2{7.0, 8.0, 9.0};

    // attach a serialization context to serialize the input
    auto callable = process.on(server)
                           .with_serialization_context(std::ref(q), d);
    auto response = callable(p1, p2);
    // attach a serialization context to deserialize the output
    point r = response.with_serialization_context(std::ref(q), d);

    // The above could have been written as follows:
    // point r = sum.on(server)
    //              .with_serialization_context(std::ref(q), d)
    //              (p1, p2)
    //              .with_serialization_context(std::ref(q), d);

    return 0;
}

Server

On the server side, shown bellow, note that although the RPC takes two point objects as argument, our lambda only takes a tl::request. Doing so allows us to get the argument later on, when the context is known, by using get_input() then with_serialization_context().

Similarly, we can add with_serialization_context() before calling respond() to provide a context for the serialization of the response’s data.

#include <iostream>
#include <thallium.hpp>
#include "point.hpp"

namespace tl = thallium;

int main() {

    tl::engine myEngine("tcp", THALLIUM_SERVER_MODE);
    std::cout << "Server running at address " << myEngine.self() << std::endl;

    std::function<void(const tl::request&)> process =
        [](const tl::request& req) {
            point p1, p2;
            point q{3.0,2.0,1.0};
            double d = 3.5;
            std::tie(p1, p2) =
                req.get_input()
                   .with_serialization_context(std::ref(q), d)
                   .as<point,point>();
            std::cout << "Executing RPC" << std::endl;
            req.with_serialization_context(std::ref(q), d)
               .respond(p1+p2);
        };

    myEngine.define("process", process);

    return 0;
}

Type

The context provided in the calls above is accessible in serialization functions such as serialize or load/store, using get_context() on the archive parameter. This function returns a reference to a tuple containing the provided context variables.

It is of course possible to require different types of context when serializing and when deserializing, simply by using the load/store.

The code bellow exemplifies accessing the context in a point class.

#include <iostream>
#include <sstream>

class point {

    private:

        double x;
        double y;
        double z;

    public:

        point(double a=0.0, double b=0.0, double c=0.0)
            : x(a), y(b), z(c) {}

        template<typename A> friend void serialize(A& ar, point& p);

        double operator*(const point& p) const {
            return p.x * x + p.y * y + p.z * z;
        }

        std::string to_string() const {
            std::stringstream ss;
            ss << "(" << x << "," << y << "," << z << ")";
            return ss.str();
        }

        point operator+(const point& p) const {
            return point{p.x + x, p.y + y, p.z + z};
        }
};

template<typename A>
void serialize(A& ar, point& p) {
    point& q = std::get<0>(ar.get_context());
    double d = std::get<1>(ar.get_context());
    std::cout << "Serializing with context q = "
              << q.to_string()
              << " and d = "
              << d << std::endl;
    ar & (p.x + q.x)*d;
    ar & (p.y + q.y)*d;
    ar & (p.z + q.z)*d;
}