Page 1 of 1

Google Protocol Buffers in action (C++)

Posted: Sun Sep 16, 2012 1:56 pm
by fips
I ran across Google Protocol Buffers quite some time ago in my regular pursuit of having a simple to use and efficient data exchange format that I would widely use in all my applications and tools. PB seemed quite promising in concept at that time, but I was too lazy to actually test it properly as the supplied Visual C++ projects always seemed outdated. However, It all changed quite recently, when I decided to spend some time with the project setup and managed to run a simple test (it was actually easier then expected, surely worth the effort).

First, there's a list of PB key features that I find crucial:
  • Data structures are described using a very simple .proto files (what a relief considering the verbosity of DTD/XSDs).
  • .proto files are then used to generate data access classes (no need for hand-written loaders/savers any more).
  • The produced binary stream is very compact and efficient, and can be easily translated back and forth to a human readable JSON-like textual form, using an offline compiler (this is a killer feature that allows one to edit/hack the binary files in a regular text editor or more importantly use a scripting language to make various data transformations if necessary = BIG WIN!).
  • The data access classes can be generated for both of my favourite languages: C++ (run-time) and Python (tools).
  • PB sources can be directly dropped into and existing project (no need to maintain the countless number of lib variants on all the platforms, I find this very convenient in general as it dramatically reduces the maintenance cost, I'm a big fan of single-source-file amalgamated libs, but that's a completely different story...).
So here's a bunch of steps to follow if you want to get a nice self-contained VC2010 project ready to use (or you can just download the whole package HERE):
  • Create an empty console project.
  • Add '.' to the include directories.
  • From 'protobuf-2.4.1.zip/vsprojects' copy './config.h'.
  • From 'protobuf-2.4.1.zip/src/' copy:
    • './google/protobuf/*.*'
    • './google/protobuf/io/*.*'
    • './google/protobuf/stubs/*.*'
  • Delete:
    • './google/protobuf/test_util.h&cc'
    • './google/protobuf/test_util_lite.h&cc'
  • Delete the unit-test related stuff, everything matching: '*_unittest.*', '*.proto'.
  • Add all the '*.h' and '*.cc' to the project, preserving the directory structure.
  • From 'protoc-2.4.1-win32.zip' also copy './protoc.exe'.
Then, it's time to define our first .proto file, it might look like this (business.proto):

Code: Select all

package business;

message Employee
{
    required string first_name = 1;
    required string last_name = 2;
    required string email = 3;
}

message Company
{
    required string name = 1;
    optional string url = 2;
    repeated Employee employee = 3;
}
We can easily translate it to the C++ data access classes by calling:

Code: Select all

protoc.exe -I=. --cpp_out=. business.proto
Which produces:

Code: Select all

business.pb.h
business.pb.cc
(remember to add these two files to the project as well)

Now, it's time to test our effort. There's nothing easier than filling in a demo company object, saving it to 'company.bin' and loading it back from the binary file, as shows the example below:
VIEW THE CODE BELOW IN FULL-SCREEN (protobuf_sample.cpp)

Code: Select all

/*
(c) 2012 +++ Filip Stoklas, aka FipS, http://www.4FipS.com +++
THIS CODE IS FREE - LICENSED UNDER THE MIT LICENSE
ARTICLE URL: http://forums.4fips.com/viewtopic.php?f=3&t=807
*/

#include <iostream>
#include <fstream>
#include "business.pb.h"

using namespace std;

/// Saves a demo company object to 'company.bin'.
void save()
{
    business::Company company;
    company.set_name("Example Ltd.");
    company.set_url("http://www.example.com");

    // 1st employee
    {
        business::Employee *employee = company.add_employee();
        employee->set_first_name("John");
        employee->set_last_name("Doe");
        employee->set_email("john.doe@example.com");
    }

    // 2nd employee
    {
        business::Employee *employee = company.add_employee();
        employee->set_first_name("Jane");
        employee->set_last_name("Roe");
        employee->set_email("jane.roe@example.com");
    }

    fstream output("company.bin", ios::out | ios::trunc | ios::binary);
    company.SerializeToOstream(&output);
}

/// Loads a demo company object from 'company.bin' and dumps its data.
void load()
{
    business::Company company;
    fstream input("company.bin", ios::in | ios::binary);

    company.ParseFromIstream(&input);
    cout << "Company: " << company.name() << "\n";
    cout << "URL: " << (company.has_url() ? company.url() : "N/A") << "\n";

    cout << "\nEmployees: \n\n";
    for(int i = 0, n = company.employee_size(); i < n; ++i)
    {
        const business::Employee &employee = company.employee(i);
        cout << "First name: " << employee.first_name() << "\n";
        cout << "Last name: " << employee.last_name() << "\n";
        cout << "Email: " << employee.email() << "\n";
        cout << "\n";
    }
}

int main()
{
    save();
    load();
    return 0;
}

// output:
// Company: Example Ltd.
// URL: http://www.example.com
//
// Employees:
//
// First name: John
// Last name: Doe
// Email: john.doe@example.com

// First name: Jane
// Last name: Roe
// Email: jane.roe@example.com
And that's not all! By calling:

Code: Select all

protoc.exe business.proto --decode=business.Company < company.bin > company.txt
We can dump 'binary.bin' into a human readable 'company.txt':

Code: Select all

name: "Example Ltd."
url: "http://www.example.com"
employee {
  first_name: "John"
  last_name: "Doe"
  email: "john.doe@example.com"
}
employee {
  first_name: "Jane"
  last_name: "Roe"
  email: "jane.roe@example.com"
}
Modify it, and then save it back to 'company.bin':

Code: Select all

protoc.exe business.proto --encode=business.Company < company.txt > company.bin
Isn't that sweet!