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;
}