Serializing and Deserializing with PUP

CS 641 Lecture, Dr. Lawlor
/* The basic idea with a Pack/UnPacker (PUPer) is to overload operator|
to pass each piece of your class into the PUPer. The PUPer can then
read, write, or modify anything in your class, which gives you serialization
and deserialization over the network, to disk, etc. */

/* Simple PUP-capable classes */
class has_strings {
public:
std::string name, address;
template <class PUPer> friend void operator|(PUPer &p,has_strings &me) {
p|me.name; p|me.address;
}
};

class vector2d {
double x,y;
public:
vector2d(double s_) :x(s_), y(s_) {}
vector2d(double x_=0.0,double y_=0.0) :x(x_), y(y_) {}
vector2d operator+=(const vector2d &rhs) {x+=rhs.x; y+=rhs.y; return *this;}
friend vector2d operator+(vector2d lhs,const vector2d &rhs)
{lhs+=rhs; return lhs;}
template <class PUPer> friend void operator|(PUPer &p,vector2d &v) {
p|v.x; p|v.y;
}
};

/* Generic PUP support for STL containers */
template <class PUPer,class T>
void operator|(PUPer &p,std::vector<T> &v) {
long size=v.size();
p|size;
v.resize(size);
for (long i=0;i<size;i++) {
T &vi=v[i];
p|vi;
}
}
template <class PUPer>
void operator|(PUPer &p,std::string &s) {
long size=s.size();
p|size;
s.resize(size);
for (long i=0;i<size;i++) p|s[i];
}

/* Backend PUPers: */
// Write PUP'd data to cout, prefixed by the datatype
class PUP_print {
public:
void operator|(int &v) {std::cout<<"int "<<v<<"\n";}
void operator|(long &v) {std::cout<<"long "<<v<<"\n";}
void operator|(double &v) {std::cout<<"double "<<v<<"\n";}
void operator|(char &v) {std::cout<<"char "<<v<<"\n";}
//... and so on for other primitive types
};

// Total up the number of bytes of binary data being PUP'd
class PUP_size {
public:
long size;
PUP_size() :size(0) {}
void operator|(int &v) {size+=sizeof(int);}
void operator|(long &v) {size+=sizeof(long);}
void operator|(double &v) {size+=sizeof(double);}
void operator|(char &v) {size+=sizeof(char);}
//... and so on for other primitive types
};
template <class T> long size_in_bytes(T &t) {PUP_size p; p|t; return p.size;}

// Read out binary data from each class into the given buffer
class PUP_binary_read {
public:
char *dest;
PUP_binary_read(char *dest_) :dest(dest_) {}
void operator|(int &v)
{memcpy(dest,&v,sizeof(int)); dest+=sizeof(int);}
void operator|(long &v)
{memcpy(dest,&v,sizeof(long)); dest+=sizeof(long);}
void operator|(double &v)
{memcpy(dest,&v,sizeof(double)); dest+=sizeof(double);}
void operator|(char &v) {*dest=v; dest+=sizeof(char);}
//... and so on for other primitive types
};

// Write binary data into each class from the given buffer
class PUP_binary_write {
public:
const char *src;
PUP_binary_write(const char *src_) :src(src_) {}
void operator|(int &v)
{memcpy(&v,src,sizeof(int)); src+=sizeof(int);}
void operator|(long &v)
{memcpy(&v,src,sizeof(long)); src+=sizeof(long);}
void operator|(double &v)
{memcpy(&v,src,sizeof(double)); src+=sizeof(double);}
void operator|(char &v) {v=*src; src+=sizeof(char);}
//... and so on for other primitive types
};

// Run some PUPers on this datatype:
template <class T>
void play_with_pup(T &t,const char *description) {
std::cout<<"\nPlaying with "<<description<<":\n";
int len=size_in_bytes(t);
std::cout<<"Size in bytes: "<<len<<"\n";

// Read the class into a buffer (serialize)
char sentinal=0xff;
char *buf=new char[len+1]; buf[len]=sentinal;
PUP_binary_read pR(buf); pR|t;
if (buf[len]!=sentinal) std::cout<<"wrote past end of buffer!\n";
if (pR.dest!=buf+len) std::cout<<"size/copy mismatch!\n";

// Overwrite the class from a buffer (deserialize)
PUP_binary_write pW(buf); pW|t;

delete[] buf;

PUP_print pP; pP|t;
}

int foo(void) {
std::vector<int> vi; vi.push_back(17); vi.push_back(23);
play_with_pup(vi,"std::vector of ints");

std::vector<std::vector<int> > vvi;
vvi.push_back(vi); vi.push_back(9); vvi.push_back(vi);
play_with_pup(vvi,"std::vector of vector of ints");

std::string s="bar";
play_with_pup(s,"std::string");

std::vector<std::string> vs; vs.push_back(s);
play_with_pup(vs,"std::vector of string");

vector2d v(3.4,5.7);
play_with_pup(v,"vector2d");

return 0;
}

(Try this in NetRun now!)

Here's an example where I'm using PUP to send a complicated class over the network as one big packet.  This is both simpler and much more efficient than doing dozens of skt_send calls for each piece of "ApplicationData":
/* Here we're using PUP to send data across the network */
#include "osl/socket.h"
#include "osl/socket.cpp"
#include <sys/wait.h> /* for wait() */
#include <unistd.h> /* for fork() */

