RPC (Remote Procedure Call) API

Using the RPC API

This API provides a client-server model for remote calls with serialized argument- and return data.

Calling a Remote Procedure

The IRpcClient is instantiated from an IParticipant instance by calling the CreateRpcClient() method.

auto rpcCallResultHandler = [] (IRpcClient*, const RpcCallResultEvent& event) {
    if (event.callStatus == SilKit::Services::Rpc::RpcCallStatus::Success)
    {
        return;
    }

    SilKit::Util::SerDes::Deserializer deserializer{SilKit::Util::ToStdVector(event.resultData)};
    std::cout << "sum is " << deserializer.Deserialize<uint32_t>(32) << " with user context " << event.userContext << std::endl;
};

SilKit::Services::Rpc::RpcSpec rpcSpec{"Add", SilKit::Util::SerDes::MediaTypeRpc()};
auto* client = participant->CreateRpcClient("AddClient", rpcSpec, rpcCallResultHandler);

Remote procedures are invoked through the Call() method of an IRpcClient instance. The Call() method is non-blocking and allows for later identification of the call through an additional user context pointer (of type void *) which is passed as an optional, second argument and provided in the call return handler.

Additionally, CallWithTimeout() can be used to trigger calls that have to be replied to within a specified timeout duration. Otherwise the call will lead to a timeout RpcCallResultEvent.

The IRpcClient receives the call result in a callback specified during creation of the IRpcClient, and can be overwritten with SetCallReturnHandler(). The callback provides the user context pointer passed to Call() or CallWithTimeout(), the return data, and a call status indicating success or an error during the procedure.

SilKit::Util::SerDes::Serializer serializer;
serializer.BeginStruct();
serializer.Serialize(uint32_t{31}, 32);
serializer.Serialize(uint32_t{11}, 32);
serializer.EndStruct();

client->Call(serializer.ReleaseBuffer());

Serving a Remote Procedure

The IRpcServer is instantiated from an IParticipant instance by calling the CreateRpcServer() method.

Any call that arrives at the IRpcServer is delivered via a callback specified during creation of the IRpcServer, which can be overwritten using the SetCallHandler() method. There, the argument data and call handle are provided and can be processed.

The IRpcServer must submit the answer to the call at a later point in time with the call handle obtained in the RpcCallHandler by using the SubmitResult() method providing the return data for the calling IRpcClient.

auto rpcCallHandler = [](IRpcServer* server, const RpcCallEvent& event) {
    SilKit::Util::SerDes::Deserializer deserializer{SilKit::Util::ToStdVector(event.argumentData)};
    deserializer.BeginStruct();
    const auto lhs = deserializer.Deserialize<uint32_t>(32);
    const auto rhs = deserializer.Deserialize<uint32_t>(32);
    deserializer.EndStruct();

    SilKit::Util::SerDes::Serializer serializer;
    serializer.Serialize(lhs + rhs, 32);

    server->SubmitResult(event.callHandle, serializer.ReleaseBuffer());
};

SilKit::Services::Rpc::RpcSpec rpcSpec{"Add", SilKit::Util::SerDes::MediaTypeRpc()};
auto* server = participant->CreateRpcServer("AddServer", rpcSpec, rpcCallHandler);

Argument and return data is represented as a byte vector, so the serialization schema can be chosen by the user. Nonetheless, it is highly recommended to use SIL Kit’s Data Serialization/Deserialization API to ensure compatibility among all SIL Kit participants.

Usage Examples

Example: Simple Calculator

In this example, the RPC Server offers a simple function for adding two numbers. The example shows the usage of the RPC Server / Client and data (de-)serialization. Note that the availability of the RPC Server is not guaranteed and will depend on the starting order of the two participants. The next example shows how a coordinated lifecycle can be set up to guarantee the reception of the RPC client call.

Server - Addition

auto rpcCallHandler = [](IRpcServer* server, const RpcCallEvent& event) {
    SilKit::Util::SerDes::Deserializer deserializer{SilKit::Util::ToStdVector(event.argumentData)};
    deserializer.BeginStruct();
    const auto lhs = deserializer.Deserialize<uint32_t>(32);
    const auto rhs = deserializer.Deserialize<uint32_t>(32);
    deserializer.EndStruct();

    SilKit::Util::SerDes::Serializer serializer;
    serializer.Serialize(lhs + rhs, 32);

    server->SubmitResult(event.callHandle, serializer.ReleaseBuffer());
};

