Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[core] protobuf service client to v6 #1981

Open
wants to merge 18 commits into
base: master
Choose a base branch
from

Conversation

rex-schilasky
Copy link
Contributor

@rex-schilasky rex-schilasky commented Jan 30, 2025

Description

This pull request overhauls the eCAL Protobuf service client API by replacing the legacy client implementation with a more modern, type-safe, and modular design. In the old API, the class eCAL::protobuf::CServiceClient was implemented as a thin wrapper around the eCAL v5 client and provided only a generic (untyped) call interface. The new design introduces several major improvements:

Key Enhancements

1. Separation of Typed and Untyped APIs

  • Typed API:

    • A new templated response structure, TMsgServiceResponse<ResponseT>, now encapsulates a strongly typed protobuf response along with metadata (call state, server ID, method information, return state, and error message).
    • A vector alias, TMsgServiceResponseVecT<ResponseT>, is provided to ease handling of responses from multiple service instances.
    • The new client class CServiceClientTyped<T> offers templated call methods (for both blocking and callback-based calls) that automatically parse the raw response into a typed response.
  • Untyped API:

    • The untyped API is now encapsulated in CServiceClient<T>, which provides methods that return the raw SServiceResponse. This retains compatibility for users who prefer to handle response parsing manually.

2. Client Instance Refactoring

  • The legacy CServiceClient implementation relied on a single client instance wrapper. The new design splits the functionality into two distinct client instance wrappers:
    • CClientInstanceUntyped:
      Provides untyped call methods that directly return raw responses.
    • CClientInstanceTyped:
      Provides templated call methods that parse and convert raw responses into strongly typed responses.
  • Both wrappers now use composition (i.e., they wrap an underlying eCAL::CClientInstance) rather than inheritance, which improves modularity and maintainability.

3. Unified Base Class for Service Clients

  • A new two-parameter base class, CServiceClientBase<T, ClientInstanceT>, has been introduced. It centralizes common functionality such as:
    • Converting the underlying generic client instances into templated client instance wrappers.
    • Providing helper functions (e.g., ProcessInstances) to uniformly iterate over and process all available client instances.
  • The untyped (CServiceClient<T>) and typed (CServiceClientTyped<T>) client classes derive from this new base class, passing in CClientInstanceUntyped<T> or CClientInstanceTyped<T> as the second template parameter, respectively.

4. Enhanced Call Method Overloads

  • Blocking Calls:

    • The new CallWithResponse methods return a std::pair<bool, std::vector<...>> where:
      • The bool indicates whether all client instances responded successfully.
      • The std::vector contains responses from all matching service instances.
      • In the typed version, the vector holds TMsgServiceResponse<ResponseT>.
  • Callback Calls:

    • Both synchronous and asynchronous callback-based calls have been updated:
      • The untyped variant uses the legacy ResponseCallbackT to deliver the raw response.
      • The typed variant uses TMsgResponseCallbackT<ResponseT> so that the client callback receives an automatically parsed, strongly typed response.
  • Internal helper functions (such as those used in ProcessInstances) have been refactored to minimize code duplication and simplify the overall implementation.

5. Code Duplication Reduction and Better Maintainability

  • Common response processing logic has been factored out into helper functions, reducing code duplication across different call variants.
  • The overall structure is cleaner and more modular, which should ease future maintenance and extension of the API.

Backwards Compatibility

  • While the legacy v5 API overloads (which used raw serialized strings and generic responses) have been replaced, the overall behavior remains consistent.
  • Existing code can be migrated by switching to the new templated call methods:
    • Use CServiceClient<T> for untyped calls.
    • Use CServiceClientTyped<T> for strongly typed calls.
  • The new API retains support for both blocking (synchronous) and callback-based (asynchronous) service calls.

Impact on Client Code

Client applications will now benefit from improved type safety and easier access to service responses. For example:

Using the Typed Service Client

// Using the CallWithCallback variant:

{
  std::cout << "Calling MathService::Add (callback) with inputs: "
            << math_request.inp1() << " and " << math_request.inp2() << std::endl;
  if (!math_client.CallWithCallback<SFloat>("Add", math_request, OnMathResponse))
  {
    std::cout << "MathService::Add method call (callback) failed." << std::endl;
  }
}

// Using the CallWithResponse (blocking) variant:

{
  std::cout << "Calling MathService::Multiply (blocking) with inputs: "
            << math_request.inp1() << " and " << math_request.inp2() << std::endl;
  auto multiply_response = math_client.CallWithResponse<SFloat>("Multiply", math_request);
  if (multiply_response.first)
  {
    for (const auto& resp : multiply_response.second)
    {
      if (resp.response)
      {
        std::cout << "Received typed response: " << resp.response->out() << std::endl;
      }
      else
      {
        std::cout << "Error in response: " << resp.error_msg << std::endl;
      }
    }
  }
  else
  {
    std::cout << "MathService::Multiply call failed." << std::endl;
  }
}

Iterating Over Client Instances

// Iterative CallWithCallback on each client instance:

{
  auto instances = math_client.GetClientInstances();
  for (auto& instance : instances)
  {
    if (!instance.CallWithCallback<SFloat>("Divide", math_request, OnMathResponse))
    {
      std::cout << "MathService::Divide call on an instance failed." << std::endl;
    }
  }
}

// Iterative blocking calls:

{
  auto instances = math_client.GetClientInstances();
  for (auto& instance : instances)
  {
    auto divide_response = instance.CallWithResponse<SFloat>("Divide", math_request);
    if (divide_response.first)
    {
      std::cout << "Received typed response for Divide: " << divide_response.second.response->out() << std::endl;
    }
    else
    {
      std::cout << "MathService::Divide call failed: " << divide_response.second.error_msg << std::endl;
    }
  }
}

Callback Example

void OnMathResponse(const eCAL::protobuf::TMsgServiceResponse<SFloat>& service_response)
{
  const auto& method_name = service_response.service_method_information.method_name;
  const auto& host_name   = service_response.server_id.service_id.host_name;
  const int32_t process_id = service_response.server_id.service_id.process_id;

  if (service_response.call_state == eCAL::eCallState::executed && service_response.response)
  {
    std::cout << "Callback: " << method_name << " returned " 
              << service_response.response->out()
              << " from host " << host_name << " (pid " << process_id << ")" << std::endl;
  }
  else
  {
    std::cout << "Callback error for " << method_name << ": "
              << service_response.error_msg << " from host " << host_name << std::endl;
  }
}

Summary

  • Enhanced API:
    The new design cleanly separates typed and untyped responses and introduces a unified base class for service clients.

  • Stronger Type Safety:
    With templated call methods, client code can directly work with strongly typed responses.

  • Modular and Maintainable:
    The client instance wrappers and common helper functions reduce code duplication and improve maintainability.

  • Seamless Migration:
    Although the legacy API has been replaced, the overall behavior remains consistent. Existing client code can be updated by migrating to the new templated call methods.

@rex-schilasky rex-schilasky added the cherry-pick-to-NONE Don't cherry-pick these changes label Jan 30, 2025
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

There were too many comments to post at once. Showing the first 20 out of 21. Check the log or trigger a new build to see more.