/* Simple PUP-capable classes */
class vector2d {
double x,y;
public:
vector2d(double s_) :x(s_), y(s_) {}
vector2d(double x_=0.0,double y_=0.0) :x(x_), y(y_) {}
vector2d operator+=(const vector2d &rhs) {x+=rhs.x; y+=rhs.y; return *this;}
friend vector2d operator+(vector2d lhs,const vector2d &rhs)
{lhs+=rhs; return lhs;}
template <class PUPer> friend void operator|(PUPer &p,vector2d &v) {
p|v.x; p|v.y;
}
};

/* Generic PUP support for STL containers */
template <class PUPer,class T>
void operator|(PUPer &p,std::vector<T> &v) {
long size=v.size();
p|size;
v.resize(size);
for (long i=0;i<size;i++) {
T &vi=v[i];
p|vi;
}
}
template <class PUPer>
void operator|(PUPer &p,std::string &s) {
long size=s.size();
p|size;
s.resize(size);
for (long i=0;i<size;i++) p|s[i];
}

/* Backend PUPers: */
// Total up the number of bytes of binary data being PUP'd
class PUP_size {
public:
long size;
PUP_size() :size(0) {}
void operator|(int &v) {size+=sizeof(int);}
void operator|(long &v) {size+=sizeof(long);}
void operator|(double &v) {size+=sizeof(double);}
void operator|(char &v) {size+=sizeof(char);}
//... and so on for other primitive types
};
template <class T> long size_in_bytes(T &t) {PUP_size p; p|t; return p.size;}

// Read out binary data from each class into the given buffer
class PUP_binary_read {
public:
char *dest;
PUP_binary_read(char *dest_) :dest(dest_) {}
void operator|(int &v)
{memcpy(dest,&v,sizeof(int)); dest+=sizeof(int);}
void operator|(long &v)
{memcpy(dest,&v,sizeof(long)); dest+=sizeof(long);}
void operator|(double &v)
{memcpy(dest,&v,sizeof(double)); dest+=sizeof(double);}
void operator|(char &v) {*dest=v; dest+=sizeof(char);}
//... and so on for other primitive types
};

// Write binary data into each class from the given buffer
class PUP_binary_write {
public:
const char *src;
PUP_binary_write(const char *src_) :src(src_) {}
void operator|(int &v)
{memcpy(&v,src,sizeof(int)); src+=sizeof(int);}
void operator|(long &v)
{memcpy(&v,src,sizeof(long)); src+=sizeof(long);}
void operator|(double &v)
{memcpy(&v,src,sizeof(double)); src+=sizeof(double);}
void operator|(char &v) {v=*src; src+=sizeof(char);}
//... and so on for other primitive types
};

/**************** PUP-and-network interface code ************/
template <class T>
void send_pup(SOCKET s,T &t) {
int len=size_in_bytes(t); // how many bytes in the message
len+=sizeof(len); // leave room for the length field
char *buf=new char[len]; // allocate message buffer
PUP_binary_read pR(buf); pR|len; pR|t; // fill message buffer: len and t
skt_sendN(s,buf,len); // send off
delete[] buf; // cleanup
}

template <class T>
void recv_pup(SOCKET s,T &t) {
int len=0;
skt_recvN(s,&len,sizeof(len)); // receive len first
len-=sizeof(len);
char *buf=new char[len]; // message buffer
skt_recvN(s,buf,len); // receive buffer
PUP_binary_write pW(buf); pW|t; // unpack into t
delete[] buf; // cleanup
}

template <class T>
int compute_checksum(T &t) {
return size_in_bytes(t);
}

/**************** Example application class *************/
class ApplicationData {
public:
std::string name;
std::vector<int> someints;
std::vector<vector2d> somepoints;

template <class PUPer> friend void operator|(PUPer &p,ApplicationData &me) {
p|me.name; p|me.someints; p|me.somepoints;
}
};

/*************** Socket code *********************/

/* Run child process's code. Socket connects to parent. */
void run_child(SOCKET s) {
cout<<"Child alive! Sending data to parent."<<std::endl;

ApplicationData ad;
ad.name="Cybertron";
for (int i=0;i<20;i++) ad.someints.push_back(rand()%4);
for (int i=0;i<6;i++) ad.somepoints.push_back(vector2d(rand()%10,rand()%10));
send_pup(s,ad);

cout<<"Child done. Checksum="<<compute_checksum(ad)<<std::endl;
}

/* Run parent process's code. Socket connects to child */
void run_parent(SOCKET s) {
cout<<"Parent alive! Getting data from child"<<std::endl;

ApplicationData ad;
recv_pup(s,ad);

cout<<"Parent done. Checksum="<<compute_checksum(ad)
<<", name="<<ad.name<<", etc"<<std::endl;
}


int foo(void) {
unsigned int port=12345; cout.flush();
int newpid=fork();
if (newpid!=0) { /* I'm the parent */
SERVER_SOCKET serv=skt_server(&port);
SOCKET s=skt_accept(serv,0,0);
run_parent(s);
skt_close(s);
int status=0;
wait(&status); /* wait for child to finish */
} else { /* I'm the child */
SOCKET s=skt_connect(skt_lookup_ip("127.0.0.1"),port,2);
usleep(1000); /* slow down child, to avoid corrupted cout! */
run_child(s);
skt_close(s);
exit(0); /* close out child process when done */
}
return 0;
}

(Try this in NetRun now!)