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.
-
virtual ~IRpcClient() = default
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
-
virtual ~IRpcServer() = default
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.
-
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.
-
std::chrono::nanoseconds timestamp
-
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 Labels() const -> const std::vector<SilKit::Services::MatchingLabel>&
Get the labels of the RpcSpec.
-
RpcSpec() = default
Enumerations
-
enum SilKit::Services::Rpc::RpcCallStatus
The status of a RpcCallResultEvent. Informs whether a call was successful.
Values:
-
enumerator Success
Call was successful.
-
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.
-
enumerator Success
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.
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, theCallReturnHandler
is triggered immediately withRpcCallStatus::ServerNotReachable
.SubmitResult()
must only be used with a valid call handle received in theRpcHandler
.The
RpcCallResultEvent::resultData
member is only valid ifcallStatus == RpcCallStatus::Success
.If the RPC server receives a call but does not have a valid call handler, the RPC client will receive an
RpcCallResultEvent
withcallStatus == RpcCallStatus::InternalServerError
.If the RpcServer does not reply within the specified timeout of
CallWithTimeout()
, the CallReturnHandler is triggered immediately withRpcCallStatus::Timeout
.