GitHunt

cpp-async-rpc: Library for Asynchronicity, Serialization and Remoting

This is cpp-async-rpc, a C++17 library supporting template meta-programming,
asynchronous network programming, binary serialization and RPC.

Disclaimer: This is not a Google supported product.

What does cpp-async-rpc look like?

RPC server and client

Here a single small binary includes an interface definition, the server and the
client.

#include <iostream>
#include <string>
#include "arpc/client.h"
#include "arpc/interface.h"
#include "arpc/server.h"

/// This is the definition of the Greeter interface.
ARPC_INTERFACE(Greeter, (/* doesn't extend other interfaces */),
               (  // Return a salutation..
                   ((std::string), say_hello_to,
                    (((const std::string&), name)))));

/// This is the server-side implementation of the Greeter interface.
struct GreeterImpl : Greeter {
  std::string say_hello_to(const std::string& name) override {
    return "Hello " + name + "!";
  }
};

int main(int argc, char* argv[]) {
  // Create a server-side implementation object.
  arpc::server_object<GreeterImpl> greeter;

  // Set up the server on TCP port 9999 and register the object in it with under
  // the "greeter" name.
  arpc::server server({/* default options */}, arpc::endpoint().port(9999));
  server.register_object("greeter", greeter);

  // Start serving RPCs.
  server.start();

  // Create a client connection to the server.
  arpc::client_connection client(arpc::endpoint().name("localhost").port(9999));

  // Get a proxy for the "greeter" remote object.
  auto greeter_proxy = client.get_proxy<Greeter>("greeter");

  // Call the remote method and print the results.
  std::cout << greeter_proxy.say_hello_to("world") << std::endl;

  return 0;
}

Yeah, we use the C preprocessor as our IDL compiler, which makes the syntax
awkward, but on the other hand your interface definition remains a valid (even
if ugly) C++ header file.

Serialization of custom structures

cpp-async-rpc is able to serialize and deserialize most standard library classes
out of the box. It also handles custom classes that implement standard
interfaces, like the begin()/end() contract for containers. But it wouldn't
be so useful if you couldn't teach it how to serialize your own data structures
(for instance for their use as RPC arguments). Doing so is easy:

#include <iostream>
#include <string>
#include <vector>
#include "arpc/binary_codecs.h"
#include "arpc/iostream_adapters.h"
#include "arpc/serializable.h"

// To be serializable, MyClass must inherit arpc::serializable<MyClass> or
// arpc::dynamic<MyClass> if run-time polymorphism is required.
struct MyClass : arpc::serializable<MyClass> {
  int x;
  double y;
  std::vector<std::string> z;

  // Enumerate the fields that must be serialized.
  ARPC_FIELDS(x, y, z);
};

int main(int argc, char* argv[]) {
  // Create an instance of my data structure.
  MyClass data{{/* base class init */}, 4, 5.5, {"first", "second", "third"}};

  // Create a binary encoder outputting to stdout.
  arpc::ostream_output_stream oos(std::cout);
  arpc::little_endian_binary_encoder encoder(oos);

  // Write the binary data. Try piping the output of this program to xxd.
  encoder(data);

  return 0;
}

Asynchronous networking

This example sends an HTTP get request and prints out the response in an
asynchronous manner. It uses a local context to set a timeout of 10 seconds
for the whole set of calls.

#include <chrono>
#include <exception>
#include <iostream>
#include <string>
#include "arpc/awaitable.h"
#include "arpc/context.h"
#include "arpc/errors.h"
#include "arpc/select.h"
#include "arpc/socket.h"