app/sys/sys_core/src/connection/remote_connection.cpp Outdated Show resolved Hide resolved
{
public:
// Constructors
CClientInstance(eCAL::CClientInstance&& base_instance_) noexcept
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: rvalue reference parameter 'base_instance_' is never moved from inside the function body [cppcoreguidelines-rvalue-reference-param-not-moved]

      CClientInstance(eCAL::CClientInstance&& base_instance_) noexcept
                                              ^

@FlorianReimold FlorianReimold added this to the eCAL 6 milestone Feb 6, 2025
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

@hannemn hannemn linked an issue Feb 6, 2025 that may be closed by this pull request
6 tasks
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

@rex-schilasky rex-schilasky marked this pull request as ready for review February 6, 2025 10:19
Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions


#pragma once

#include <ecal/service/client.h>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: 'ecal/service/client.h' file not found [clang-diagnostic-error]

#include <ecal/service/client.h>
         ^


ServiceMethodInformationSetT method_information_set;
CProtoDynDecoder dyn_decoder;
std::string error_s;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'error_s' is not initialized [cppcoreguidelines-init-variables]

Suggested change
std::string error_s;
std::string error_s = 0;

const std::string& request_type_name = method_descriptor->input_type()->name();
const std::string& response_type_name = method_descriptor->output_type()->name();

std::string request_type_descriptor;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'request_type_descriptor' is not initialized [cppcoreguidelines-init-variables]

Suggested change
std::string request_type_descriptor;
std::string request_type_descriptor = 0;

const std::string& response_type_name = method_descriptor->output_type()->name();

std::string request_type_descriptor;
std::string response_type_descriptor;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'response_type_descriptor' is not initialized [cppcoreguidelines-init-variables]

Suggested change
std::string response_type_descriptor;
std::string response_type_descriptor = 0;

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions


static eCAL::rec::Error CallRemoteEcalrecService(const std::shared_ptr<eCAL::protobuf::CServiceClient<eCAL::pb::rec_server::EcalRecServerService>>& remote_ecalsys_service
static eCAL::rec::Error CallRemoteEcalrecService(const std::shared_ptr<eCAL::protobuf::CServiceClientUntypedCallback<eCAL::pb::rec_server::EcalRecServerService>>& remote_ecalsys_service
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: function 'eCAL::rec_cli::command::Command::CallRemoteEcalrecService' has a definition with different parameter names [readability-inconsistent-declaration-parameter-name]

        static eCAL::rec::Error CallRemoteEcalrecService(const std::shared_ptr<eCAL::protobuf::CServiceClientUntypedCallback<eCAL::pb::rec_server::EcalRecServerService>>& remote_ecalsys_service
                                ^
Additional context

app/rec/rec_server_cli/src/commands/command.cpp:75: the definition seen here

      eCAL::rec::Error Command::CallRemoteEcalrecService(const std::shared_ptr<eCAL::protobuf::CServiceClientUntypedCallback<eCAL::pb::rec_server::EcalRecServerService>>& remote_ecalrec_service
                                ^

app/rec/rec_server_cli/src/commands/command.h:63: differing parameters are named here: ('remote_ecalsys_service'), in definition: ('remote_ecalrec_service')

        static eCAL::rec::Error CallRemoteEcalrecService(const std::shared_ptr<eCAL::protobuf::CServiceClientUntypedCallback<eCAL::pb::rec_server::EcalRecServerService>>& remote_ecalsys_service
                                ^

@@ -119,7 +119,7 @@ bool IsBuiltInFtpServerBusy(bool print_status = false);

// Rec Sever instance and rec_server_service. We will only use one of those, depending on the remote-control setting
std::shared_ptr<eCAL::rec_server::RecServer> rec_server_instance;
std::shared_ptr<eCAL::protobuf::CServiceClient<eCAL::pb::rec_server::EcalRecServerService>> remote_rec_server_service;
std::shared_ptr<eCAL::protobuf::CServiceClientUntypedCallback<eCAL::pb::rec_server::EcalRecServerService>> remote_rec_server_service;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'remote_rec_server_service' is non-const and globally accessible, consider making it const [cppcoreguidelines-avoid-non-const-global-variables]

std::shared_ptr<eCAL::protobuf::CServiceClientUntypedCallback<eCAL::pb::rec_server::EcalRecServerService>> remote_rec_server_service;
                                                                                                           ^

@@ -29,15 +29,14 @@ int main()
// initialize eCAL API
eCAL::Initialize("orchestrator");

eCAL::protobuf::CServiceClient<orchestrator::ComponentService> component1("component1");
eCAL::protobuf::CServiceClient<orchestrator::ComponentService> component2("component2");
eCAL::protobuf::CServiceClientTypedResponse<orchestrator::ComponentService> component1("component1");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'component1' of type 'eCAL::protobuf::CServiceClientTypedResponseorchestrator::ComponentService' can be declared 'const' [misc-const-correctness]

Suggested change
eCAL::protobuf::CServiceClientTypedResponse<orchestrator::ComponentService> component1("component1");
eCAL::protobuf::CServiceClientTypedResponse<orchestrator::ComponentService> const component1("component1");

eCAL::protobuf::CServiceClient<orchestrator::ComponentService> component1("component1");
eCAL::protobuf::CServiceClient<orchestrator::ComponentService> component2("component2");
eCAL::protobuf::CServiceClientTypedResponse<orchestrator::ComponentService> component1("component1");
eCAL::protobuf::CServiceClientTypedResponse<orchestrator::ComponentService> component2("component2");
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'component2' of type 'eCAL::protobuf::CServiceClientTypedResponseorchestrator::ComponentService' can be declared 'const' [misc-const-correctness]

Suggested change
eCAL::protobuf::CServiceClientTypedResponse<orchestrator::ComponentService> component2("component2");
eCAL::protobuf::CServiceClientTypedResponse<orchestrator::ComponentService> const component2("component2");

@@ -89,8 +92,7 @@ int main()
eCAL::Initialize("ecalplayer client");

// create player service client
eCAL::protobuf::CServiceClient<eCAL::pb::play::EcalPlayService> player_service;
player_service.AddResponseCallback(OnPlayerResponse);
eCAL::protobuf::CServiceClientUntypedCallback<eCAL::pb::play::EcalPlayService> player_service;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: variable 'player_service' of type 'eCAL::protobuf::CServiceClientUntypedCallbackeCAL::pb::play::EcalPlayService' can be declared 'const' [misc-const-correctness]

Suggested change
eCAL::protobuf::CServiceClientUntypedCallback<eCAL::pb::play::EcalPlayService> player_service;
eCAL::protobuf::CServiceClientUntypedCallback<eCAL::pb::play::EcalPlayService> const player_service;

{
public:
using CServiceClientBase<T>::CServiceClientBase;
virtual ~CServiceClientCallbackBase() override = default;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: 'virtual' is redundant since the function is already declared 'override' [cppcoreguidelines-explicit-virtual-functions]

Suggested change
virtual ~CServiceClientCallbackBase() override = default;
;


#pragma once

#include <ecal/service/types.h>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: 'ecal/service/types.h' file not found [clang-diagnostic-error]

#include <ecal/service/types.h>
         ^

* @tparam ResponseT The expected protobuf response type.
*/
template <typename ResponseT>
struct TMsgServiceResponse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: constructor does not initialize these fields: server_id, service_method_information, error_msg [cppcoreguidelines-pro-type-member-init]

serialization/protobuf/protobuf/include/ecal/msg/protobuf/client_protobuf_types.h:50:

-       SServiceId                         server_id;                     //!< Identifier of the server that executed the call
-       SServiceMethodInformation          service_method_information;    //!< Information about the called method
+       SServiceId                         server_id{};                     //!< Identifier of the server that executed the call
+       SServiceMethodInformation          service_method_information{};    //!< Information about the called method

serialization/protobuf/protobuf/include/ecal/msg/protobuf/client_protobuf_types.h:54:

-       std::string                        error_msg;                     //!< Error message if the call failed
+       std::string                        error_msg{};                     //!< Error message if the call failed

* This class will serve as the parent for the typed and untyped variants.
*/
template <typename T>
class CServiceClientResponseBase : public CServiceClientBase<T>
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: class 'CServiceClientResponseBase' defines a default destructor but does not define a copy constructor, a copy assignment operator, a move constructor or a move assignment operator [cppcoreguidelines-special-member-functions]

>
            ^

{
public:
using CServiceClientBase<T>::CServiceClientBase; // Inherit constructors
virtual ~CServiceClientResponseBase() override = default;
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: 'virtual' is redundant since the function is already declared 'override' [cppcoreguidelines-explicit-virtual-functions]

Suggested change
virtual ~CServiceClientResponseBase() override = default;
s

Copy link
Contributor

@github-actions github-actions bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

clang-tidy made some suggestions

{
public:
// Constructors
CClientInstanceTyped(eCAL::CClientInstance&& base_instance_) noexcept
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: rvalue reference parameter 'base_instance_' is never moved from inside the function body [cppcoreguidelines-rvalue-reference-param-not-moved]

      CClientInstanceTyped(eCAL::CClientInstance&& base_instance_) noexcept
                                                   ^

{
}

CClientInstanceTyped(const SEntityId& entity_id_, const std::shared_ptr<eCAL::CServiceClientImpl>& service_client_impl_)
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: constructor does not initialize these fields: m_instance [cppcoreguidelines-pro-type-member-init]

      CClientInstanceTyped(const SEntityId& entity_id_, const std::shared_ptr<eCAL::CServiceClientImpl>& service_client_impl_)
      ^

* @tparam ResponseT The expected protobuf response type.
*/
template <typename ResponseT>
struct TMsgServiceResponse
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

warning: constructor does not initialize these fields: server_id, service_method_information, error_msg [cppcoreguidelines-pro-type-member-init]

serialization/protobuf/protobuf/include/ecal/msg/protobuf/client_protobuf_types.h:50:

-       SServiceId                  server_id;                     //!< Identifier of the server that executed the call
-       SServiceMethodInformation   service_method_information;    //!< Information about the called method
+       SServiceId                  server_id{};                     //!< Identifier of the server that executed the call
+       SServiceMethodInformation   service_method_information{};    //!< Information about the called method

serialization/protobuf/protobuf/include/ecal/msg/protobuf/client_protobuf_types.h:54:

-       std::string                 error_msg;                     //!< Error message if the call failed
+       std::string                 error_msg{};                     //!< Error message if the call failed

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
cherry-pick-to-NONE Don't cherry-pick these changes
Projects
None yet
Development

Successfully merging this pull request may close these issues.

eCAL 6 Serialization / Msg API Review
2 participants