SilKit::Services::Rpc::RpcSpec rpcSpec{"Add", SilKit::Util::SerDes::MediaTypeRpc()};
auto* server = participant->CreateRpcServer("AddServer", rpcSpec, rpcCallHandler);

Client - Addition

auto rpcCallResultHandler = [] (IRpcClient*, const RpcCallResultEvent& event) {
    if (event.callStatus != SilKit::Services::Rpc::RpcCallStatus::Success)
    {
        return;
    }

    SilKit::Util::SerDes::Deserializer deserializer{SilKit::Util::ToStdVector(event.resultData)};
    std::cout << "sum is " << deserializer.Deserialize<uint32_t>(32) << std::endl;
};

SilKit::Services::Rpc::RpcSpec rpcSpec{"Add", SilKit::Util::SerDes::MediaTypeRpc()};
auto* client = participant->CreateRpcClient("AddClient", rpcSpec, rpcCallResultHandler);

std::this_thread::sleep_for(1s);

SilKit::Util::SerDes::Serializer serializer;
serializer.BeginStruct();
serializer.Serialize(uint32_t{31}, 32);
serializer.Serialize(uint32_t{11}, 32);
serializer.EndStruct();

client->Call(serializer.ReleaseBuffer());

Example: RPC with guaranteed call reception

This example is based on the previous one and includes participant creation and the setup of a coordinated lifecycle. This guarantees that the RPC client and server are validly connected at the time the client makes the call.

Server - Addition

#include <iostream>

#include "silkit/SilKit.hpp"
#include "silkit/services/rpc/all.hpp"
#include "silkit/services/orchestration/all.hpp"
#include "silkit/util/serdes/Serialization.hpp"

using namespace SilKit::Services::Orchestration;
using namespace SilKit::Services::Rpc;

int main(int argc, char** argv)
{
    auto config = SilKit::Config::ParticipantConfigurationFromString("");
    auto participant = SilKit::CreateParticipant(config, "Server", "silkit://localhost:8500");
    auto* lifecycleService = participant->CreateLifecycleService({OperationMode::Coordinated});

    auto rpcCallHandler = [](IRpcServer* server, const RpcCallEvent& event) {
        SilKit::Util::SerDes::Deserializer deserializer{SilKit::Util::ToStdVector(event.argumentData)};
        deserializer.BeginStruct();
        const auto lhs = deserializer.Deserialize<uint32_t>(32);
        const auto rhs = deserializer.Deserialize<uint32_t>(32);
        deserializer.EndStruct();

        SilKit::Util::SerDes::Serializer serializer;
        serializer.Serialize(lhs + rhs, 32);

        std::cout << "Server function 'Add' is called with parameters: " << lhs << ", " << rhs << std::endl;
        server->SubmitResult(event.callHandle, serializer.ReleaseBuffer());
    };

    SilKit::Services::Rpc::RpcSpec rpcSpec{"Add", SilKit::Util::SerDes::MediaTypeRpc()};
    auto* server = participant->CreateRpcServer("AddServer", rpcSpec, rpcCallHandler);

    auto finalStateFuture = lifecycleService->StartLifecycle();
    finalStateFuture.get();

    return 0;
}

Client - Addition

#include <iostream>

#include "silkit/SilKit.hpp"
#include "silkit/services/rpc/all.hpp"
#include "silkit/services/orchestration/all.hpp"
#include "silkit/util/serdes/Serialization.hpp"

using namespace SilKit::Services::Orchestration;
using namespace SilKit::Services::Rpc;