int main(int argc, char* argv[]) {
  try {
    arpc::context ctx;
    ctx.set_timeout(std::chrono::seconds(10));

    auto s =
        arpc::dial(arpc::endpoint().name("www.kernel.org").service("http"));

    std::string request = "GET / HTTP/1.0\r\nHost: www.kernel.org\r\n\r\n";
    char buf[256];

    while (true) {
      auto [sent, received] = arpc::select(
          request.size() ? s.async_write(request.data(), request.size())
                         : arpc::never().then([]() { return std::size_t{0}; }),
          s.async_read(buf, sizeof(buf)));

      if (sent) {
        std::cout << "S(" << *sent << ")" << std::endl;
        // Remove the bytes we already sent.
        request.erase(0, *sent);
      }

      if (received) {
        std::cout << "R(" << *received << ")" << std::endl
                  << std::string(buf, buf + *received) << std::endl;
      }
    }

    return 0;
  } catch (const arpc::errors::base_error& e) {
    std::cerr << "Exception of type " << e.portable_error_class_name()
              << " with message: " << e.what() << std::endl;
    return 1;
  } catch (...) {
    std::cerr << "Some other exception." << std::endl;
    return 1;
  }
}

Why cpp-async-rpc?

This library evolved from plans to communicate two distinct microcontrollers
over a serial line.

It eventually became a generic serialization framework (because none of the
existing ones would be easy to adapt to the target embedded environments), and
from there on the jump to an RPC framework with support with cancellation and
asynchronous networking was easier.

As cpp-async-rpc built on template metaprogramming to reduce the amount of code
the user would need to write, the library eventually moved from targeting C++11
to target C++17, which simplified a number of things and made some syntax way
more natural.

At of today, the library supports POSIX environments (it's developed on Linux),
but the plan is to eventually make it work for Espressif's
ESP-IDF environment, so that RPC
servers can be implemented in the
ESP32 hardware
platform.

What's in cpp-async-rpc?

  • Preprocessor-based code generation (sequences, iteration and conditionals).
    This is used so that cpp-async-rpc doesn't require an IDL compiler to define
    its serializable structures or RPC interfaces. Thanks to the preprocessor,
    these are just defined in C++ native headers (albeit with some syntax
    contortions).

  • Meta-programming toolkit: support for compile-time sequences with associated
    compile-time or run-time values. The library supports compile-time iteration
    on lists of types or values, and compile-time and constexpr mapping
    (transform()) and aggregation (accumulate()) of data sequences.

  • A serialization framework that supports portable binary serialization of most
    of the standard types in modern C++, including primitive types, strings,
    sequences and associative containers. cpp-async-rpc can also serialize smart
    pointers (including run-time polymorphic types and possibly cyclic object
    graphs).

  • Asynchronous primitives for operating on files, sockets and timers, coupled
    with synchronization primitives (mutexes, semaphores, flags, queues, futures)
    that also support asynchronicity and event composition. A select() function
    allows to compose events so that the first one to trigger is notified. The
    synchronization primitives are drop-in replacements for the standard library
    ones, but they are built on POSIX pipes so that they can be used in
    select() or poll().

  • A subclass of std::thread with support for stackable execution contexts.
    The context primitive can hold data or deadlines that are propagated across
    RPC call boundaries. If a context is cancelled, any I/O operation or
    asynchronous wait related to it will throw an exception immediately.

  • Networking support for resolving domain names and ports, and establishing
    both client and server connections.

  • Preprocessor-based generation of RPC-oriented "C++ interfaces" (classes with
    just pure virtual methods), including asynchronous variants and the
    associated RPC proxy implementations.

  • RPC client and server support for remoting over RPC.

What's still missing?

  • Discovering and fixing all the bugs.

  • A usable port to the ESP32 (which will likely involve some
    VFS
    component to support vestigial pipes as select-able locks.

  • Non-token test coverage.

  • Documentation.

Inspiration

  • Cereal: A C++11 library for
    serialization. This was the original source of inspiration for cpp-async-rpc.

  • Boost MPL, Fusion and Hana libraries. These
    provided inspiration on how to process sequences of types or data at compile
    time (for example for iterating on struct fields).

  • The Go language. The cancellable contexts, the channel
    multiplexing and the clean networking libraries in Go have inspired some of
    the work in cpp-async-rpc.

  • JavaScript Promises
    as the chosen way to encapsulate behaviour to be run once asynchronous
    conditions are met.

Dependencies