Advanced Agent

This example shows a more realistic case of a C++ application that is comprised of header and source files and using more advanced language data-types and constructs. By going through it, you will hopefully notice that creating VRPC bindings still remains a trivial task...

NOTE

In order to follow this example from scratch, create a new directory (e.g. vrpc-bar-agent) and cd into it.

Next, download and unpack the VRPC C++ tarball (containing the header-only library):

wget https://vrpc.io/vrpc-3.0.0.tar.gz
tar -xzf vrpc.tar.gz

Finally create a directory src and you are good to go.

1

Existing C++ code

This time our code is a bit more elaborate and split into header and corresponding source file.

src/Bar.hpp

#include <functional>
#include <unordered_map>
#include <vector>

struct Bottle {
  std::string name;
  std::string category;
  std::string country;
};

class Bar {
 public:
  typedef std::function<void(const std::string&)> StringCallback;
  typedef std::function<void(const Bottle&)> BottleCallback;
  typedef std::vector<BottleCallback> BottleCallbacks;
  typedef std::vector<Bottle> Selection;

  static std::string philosophy();

  Bar() = default;

  explicit Bar(const Selection& selection);

  void addBottle(const std::string& name,
                 const std::string& category = "n/a",
                 const std::string& country = "n/a");

  Bottle removeBottle(const std::string& name);

  void onAdd(const BottleCallback& listener);

  void onRemove(const BottleCallback& listener);

  std::string prepareDrink(const StringCallback& done) const;

  Selection getSelection() const;

 private:

  std::string _random() const;

  BottleCallbacks _addListeners;
  BottleCallbacks _removeListeners;
  Selection _selection;
};

src/Bar.cpp

#include "Bar.hpp"
#include <chrono>
#include <iostream>
#include <thread>

std::string Bar::philosophy() {
  return "I have mixed drinks about feelings.";
}

Bar::Bar(const Selection& selection) : _selection(selection) {}

void Bar::addBottle(const std::string& name,
                    const std::string& category,
                    const std::string& country) {
  Bottle bottle = {name, category, country};
  _selection.push_back(bottle);
  for (const auto& notify : _addListeners) notify(bottle);
}

Bottle Bar::removeBottle(const std::string& name) {
  Selection filtered;
  Bottle bottle;
  for (const auto& x : _selection) {
    if (bottle.name.empty() && (x.name == name)) {
      for (const auto& notify : _removeListeners) notify(x);
      bottle = x;
      continue;
    }
    filtered.push_back(x);
  }
  if (bottle.name.empty()) {
    throw std::runtime_error("Sorry, this bottle is not in our selection");
  }
  _selection = filtered;
  return bottle;
}

void Bar::onAdd(const Bar::BottleCallback& listener) {
  _addListeners.push_back(listener);
}

void Bar::onRemove(const Bar::BottleCallback& listener) {
  _removeListeners.push_back(listener);
}

std::string Bar::prepareDrink(const Bar::StringCallback& done) const {
  const std::vector<std::string> v = {_random(), _random(), _random()};
  std::thread([=]() {
    std::this_thread::sleep_for(std::chrono::seconds(3));
    done("Your drink is ready! I mixed " + v[0] + " with " + v[1] +
         " and a bit of " + v[2] + ".");
  }).detach();
  return "In preparation...";
}

Bar::Selection Bar::getSelection() const {
  return _selection;
}

std::string Bar::_random() const {
  if (_selection.size() == 0) {
    throw std::runtime_error("I searched, but couldn\'t find any bottles");
  }
  int index = std::rand() % _selection.size();
  return _selection[index].name;
}
2

Make it accessible from remote

We are going to produce an executable that starts an agent and sits waiting until it receives remote requests to call functions. Hence, we have to provide a main.cpp file.

src/main.cpp

#include <vrpc/adapter.hpp>
#include <vrpc/agent.hpp>
#include "Bar.hpp"

namespace vrpc {

// Adapt custom type: Bottle
VRPC_DEFINE_TYPE(Bottle, name, category, country);

// Adapt static function
VRPC_STATIC_FUNCTION(Bar, std::string, philosophy)

// Adapt constructors
VRPC_CTOR(Bar)
VRPC_CTOR(Bar, const Bar::Selection&)

// Adapt member functions
VRPC_MEMBER_FUNCTION_X(Bar,
                     void, "",
                     addBottle, "Adds a bottle to the bar",
                     const std::string&, "name", required(), "name of the bottle",
                     const std::string&, "category", "n/a", "category of the drink",
                     const std::string&, "country", "n/a", "country of production")
VRPC_MEMBER_FUNCTION(Bar, Bottle, removeBottle, const std::string&)
VRPC_MEMBER_FUNCTION(Bar, void, onAdd, VRPC_CALLBACK(const Bottle&))
VRPC_MEMBER_FUNCTION(Bar, void, onRemove, VRPC_CALLBACK(const Bottle&))
VRPC_CONST_MEMBER_FUNCTION(Bar, std::string, prepareDrink, VRPC_CALLBACK(const std::string&))
VRPC_CONST_MEMBER_FUNCTION(Bar, Bar::Selection, getSelection)

}  // namespace vrpc

int main(int argc, char** argv) {
  auto agent = vrpc::VrpcAgent::from_commandline(argc, argv);
  if (agent) agent->serve();
  return EXIT_SUCCESS;
}

As you can see, even complex code can be adapted using VRPC macros.

Note the usage of VRPC_MEMBER_FUNCTION_X, which allows you to put meta information to the adapted functions.

While this is really helpful for others to use your API (as its shown in the browser "intellisensly"), this is the only way to propagate C++ default arguments to remote clients.

3

Compilation

Certainly, you want to use the builtin tools of your IDE or any other make file generator (such as cmake) you feel familiar with.

You are encouraged to do so! For getting VRPC compiled only make sure you are including the headers provided in the vrpc folder.

For getting this example working we quickly write a Makefile by hand, like so:

Makefile

TARGET = vrpc-bar-agent
CPPFLAGS = -I. -pthread -fPIC -m64 -O3 -std=c++14
LDFLAGS = -pthread
LDLIBS =

SRCS := $(shell find ./src -name *.cpp)
OBJS := $(addsuffix .o,$(basename $(SRCS)))
DEPS := $(OBJS:.o=.d)

$(TARGET): $(OBJS)
	$(CXX) $(LDFLAGS) $(OBJS) -o $@ $(LOADLIBES) $(LDLIBS)

.PHONY: clean
clean:
	$(RM) $(TARGET) $(OBJS) $(DEPS)

-include $(DEPS)

That's already it, after typing

make

your agent should build and be ready to use.

NOTE

If you copied the code directly from the browser make sure your IDE inserts tabs and not spaces (as Makefiles need tabs).

Try it by simply running the executable in an all-default setting (using the free vrpc.io broker and the default vrpc domain):

./vrpc-bar-agent

Once you see the line

Connecting to message broker... [OK]

appearing in your terminal, you made it and your C++ code is remotely callable!

Last updated