int main(int argc, char** argv)
{
    auto config = SilKit::Config::ParticipantConfigurationFromString("");
    auto participant = SilKit::CreateParticipant(config, "Client", "silkit://localhost:8500");
    auto* lifecycleService = participant->CreateLifecycleService({OperationMode::Coordinated});

    auto rpcCallResultHandler = [](IRpcClient*, const RpcCallResultEvent& event) {
        if (event.callStatus != SilKit::Services::Rpc::RpcCallStatus::Success)
        {
            return;
        }

        SilKit::Util::SerDes::Deserializer deserializer{SilKit::Util::ToStdVector(event.resultData)};
        std::cout << "Client obtained result: " << deserializer.Deserialize<uint32_t>(32) << std::endl;
    };

    SilKit::Services::Rpc::RpcSpec rpcSpec{"Add", SilKit::Util::SerDes::MediaTypeRpc()};
    auto* client = participant->CreateRpcClient("AddClient", rpcSpec, rpcCallResultHandler);

    lifecycleService->SetCommunicationReadyHandler([client]() {
        SilKit::Util::SerDes::Serializer serializer;
        serializer.BeginStruct();
        serializer.Serialize(uint32_t{31}, 32);
        serializer.Serialize(uint32_t{11}, 32);
        serializer.EndStruct();

        std::cout << "Client calls: 'Add(31, 11)'" << std::endl;
        client->Call(serializer.ReleaseBuffer());
    });

    auto finalStateFuture = lifecycleService->StartLifecycle();
    finalStateFuture.get();

    return 0;
}

API and Data Type Reference

RpcClient API

class IRpcClient

Public Functions

virtual ~IRpcClient() = default
virtual void Call(Util::Span<const uint8_t> data, void *userContext = nullptr) = 0

Initiate a remote procedure call.

Parameters
  • data – A non-owning reference to an opaque block of raw data

  • userContext – An optional user provided pointer that is reobtained when receiving the call result.

virtual void SetCallResultHandler(RpcCallResultHandler handler) = 0

Overwrite the call return handler of this client.

The signature of the handler is void(IRpcClient* client, RpcCallResultEvent event).

Parameters

handler – A std::function with the above signature

virtual void CallWithTimeout(Util::Span<const uint8_t> data, std::chrono::nanoseconds timeout, void *userContext = nullptr) = 0

Initiate a remote procedure call with a specified timeout.

In a synchronized execution, simulation time is used for the timeout, in an unsynchronized execution, system time is used for the timeout. If a timeout occurs the CallResultHandler is called with status timeout. After the timeout occurred, no further call result events will be triggered for this call.

Parameters
  • data – A non-owning reference to an opaque block of raw data

  • timeout – A duration in nanoseconds after which the call runs into a timeout and is canceled

  • userContext – An optional user provided pointer that is reobtained when receiving the call result.

RpcServers API

class IRpcServer

Public Functions

virtual ~IRpcServer() = default
virtual void SubmitResult(IRpcCallHandle *callHandle, Util::Span<const uint8_t> resultData) = 0

Answers an already received call from remote with arbitrary data.

Using the call handle obtained in the call handler, the result is send back to the calling client. This can happen directly in the call handler or at a later point in time.

Parameters
  • callHandle – A unique identifier of this call

  • resultData – The byte vector to be returned to the client

virtual void SetCallHandler(RpcCallHandler handler) = 0

Overwrite the call handler of this server.

The signature of the call handler is void(IRpcServer* server, RpcCallEvent event).

Parameters

handler – A std::function with the above signature

Callback Types

using SilKit::Services::Rpc::RpcCallHandler = std::function<void(IRpcServer *server, const RpcCallEvent &event)>

The callback function invoked when a call is to be handled by the IRpcServer.

using SilKit::Services::Rpc::RpcCallResultHandler = std::function<void(IRpcClient *client, const RpcCallResultEvent &event)>

The callback function invoked when a call result is delivered to the IRpcClient.

Data Structures

struct RpcCallEvent

An incoming rpc call from a RpcClient with call data and timestamp.

Public Members

std::chrono::nanoseconds timestamp

Send timestamp of the event.

IRpcCallHandle *callHandle

Handle of the rpc call by which the call can be identified and its result can be submitted.

Util::Span<const uint8_t> argumentData

Data of the rpc call as provided by the client on call.

struct RpcCallResultEvent

An incoming rpc call result of a RpcServer containing result data and timestamp.

Public Members

std::chrono::nanoseconds timestamp

Send timestamp of the event.

void *userContext

The user context pointer as it was provided when the call was triggered.

RpcCallStatus callStatus

The status of the call, resultData is only valid if callStats == RpcCallStatus::Success.

Util::Span<const uint8_t> resultData

