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

RSDK-4573: remove proto types from robot client apis #207

Conversation

purplenicole730
Copy link
Member

@purplenicole730 purplenicole730 commented Jan 28, 2024

Jira Ticket

Removed the proto types that were in the robot client API's:

  • defined new discovery_query, discovery, frame_system_config, status, and operation
  • changed robot client API's to use already defined WorldState::transform and Name
  • defined proto conversion methods in a private namespace in robot/client.cpp
  • created functions to return custom defined types while keeping old proto functions for MockRobotService
  • added new tests to compare mock proto and custom methods to ensure same fields
  • changed test_robot.cpp to test custom types
  • added a new Impl object to handle robot::v1::RobotService

@purplenicole730 purplenicole730 marked this pull request as ready for review February 2, 2024 20:26
@purplenicole730 purplenicole730 requested a review from a team as a code owner February 2, 2024 20:26
@purplenicole730 purplenicole730 requested review from stuqdog, benjirewis and acmorrow and removed request for a team February 2, 2024 20:26
Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

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

Lots of comments and ideas, but I think this is headed in a good direction. @stuqdog I have some questions here and there for you.

In some places I've left notes on existing code that looks like it needs help or can be improved. Those things don't necessarily need to be acted on to land this review but if they aren't they should probably be ticketed and TODOs added to the review.

src/viam/sdk/common/utils.cpp Show resolved Hide resolved
.append(resource_subtype)
.append("/")
.append(resource->name());
resource_names.push_back(Name::from_string(name_str));
Copy link
Member

Choose a reason for hiding this comment

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

So, Name::from_string actually just does splits on \ and :, so more or less this string is being built up just to be decomposed. Could you just do

resource_names.push_back({{kRDK, std::move(resource_type), std::move(resource_subtype)}, resource->name()})

