Sending arguments, returning values

In this example we will define a “sum” RPC that will take two integers and return their sum.

Server

Here is the server code.

#include <iostream>
#include <thallium.hpp>

namespace tl = thallium;

void sum(const tl::request& req, int x, int y) {
    std::cout << "Computing " << x << "+" << y << std::endl;
    req.respond(x+y);
}

int main(int argc, char** argv) {

    tl::engine myEngine("tcp://127.0.0.1:1234", THALLIUM_SERVER_MODE);
    std::cout << "Server running at address " << myEngine.self() << std::endl;
    myEngine.define("sum", sum);

    return 0;
}

Notice that our sum function now takes two integers in addition to the const reference to a thallium::request. You can also see that this request object is used to send a response back to the client. Because the server now sends something back to the client, we do not call ignore_response() when defining the RPC.

Client

Let’s now take a look at the client code.

#include <iostream>
#include <thallium.hpp>

namespace tl = thallium;

int main(int argc, char** argv) {
    if(argc != 2) {
        std::cerr << "Usage: " << argv[0] << " <address>" << std::endl;
        exit(0);
    }
    tl::engine myEngine("tcp", THALLIUM_CLIENT_MODE);
    tl::remote_procedure sum = myEngine.define("sum");
    tl::endpoint server = myEngine.lookup(argv[1]);
    int ret = sum.on(server)(42,63);
    std::cout << "Server answered " << ret << std::endl;

    return 0;
}

The client calls the remote procedure with two integers and gets an integer back. This way of passing parameters and returning a value hides many implementation details that are handled with a lot of template metaprogramming. Effectively, what happens is the following. When passing the sum function to engine::define, the compiler deduces from its signature that clients will send two integers. Thus it creates the code necessary to deserialize two integers before calling the function.

On the client side, calling sum.on(server)(42,63) makes the compiler realize that the client wants to serialize two integers and send them along with the RPC. It therefore also generates the code for that. The same happens when calling req.respond(...) in the server, the compiler generates the code necessary to serialize whatever object has been passed.

Back on the client side, sum.on(server)(42,63) does not actually return an integer. It returns an instance of thallium::packed_response, which can be cast into any type, here an integer. Asking the packed_response to be cast into an integer also instructs the compiler to generate the right deserialization code.

Warning

A common miskate consists of changing the arguments accepted by an RPC handler but forgetting to update the calls to that RPC on clients. This can lead to data corruptions or crashes. Indeed, Thallium has no way to check that the types passed by the client to the RPC call are the ones expected by the server.

Warning

Another common mistake is to use integers of different size on client and server. For example sum.on(server)(42,63); on the client side will serialize two int values, because int is the default for integer litterals. If the corresponding RPC handler on the server side had been void sum(const tl::request& req, int64_t x, int64_t y), the call would have led to data corruptions and potential crash. One way to ensure that the right types are used is to explicitely cast the litterals: sum.on(server)(static_cast<int64_t>(42), static_cast<int64_t>(63));.

Timeout

It can sometime be useful for an operation to be given a certain amount of time before timing out. This can be done using the callable_remote_procedure::timed() function. This function behaves like the operator() but takes a first parameter of type std::chrono::duration representing an amount of time after which the call will throw a thallium::timeout exception. For instance in the above client code, int ret = sum.on(server)(42,63); would become int ret = sum.on(server).timed(std::chrono::milliseconds(5), 42 ,63); to allow for a 5ms timeout.