Data of the rpc call result as provided by the server.

class RpcSpec

The specification of topic, media type and labels for RpcClients and RpcServers.

Public Functions

RpcSpec() = default
inline RpcSpec(std::string functionName, std::string mediaType)

Construct a RpcSpec via topic and mediaType.

inline void AddLabel(const SilKit::Services::MatchingLabel &label)

Add a given MatchingLabel.

inline void AddLabel(const std::string &key, const std::string &value, SilKit::Services::MatchingLabel::Kind kind)

Add a MatchingLabel via key, value and matching kind.

inline auto FunctionName() const -> const std::string&

Get the topic of the RpcSpec.

inline auto MediaType() const -> const std::string&

Get the media type of the RpcSpec.

inline auto Labels() const -> const std::vector<SilKit::Services::MatchingLabel>&

Get the labels of the RpcSpec.

Enumerations

enum SilKit::Services::Rpc::RpcCallStatus

The status of a RpcCallResultEvent. Informs whether a call was successful.

Values:

enumerator Success

Call was successful.

enumerator ServerNotReachable

No server matching the RpcSpec was found.

enumerator UndefinedError

An unidentified error occured.

enumerator InternalServerError

The Call lead to an internal RpcServer error. This might happen if no CallHandler was specified for the RpcServer.

enumerator Timeout

The Call did run into a timeout and was canceled. This might happen if a corresponding server crashed, ran into an error or took too long to answer the call.

Advanced Usage and Configuration

Function Name

RPC clients and RPC servers provide a function name which is part of their RpcSpec.

Communication only takes place among RPC clients and RPC servers with the same function name.

Media Type

Both RPC clients and RPC servers define a media type as part of their RpcSpec. It is a meta description of the transmitted data in accordance to RFC2046 and should be used to provide information about the de-/serialization of the underlying user data. Just like the function name, the media type has to match between RPC clients / RPC servers for communication to take place. An empty string on an RPC client will match any other media type on a server.

When data is serialized using SIL Kit’s Data Serialization/Deserialization API, the media type constant MediaTypeRpc() must be used.

Labels

Both RPC clients and RPC servers can be annotated with string-based key-value pairs (labels) which can be either mandatory or optional. In addition to the matching requirements given by topic and media type, RPC clients and RPC servers will only communicate if their labels match.

The labels are stored in the RpcSpec. A MatchingLabel can be added via AddLabel(), see the following code snippet:

SilKit::Services::Rpc::RpcSpec rpcSpec{"OpenMirror", "application/json"};
rpcSpec.AddLabel("Instance", "FrontLeft", SilKit::Services::MatchingLabel::Kind::Optional);
auto* client = participant->CreateRpcClient("FrontLeftDoorMirrorPanel", rpcSpec, callResultHandler);

To communicate, RPC clients and RPC Servers must conform to the following matching rules:

  • A mandatory label matches, if a label of the same key and value is found on the corresponding counterpart.

  • An optional label matches, if the label key does not exist on the counterpart or both its key and value are equal.

The following table shows how RPC clients and RPC servers with matching topics and matching media type would match corresponding to their labels. Note that the label matching is symmetric, so clients and servers are interchangeable here.

Table 3 Label combinations

Server {“Instance”, “Left”, Optional}

Server {“Instance”, “Left”, Mandatory}

Client {}

Match

No Match

Client {“Instance”, “Left”, Optional}

Match

Match

Client {“Instance”, “Right”, Optional}

No Match

No Match

Client {“Namespace”, “Car”, Optional}

Match

No Match

Client {“Namespace”, “Car”, Mandatory}

No Match

No Match

Error handling

  • If using Call() with no corresponding server available, the CallReturnHandler is triggered immediately with RpcCallStatus::ServerNotReachable.

  • SubmitResult() must only be used with a valid call handle received in the RpcHandler.

  • The RpcCallResultEvent::resultData member is only valid if callStatus == RpcCallStatus::Success.

  • If the RPC server receives a call but does not have a valid call handler, the RPC client will receive an RpcCallResultEvent with callStatus == RpcCallStatus::InternalServerError.

  • If the RpcServer does not reply within the specified timeout of CallWithTimeout(), the CallReturnHandler is triggered immediately with RpcCallStatus::Timeout.