or similar? I don't think that quite works because of something about name vs remote_name in the Name::Name constructor, which I'm not quite sure what the difference is. But maybe the constructor needs a little help.

}
static bool check_equal(const ResourceName r1, const ResourceName r2) {
return r1.SerializeAsString().compare(r2.SerializeAsString());
static bool check_equal(const Name r1, const Name r2) {
Copy link
Member

Choose a reason for hiding this comment

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

These parameters should also be by const reference, while you are here.

Copy link
Member

Choose a reason for hiding this comment

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

Actually is ResourceNameEqual even still needed? Name defines operator==.


class ResourceNameHasher {
public:
size_t operator()(ResourceName const& key) const {
return std::hash<std::string>()(key.SerializeAsString());
size_t operator()(Name const& key) const {
Copy link
Member

Choose a reason for hiding this comment

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

This class should probably be renamed to NameHasher

Copy link
Member

Choose a reason for hiding this comment

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

@stuqdog - If Names as keys in unordered_maps will almost always use this hasher, we could make life a little simpler by doing this:

...
}  // namespace viam
namespace std {
template<>
struct hash<::viam::sdk::Name>
{
    size_t operator()(const ::viam::sdk::Name& name) const noexcept {
        return name.hash();
    }
};
} // namespace std

And then add size_t Name::hash() const noexcept to the Name class. Then ResourceNameHasher just goes away, and you can use unordered_map<Name, T> without specifying the hash.

Copy link
Member

Choose a reason for hiding this comment

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

Yep I completely agree. In fact it looks like we already define hash for Name in resource_api.hpp so I think we can just delete the ResourceNameEqual and ResourceNameHasher classes entirely and just be done with it, they were only ever necessary (if at all) because ResourceName was defined in proto.

using viam::robot::v1::Status;
using time_point = std::chrono::time_point<long long, std::chrono::nanoseconds>;

struct discovery_query {
Copy link
Member

Choose a reason for hiding this comment

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

I feel like these types belong within Robot (which apparently doesn't exist?). @stuqdog should there be a Robot base class with an abstract API? Or is there no purpose / need for such a thing.

Either way, I think these types should not be at the ::viam::sdk:: level and should be inside RobotClient in the absence of Robot.

Copy link
Member

Choose a reason for hiding this comment

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

Hmm... I personally am not a fan of having a Robot base class. In my mind there's only a single canonical implementation of how one should interact with a robot as a client in C++, and so it doesn't really make sense to expose an abstract API the same way it does for the various resources.

That said, there's something very slightly smelly about having these types exist on RobotClient (as opposed to Robot) that I'm having trouble putting my finger on. So, I'm certainly open to being convinced that an abstract Robot base class makes sense.

Either way, I agree that these structs should exist within RobotClient (or Robot if we create it) rather than as free-floating types.

Copy link
Member

Choose a reason for hiding this comment

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

Yes I share your unease, and I find the lack of consistency between Robot and the other resource types a little troublesome as well. Also unclear what the use case is for RobotService_. I can't quite tell what it is for, which makes it hard for me to honestly evaluate whether a Robot base is needed. It feels like if RobotService_ is meaningful, shouldn't be what it is serving be an implementation of Robot, not a RobotClient?

/// @param extra Any extra params to pass to resources' `stop` methods, keyed by `ResourceName`.
void stop_all(std::unordered_map<ResourceName,
/// @param extra Any extra params to pass to resources' `stop` methods, keyed by `Name`.
void stop_all(std::unordered_map<Name,
Copy link
Member

Choose a reason for hiding this comment

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

Huh. So the second parameter here is like an unwrapped AttributeMap that's not held by shared_ptr. Not asking you to change it, but I think the AttributeMap infra is something we should look at cleaning up.

Copy link
Member

Choose a reason for hiding this comment

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

Oh good catch! I actually think it would be great to change this to an AttributeMap now while we're already changing the RobotClient API considerably.


cout << "Resources of the robot:" << endl;
for (vs::ResourceName resource : *resource_names) {
cout << " - " << resource.name() << " (" << resource.subtype() << ")" << endl;
for (vs::Name resource : *resource_names) {
Copy link
Member

Choose a reason for hiding this comment

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

for (const auto& resource : *resource_names) while you are here.

@@ -21,8 +21,8 @@ std::string Resource::name() const {

void Resource::reconfigure(Dependencies deps, ResourceConfig cfg){};

ResourceName Resource::get_resource_name(std::string name) {
ResourceName r;
common::v1::ResourceName Resource::get_resource_name(std::string name) {
Copy link
Member

Choose a reason for hiding this comment

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

You can bring this in with using since you are in .cpp scope, if you like.

@@ -57,6 +47,110 @@ using viam::robot::v1::Status;
// NOLINTNEXTLINE
const std::string kStreamRemoved("Stream removed");

viam::robot::v1::DiscoveryQuery discovery_query::to_proto() const {
Copy link
Member

Choose a reason for hiding this comment

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

@stuqdog - I'm considering whether we want some sort of traits class for proto to type interconversion. Think From/Into sort of thing. It'd let us use a uniform syntax for moving between our vocabulary types and the backing protos. I don't have syntax in mind yet, but any thoughts?

Copy link
Member

Choose a reason for hiding this comment

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

I like this. I don't think it's urgent but programmatically enforcing a consistent syntax and having a way of clearly expressing in code which types we expect to have proto conversions seems like a win. I'm not positive I have the look of it precisely right in my head, something like the following?

class ProtoConvertible {
  public:
   template <typename T, typename ProtoT>
   struct traits;

   template <typename T, typename ProtoT>
   static T from_proto(const ProtoT& proto) {
     return traits<T, ProtoT>::from_proto(proto);
   }

   template <typename T, typename ProtoT>
   static ProtoT to_proto(const T& t) const {
    return traits<T, ProtoT>::to_proto(t);
  }
...
}

and then define in robot_client.hpp

template <>
struct ProtoConvertible::traits<frame_system_config, FrameSystemConfig> {
  static FrameSystemConfig to_proto(const frame_system_config& fsc);
  static frame_system_config to_proto(const FrameSystemConfig& proto);
}
class RobotClient {
  ...
  struct frame_system_config {
   ...
   FrameSystemConfig to_proto() const {
    return ProtoConvertible::to_proto<frame_system_config, FrameSystemConfig>(this);
   } // actual implementation should go in .cpp

   static frame_system_config from_proto(const FrameSystemConfig& proto) {
    return ProtoConvertible::from_proto<frame_system_config, FrameSystemConfig>(proto);
   } // actual implementation should go in .cpp
  }
...
}

And then implement all the actual logic in (for this example) robot_client.cpp?

Assuming I've got the gist of it right here, it seems like a bit of boilerplate but probably worthwhile for clarity and consistency.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think that's along the lines of what I had in mind. I filed a ticket so we don't forget - it definitely isn't work to be done in this review.

const std::lock_guard<std::mutex> lock(lock_);
std::vector<ResourceName>* resources = &resource_names_;
std::vector<Name>* resources = &resource_names_;
Copy link
Member

Choose a reason for hiding this comment

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

Oh, this is worse than I thought. The lock is completely defeated here because the returned pointer is to internal state. This needs to return by value.

Copy link
Member

Choose a reason for hiding this comment

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

Not entirely certain I follow why this needs to return by value, does changing return type to const& not solve this? If so, then returning a copy seems totally fine.

Copy link
Member

Choose a reason for hiding this comment

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

No, returning by const& isn't sufficient, because you could still be doing an (unlocked) read of the state via the reference at the same time a locked mutation was in progress, since the thread holding the reference doesn't hold the lock.

Copy link
Member

Choose a reason for hiding this comment

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

Ahh sure, that makes sense. Thanks for clarifying! In that case then yeah, let's change this to return a copy.

Copy link
Member

@stuqdog stuqdog left a comment

Choose a reason for hiding this comment

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

Nice, excited to see this happen! Got some general suggestions/comments/questions but overall this definitely is looking good.

src/viam/sdk/common/utils.cpp Outdated Show resolved Hide resolved
src/viam/sdk/common/utils.hpp Outdated Show resolved Hide resolved
src/viam/sdk/tests/mocks/mock_robot.cpp Outdated Show resolved Hide resolved
src/viam/sdk/tests/mocks/mock_robot.hpp Outdated Show resolved Hide resolved
src/viam/sdk/robot/service.hpp Outdated Show resolved Hide resolved
src/viam/sdk/robot/client.hpp Outdated Show resolved Hide resolved
src/viam/sdk/robot/client.hpp Outdated Show resolved Hide resolved
src/viam/sdk/robot/client.hpp Outdated Show resolved Hide resolved
src/viam/sdk/robot/client.hpp Outdated Show resolved Hide resolved
*r.mutable_type() = api.resource_type();
*r.mutable_subtype() = api.resource_subtype();
*r.mutable_name() = std::move(name);
Name r = Name::from_string(api.to_string() + "/" + name);
Copy link
Member

Choose a reason for hiding this comment

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

This could be better written as Name r(api, "", name);

@stuqdog
Copy link
Member

stuqdog commented Feb 2, 2024

Lots of comments and ideas, but I think this is headed in a good direction. @stuqdog I have some questions here and there for you.

In some places I've left notes on existing code that looks like it needs help or can be improved. Those things don't necessarily need to be acted on to land this review but if they aren't they should probably be ticketed and TODOs added to the review.

👍 I'll take a look through your comments and reply on Monday!

Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

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

Just following up on a few comment threads, I'll take another look at this when updates have been pushed.

@@ -57,6 +47,110 @@ using viam::robot::v1::Status;
// NOLINTNEXTLINE
const std::string kStreamRemoved("Stream removed");

viam::robot::v1::DiscoveryQuery discovery_query::to_proto() const {
Copy link
Member

Choose a reason for hiding this comment

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

Yes, I think that's along the lines of what I had in mind. I filed a ticket so we don't forget - it definitely isn't work to be done in this review.

src/viam/sdk/robot/client.hpp Outdated Show resolved Hide resolved
using viam::robot::v1::Status;
using time_point = std::chrono::time_point<long long, std::chrono::nanoseconds>;

struct discovery_query {
Copy link
Member

Choose a reason for hiding this comment

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

Yes I share your unease, and I find the lack of consistency between Robot and the other resource types a little troublesome as well. Also unclear what the use case is for RobotService_. I can't quite tell what it is for, which makes it hard for me to honestly evaluate whether a Robot base is needed. It feels like if RobotService_ is meaningful, shouldn't be what it is serving be an implementation of Robot, not a RobotClient?

@@ -68,10 +119,10 @@ class RobotClient {
static std::shared_ptr<RobotClient> with_channel(std::shared_ptr<ViamChannel> channel,
Options options);
RobotClient(std::shared_ptr<ViamChannel> channel);
std::vector<ResourceName>* resource_names();
std::vector<Name>* resource_names();
Copy link
Member

Choose a reason for hiding this comment

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

See the other thread about this on the implementation but it definitely needs to be a copy.

Copy link
Member

@stuqdog stuqdog left a comment

Choose a reason for hiding this comment

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

Nice, definitely looking better! A couple general comments (including one about proto leakage in robot/client.hpp that may or may not be out of scope for this PR), but I'm really liking the way this is shaping up. Thanks!

@@ -44,11 +44,12 @@ int main() {
throw;
}

std::vector<vs::ResourceName>* resource_names = robot->resource_names();
std::vector<vs::Name>* resource_names = robot->resource_names();
Copy link
Member

Choose a reason for hiding this comment

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

Looks like this method is still returning a reference. We'll want to make a copy of the robot's resource_names and return that by value (i.e., not as a pointer).

Copy link
Member Author

Choose a reason for hiding this comment

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

Does this mean simply returning a std::vector<Name>?

Comment on lines 45 to 47
for (const Name& resource : *resource_names) {
std::cout << "\tname: " << resource.name() << " (type:" << resource.api().resource_type()
<< " subtype:" << resource.api().resource_subtype() << ")" << std::endl;
Copy link
Member

Choose a reason for hiding this comment

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

Given that we implement << for Name, maybe we can simplify this and just say std::cout << "\t" << name << "\n";

Comment on lines 47 to 49
for (const Name& resource : *resource_names) {
std::cout << "\tname: " << resource.name() << " (type:" << resource.api().resource_type()
<< " subtype:" << resource.api().resource_subtype() << ")" << std::endl;
Copy link
Member

Choose a reason for hiding this comment

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

Ditto above comment on Name implementing <<

src/viam/sdk/common/utils.cpp Show resolved Hide resolved
*r.mutable_subtype() = resource_subtype;
resource_names.push_back(r);
resource_names.push_back(
Name({kRDK, resource_type, resource_subtype}, "", resource->name()));
Copy link
Member

Choose a reason for hiding this comment

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

No need to change this, but fyi I don't think you even need to declare the type Name here. you could just push back {{kRDK, resource_type, resource_subtype}, "", resource->name()}

@@ -68,10 +119,10 @@ class RobotClient {
static std::shared_ptr<RobotClient> with_channel(std::shared_ptr<ViamChannel> channel,
Options options);
RobotClient(std::shared_ptr<ViamChannel> channel);
std::vector<ResourceName>* resource_names();
std::vector<Name>* resource_names();
Copy link
Member

Choose a reason for hiding this comment

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

@purplenicole730 per above comment, we should definitely update this to not return a pointer.

Comment on lines 45 to 46
viam::robot::v1::DiscoveryQuery to_proto() const;
static discovery_query from_proto(const viam::robot::v1::DiscoveryQuery& proto);
Copy link
Member

Choose a reason for hiding this comment

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

It occurs to me that the goal of this PR is to remove proto types from the robot_client APIs, but these structs are reintroducing a proto dependency. This isn't great! I wanna think about the best way to go about resolving this... perhaps it is best to introduce a Robot base class after all. We can define the structs in Robot, and then define their to_proto/from_proto methodology in RobotClient.

Another possibility: I believe the only users of the to_proto/from_proto methods here are the robot tests. If that is correct (which will need to be confirmed). That means possibly we could remove those tests, or have them include robot/client.*cpp* rather than robot/client.hpp in order to access methods not exposed in the .hpp. Honestly both these options seem pretty bad and probably worth just discarding (especially when the Robot base class option exists), but I'm curious if others have thoughts or other proposals.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, I mentioned this in an earlier round of review too. Ultimately, this review isn't currently achieving the stated goal of removing the proto types from the Robot API: it has just pushed them into the to_proto and from_proto methods on the nested types.

One thing you could do is just make all of them into free functions in an unnamed namespace inside robot.cpp:

// robot.cpp
...
namespace {
viam::robot::v1::DiscoveryQuery to_proto(const RobotClient::discovery_query& dq) { ... };
discovery_query from_proto(const viam::robot::v1::DiscoveryQuery& proto);

// Repeat for `discovery`, `frame_system_config`, etc.

}  // namespace

This will work very well for implementing Robot and wouldn't force the introduction of a base class for Robot.

However, it comes with one notable downside, which is that the conversion functions are no longer available to the tests.

That's frustrating, but maybe not the end of the world. After all, the tests are perfectly able to traffic in the proto types. So if you can arrange a suitable situation via mocks, then you can test indirectly: call the API with the vocabulary type, collect the proto object in the mock, and compare. Then do the reverse.

After all, the whole point of having a class like Robot is to encapsulate the proto stuff away and provide a better UX to consumers.

Copy link
Member Author

@purplenicole730 purplenicole730 Feb 10, 2024

Choose a reason for hiding this comment

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

So the move seems to be to make a base class Robot in a new robot.cpp file. As for testing, test_robot doesn't use the protos, but test_types does.

@acmorrow When you say "collect the proto object in the mock, and compare", do you mean having test_types include the robot.cpp file that has the base Robot class with all the to/from proto functions? I changed the mocks so that they return the custom defined types, but I can easily define them in test_type.

Copy link
Member

Choose a reason for hiding this comment

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

No, let's stay away from #includeing .cpp files: it is a quick way to end up with multiply defined symbols.

Instead, my suggestion is to make all of the proto conversion functions private to the robot.cpp file. Tests will no longer be able to explicitly test them.

Instead, use the "mock pipeline" approach to connect the RobotClient to a mock. Then, inside the mock, you can validate that the vocabulary types work correctly end-to-end without ever using the proto types directly: that'll happen only in the RobotClient code. So the testing will still happen, but it will be indirect.

@stuqdog - Does that make sense to you?

Copy link
Member

Choose a reason for hiding this comment

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

Yep, completely agree.

Copy link
Member

@stuqdog stuqdog Feb 12, 2024

Choose a reason for hiding this comment

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

Actually to clarify: I think the proto conversion functions can't be just private in robot.cpp if Robot is an abstract base class. Instead I think they should be private in robot/client.cpp where they are actually being used. So I think we should define the types in robot.hpp, and then add proto conversion methods privately in the client.

Copy link
Member

Choose a reason for hiding this comment

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

But, there isn't currently a robot.hpp or a Robot base class right now, is there? Do you think adding one should be part of this review? I think we can get away without it?

Copy link
Member

Choose a reason for hiding this comment

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

Yep agreed, a project for a future ticket.

@@ -155,9 +197,10 @@ class RobotClient {
std::shared_ptr<ViamChannel> viam_channel_;
std::shared_ptr<Channel> channel_;
bool should_close_channel_;
std::unique_ptr<RobotService::Stub> stub_;
struct Impl;
Copy link
Member

Choose a reason for hiding this comment

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

@acmorrow a question for you! you proposed naming this struct Impl but that naively seems to go against our standard naming convention for structs (that they be snake_case). I realize this is a bit of a unique case in the SDK (Impl is private while structs in general are defined by their public-ness, and this is our only current instance of the pimpl idiom). Is it more idiomatic given these distinctions to break the normal naming convention and call it Impl, or keep with the struct naming convention and call it impl?

Copy link
Member

Choose a reason for hiding this comment

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

Oh, I'm just typing from muscle memory there. Fine to call it impl I think.

I truly wish we could bulk convert the whole SDK to snake_case.

Comment on lines 43 to 51
using viam::common::v1::PoseInFrame;
using viam::common::v1::ResourceName;
using viam::common::v1::Transform;
using viam::robot::v1::Discovery;
using viam::robot::v1::DiscoveryQuery;
using viam::robot::v1::FrameSystemConfig;
using viam::robot::v1::Operation;
using viam::robot::v1::RobotService;
using viam::robot::v1::Status;
Copy link
Member

Choose a reason for hiding this comment

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

Not a big deal either way, but I think maintaining these using lines in the .cpp file (as opposed to the .hpp) is actually fine and may be worth doing if typing out the fully qualified type becomes tedious.

*r.mutable_type() = api.resource_type();
*r.mutable_subtype() = api.resource_subtype();
*r.mutable_name() = std::move(name);
Name r = Name(api, "", name);
Copy link
Member

Choose a reason for hiding this comment

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

(minor) I think we still want to update this to Name r(api, "", name);. I'm a little fuzzy on some of these details but I believe what we have now uses the copy constructor (i.e., we're making a Name, and then copying it and assigning r to equal that copy), where as Name r(api, "", name) only constructs one Name. I could be wrong given that we're dealing with an rvalue Name though! Happy for @acmorrow to come in and correct me on this one :)

Copy link
Member

Choose a reason for hiding this comment

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

You could just write it Name r{api, "", name}, but I don't think you even need r anymore. I'd expect the following to work fine:

auto resource = this->resource_by_name({API::get<T>(), "", std::move(name)});

Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

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

I'm in agreement with all of @stuqdog's comments and thoughts. I've shared a few of my own around the issue of the visibility of the proto conversion functions.

src/viam/sdk/common/utils.cpp Show resolved Hide resolved
}

struct RobotClient::Impl {
Impl(std::unique_ptr<RobotService::Stub> stub) : stub_(std::move(stub)) {}
Copy link
Member

Choose a reason for hiding this comment

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

Did you try this as Impl(RobotService::Stub&& stub) : stub_(std::move(stub)) {} per my other comment?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, after making it so that stub_ is no longer a pointer, it then gives me an error like so:

note: in instantiation of function template specialization 'std::make_unique<viam::sdk::RobotClient::impl, std::unique_ptr<viam::robot::v1::RobotService::Stub>>' requested here
      impl_(std::make_unique<impl>(RobotService::NewStub(channel_))) {}
                 ^

called when we try to call:

RobotClient::RobotClient(std::shared_ptr<ViamChannel> channel)
    : viam_channel_(channel),
      channel_(channel->channel()),
      should_close_channel_(false),
      impl_(std::make_unique<impl>(RobotService::NewStub(channel_))) {}

using viam::robot::v1::Discovery;
using viam::robot::v1::DiscoveryQuery;
using viam::robot::v1::FrameSystemConfig;
using viam::robot::v1::Operation;
using viam::robot::v1::RobotService;
Copy link
Member

Choose a reason for hiding this comment

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

Do you still need v1::RobotService here?

Copy link
Member Author

Choose a reason for hiding this comment

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

Yes, we call it quite often, both to call the RobotService::NewStub function, or just as a type RobotService::Stub.

Copy link
Member

Choose a reason for hiding this comment

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

Apologies, I meant to leave that comment on the client.hpp header, not the client.cpp file. I will re-add the comment in the right place.

src/viam/sdk/common/utils.cpp Show resolved Hide resolved
Comment on lines 45 to 46
viam::robot::v1::DiscoveryQuery to_proto() const;
static discovery_query from_proto(const viam::robot::v1::DiscoveryQuery& proto);
Copy link
Member

Choose a reason for hiding this comment

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

Yes, I mentioned this in an earlier round of review too. Ultimately, this review isn't currently achieving the stated goal of removing the proto types from the Robot API: it has just pushed them into the to_proto and from_proto methods on the nested types.

One thing you could do is just make all of them into free functions in an unnamed namespace inside robot.cpp:

// robot.cpp
...
namespace {
viam::robot::v1::DiscoveryQuery to_proto(const RobotClient::discovery_query& dq) { ... };
discovery_query from_proto(const viam::robot::v1::DiscoveryQuery& proto);

// Repeat for `discovery`, `frame_system_config`, etc.

}  // namespace

This will work very well for implementing Robot and wouldn't force the introduction of a base class for Robot.

However, it comes with one notable downside, which is that the conversion functions are no longer available to the tests.

That's frustrating, but maybe not the end of the world. After all, the tests are perfectly able to traffic in the proto types. So if you can arrange a suitable situation via mocks, then you can test indirectly: call the API with the vocabulary type, collect the proto object in the mock, and compare. Then do the reverse.

After all, the whole point of having a class like Robot is to encapsulate the proto stuff away and provide a better UX to consumers.

@@ -155,9 +197,10 @@ class RobotClient {
std::shared_ptr<ViamChannel> viam_channel_;
std::shared_ptr<Channel> channel_;
bool should_close_channel_;
std::unique_ptr<RobotService::Stub> stub_;
struct Impl;
Copy link
Member

Choose a reason for hiding this comment

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

Oh, I'm just typing from muscle memory there. Fine to call it impl I think.

I truly wish we could bulk convert the whole SDK to snake_case.

Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

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

This is coming along well.

Please take care of the items I've called out here, and then I think it is time to tackle moving the to_proto and from_proto methods out of the header and refactoring the testing to use a mock pipeline per discussion.

src/viam/examples/dial/example_dial.cpp Show resolved Hide resolved
@@ -23,6 +23,7 @@ namespace sdk {

using time_point = std::chrono::time_point<long long, std::chrono::nanoseconds>;

// TODO: RSDK-6627 : move this function to `Resource` class.
Copy link
Member

Choose a reason for hiding this comment

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

👍

using viam::robot::v1::Discovery;
using viam::robot::v1::DiscoveryQuery;
using viam::robot::v1::FrameSystemConfig;
using viam::robot::v1::Operation;
using viam::robot::v1::RobotService;
Copy link
Member

Choose a reason for hiding this comment

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

Apologies, I meant to leave that comment on the client.hpp header, not the client.cpp file. I will re-add the comment in the right place.

src/viam/sdk/robot/client.hpp Outdated Show resolved Hide resolved
const std::lock_guard<std::mutex> lock(lock_);
std::vector<Name>* resources = std::move(&resource_names_);
std::vector<Name> resources = std::move(resource_names_);
Copy link
Member

Choose a reason for hiding this comment

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

I think this is actually going to destroy the contents of resource_names_ since they will be moved into resources and then returned. I think you can get rid of this line entirely and now write return resource_names_ which will implicitly copy. It actually wouldn't have been possible to make this error if RobotClient::resource_names was declared const, which I think it probably should be. Can you please make it const? Note that you will probably need to declare lock_ as mutable to do so. That's fine, it is very common for private mutex members to be mutable: https://stackoverflow.com/questions/14131572/always-declare-stdmutex-as-mutable-in-c11

Copy link
Member Author

@purplenicole730 purplenicole730 Feb 12, 2024

Choose a reason for hiding this comment

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

std::vector<Name> RobotClient::resource_names() const {
    mutable std::lock_guard<std::mutex> lock(lock_);
    return resource_names_;
}

This code gave the error below:

error: 'mutable' can only be applied to member variables
    mutable std::lock_guard<std::mutex> lock(lock_);
                                        ^

Copy link
Member

Choose a reason for hiding this comment

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

The mutable should go on the declaration of RobotClient::lock_.

Name r = Name(api, "", name);

auto resource = this->resource_by_name(std::move(r));
auto resource = this->resource_by_name({API::get<T>(), "", name});
Copy link
Member

Choose a reason for hiding this comment

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

I don't think you need the this-> here. And the function is now simple enough it could be a one liner:

std::shared_ptr<T> resource_by_name(std::string name) {
    return std::dynamic_pointer_cast<T>(resource_by_name({API::get<T>(), "", std::move(name)}));
}

Note that name should be moved from.

std::vector<Transform> additional_transforms = std::vector<Transform>());
std::vector<frame_system_config> get_frame_system_config(
std::vector<WorldState::transform> additional_transforms =
std::vector<WorldState::transform>());
Copy link
Member

Choose a reason for hiding this comment

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

You can make this simpler: std::vector<WorldState::transform> additional_transforms = {}

pose_in_frame transform_pose(pose_in_frame query,
std::string destination,
std::vector<WorldState::transform> additional_transforms =
std::vector<WorldState::transform>());
Copy link
Member

Choose a reason for hiding this comment

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

Ditto as = {} for the defaulted parameter.

DiscoveryQuery RobotClient::discovery_query::to_proto() const {
DiscoveryQuery proto;
*proto.mutable_subtype() = subtype;
*proto.mutable_model() = std::move(model);
Copy link
Member

Choose a reason for hiding this comment

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

This std::move probably doesn't do anything: the method is const so model is const, so can't be moved from.

}

bool operator==(const RobotClient::discovery& lhs, const RobotClient::discovery& rhs) {
return lhs.query == rhs.query && map_to_struct(lhs.results).SerializeAsString() ==
Copy link
Member

Choose a reason for hiding this comment

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

Why do results get turned back into proto Struct's in order to compare them with SerializeAsString? I'd expect (*lhs.results == *rhs.results) to work, since then you are comparing unordered maps.

Ohhh.... I think I see. It won't work because it will end up comparing the embedded shared_ptr<ProtoType> rather than the pointed-to ProtoType. That's unfortunate.

I think we should prioritize fixing things up around AttributeMap / ProtoType. I think if we do that we will end up back here because I'd expect to see map_to_struct and struct_to_map change.

OK to go ahead with what you have here unless @stuqdog disagrees.

Copy link
Member

Choose a reason for hiding this comment

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

No disagreement here. Agreed also that AttributeMap could use another look. Do you recall offhand if we made a ticket for that already?

Copy link
Member

Choose a reason for hiding this comment

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

No, I don't think we have. Could you make one?

Copy link
Member

Choose a reason for hiding this comment

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

@@ -52,6 +52,7 @@ class ProtoType {

explicit ProtoType(bool b) : proto_type_(std::move(b)) {}
explicit ProtoType(std::string s) : proto_type_(std::move(s)) {}
explicit ProtoType(const char* c) : proto_type_(std::string(c)) {}
Copy link
Member

Choose a reason for hiding this comment

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

@stuqdog - Our godbolt session exploring this is here: https://godbolt.org/z/KP7EfTaox

Copy link
Member

@stuqdog stuqdog left a comment

Choose a reason for hiding this comment

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

Nice! This looks great. There are a couple minor comments/requested changes I left but I don't think they're significant enough to require another round of review after being fixed. Thanks so much for putting this together!

One note, however: tests will now fail in this branch because we've made clang-tidy a bit stricter recently. So you'll need to make a couple changes around const correctness to address those. To avoid having to go through the CI cycle over and over, I suggest invoking cmake with the DVIAMCPPSDK_CLANG_TIDY=ON flag and building so you can see all places that may require fixes.

Comment on lines 51 to 52
std::cout << "\tname: " << resource.name() << " (type:" << resource.api().resource_type()
<< " subtype:" << resource.api().resource_subtype() << ")" << std::endl;
Copy link
Member

Choose a reason for hiding this comment

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

(minor) we can change this to also be std::cout << "\t" << resource << "\n";

Comment on lines 62 to 64
for (const vs::Name& resource : resource_names) {
cout << " - " << resource.name() << " (" << resource.api().resource_subtype() << ")"
<< endl;
Copy link
Member

Choose a reason for hiding this comment

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

(minor) here too we can simply << resource directly.

Comment on lines 4 to 12
#include <cctype>
#include <unordered_map>

#include <boost/test/included/unit_test.hpp>

#include <viam/sdk/common/proto_type.hpp>
#include <viam/sdk/config/resource.hpp>
#include <viam/sdk/robot/client.hpp>
#include <viam/sdk/tests/mocks/mock_robot.hpp>
Copy link
Member

Choose a reason for hiding this comment

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

We can probably get rid of these extra includes here.

template <typename T>
// Sorts our vecs and converts to strings for consistent comparisons
std::vector<std::string> vec_to_string_util(std::vector<T>& vec) {
std::vector<std::string> name_vec_to_string(std::vector<Name>& vec) {
Copy link
Member

Choose a reason for hiding this comment

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

I still believe we can get rid of this entirely.

Comment on lines 122 to 125
std::vector<Name> resource_names = client->resource_names();
auto names = name_vec_to_string(resource_names);
auto mocks = mock_resource_names_response();

auto mock_resp = vec_to_string_util(mocks);
auto mock_resp = name_vec_to_string(mocks);
Copy link
Member

Choose a reason for hiding this comment

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

following up on the above comment, I deleted name_vec_to_string and changed this test to compare resource_names and mocks directly and had no issues with compilation or tests.

Comment on lines -149 to -151
// unfortunately the sorting is a bit odd so we end up with a mismatch of index,
// but this ensures that the statuses we received do exist in the mocks, as
// expected.
Copy link
Member

Choose a reason for hiding this comment

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

👍


BOOST_TEST(some_status_strs == some_mock_strs, boost::test_tools::per_element());
BOOST_AUTO_TEST_CASE(test_frame_system_config) {
Copy link
Member

Choose a reason for hiding this comment

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

I agree that this test is (sadly) necessary if we want the to_proto methods to be completely hidden but still want verification that the proto and native types have identical values, but can we add a quick comment above just explaining this point?

Comment on lines 128 to 133
std::vector<RobotClient::status> resp;
resp.push_back(camera_status);
resp.push_back(motor_status);
resp.push_back(generic_status);

return resp;
Copy link
Member

Choose a reason for hiding this comment

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

(minor) this can all be reduced to return {std::move(camera_status), std::move(motor_status), std::move(generic_status)};

We should definitely be moving these values, it's a good habit to get into.

viam::robot::v1::DiscoveryQuery query;
*query.mutable_subtype() = "camera";
*query.mutable_model() = "webcam";
std::vector<RobotClient::discovery> mock_discovery_response() {
Copy link
Member

Choose a reason for hiding this comment

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

I was a bit hasty with my edits to this method, do you mind std::moveing values when they're last used (query at line 88, discovery at line 90, etc.).


google::protobuf::Map<std::string, google::protobuf::Value>* map = results.mutable_fields();
map->insert(pair);
v.push_back(discovery);
Copy link
Member

Choose a reason for hiding this comment

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

Another "me being too hasty" thing, this discovery can be moved :)

Copy link
Member

@acmorrow acmorrow left a comment

Choose a reason for hiding this comment

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

LGTM with one optional suggestion / thing to try. I only took a quick look at the mock / test stuff because I think Ethan has a good handle on what is going on there.

This was quite a review but I think the final result looks great, with a much cleaner RobotClient API!

}

struct RobotClient::impl {
impl(std::unique_ptr<RobotService::Stub> stub) : stub_(std::move(stub)) {}
Copy link
Member

Choose a reason for hiding this comment

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

I still think you can write this as impl(RobotService::Stub&& stub) : stub_(std::move(stub) and get rid of the unique_ptr, but, at the same time, what you have here isn't wrong, it just has an extra level of indirection.

Copy link
Member

Choose a reason for hiding this comment

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

fwiw I get errors when I try to make this change since the RobotService::NewStub method returns a unique_ptr and not a raw Stub type. I can get rid of the unique_ptr by dereferencing it in the constructor but I'm not sure that's better.

Copy link
Member Author

@purplenicole730 purplenicole730 Feb 15, 2024

Choose a reason for hiding this comment

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

I tried this and it gave me errors even though I changed everything. It's a little hard to find the two conversations we had on this since I was asked not to resolve the comments, but I had tried twice before.
I'm guessing it's because of what Ethan just said.

@purplenicole730 purplenicole730 merged commit 1dac2e9 into viamrobotics:main Feb 15, 2024
3 checks passed
@purplenicole730 purplenicole730 deleted the RSDK-4573-remove-proto-types-from-robot-client-apis branch February 15, 2024 15:36
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants