Object Serialization with PUP

CS 641 Lecture, Dr. Lawlor

Most message-passing protocols only support sending raw bytes from one machine to another.  Slightly more advance protocols allow one to send flat arrays of ints, or floats.  I don't know of a message-passing protocol that supports sending more complex objects, like:
    std::map<std::string,std::vector<int> > gradeVec;

However, there's actually a pretty simple (but very C++) way to support sending arbitrarily complex nested objects.  I call this "pup".

Pack/UnPack (PUP)

The basic idea is that packing an object into a buffer of bytes, and unpacking an object from a buffer, are basically the same operation--you just have to get access to the bytes of the object and store or restore them.

Here's the example we covered in class, with names changed from "the_bytes" and "bytes" to my standard names "pup_er" and "pup":
#include <map>
#include <string>
#include <vector>

class pup_er {
public:
char *b; /* where the bytes are coming from */
bool is_recv;
};
/* Meaning: I need n bytes at pointer p. */
void pup(pup_er &p, char *d,int n)
{
for (int i=0;i<n;i++) {
if (p.is_recv)
d[i]=*p.b++; /* recv */
else
*p.b++=d[i]; /* send */
}
}
void pup(pup_er &p, int &i) {
pup(p,(char *)&i,sizeof(i));
}
void pup(pup_er &p, std::string &s) {
int sz=s.size();
pup(p,sz); // how many elements?
s.resize(sz); // allocate space for elements
pup(p,&s[0],s.size()); // get the elements
}

template <class T>
void pup(pup_er &p, std::vector<T> &v) {
int sz=v.size();
pup(p,sz); // how many elements?
v.resize(sz); // allocate space for elements
for (unsigned int i=0;i<v.size();i++)
pup(p,v[i]); // get the elements
}

int foo(void) {
char buf[1000];
pup_er p; p.b=buf; p.is_recv=false;

std::vector<std::string> v;
v.push_back("bobo"); v.push_back("zzz");
pup(p,v); /* puts bytes of v into p's array */
int n=p.b-buf;
std::cout<<"During packing, pup_er class got "<<n<<" bytes:\n";

for (int i=0;i<n;i++) printf("%c(0x%02x) ",buf[i],buf[i]);

p.b=buf; p.is_recv=true;
std::vector<std::string> s;
pup(p,s); /* creates s from p's array */
std::cout<<"\nStrings I received: \n";
for (unsigned int i=0;i<s.size();i++) std::cout<<" Got '"<<s[i]<<"'\n";
std::cout<<"During unpacking, pup_er class got "<<p.b-buf<<" bytes\n";
return 0;
}

(Try this in NetRun now!)

Here's the corresponding code for a std::map.  Because you can't iterate through an empty map, and you can't index into an existing map, you've got to have slightly different code for the send and receive sides:
template <class IDX,class DATA>
void pup(pup_er &p, std::map<IDX,DATA> &v) {
int sz=v.size();
pup(p,sz); // how many elements?
if (p.is_recv) { /* receiving: pull down elements */
for (int i=0;i<sz;i++) {
IDX x; DATA d;
pup(p,x); // get the elements
pup(p,d);
v[x]=d; /* put into map */
}
} else { /* sending: use iterator to find elements */
typename std::map<IDX,DATA>::iterator it;
for (it=v.begin();it!=v.end();++it) {
pup(p,*(IDX *)&(*it).first);
pup(p,(*it).second);
}
}
}
Finally, here's a more complex example, where "pup_er" figures out what to do with the bytes in a virtual "bytes" method.