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

[systems] Add DiagramBuilder overloads for shared_ptr #22347

Merged
merged 1 commit into from
Jan 2, 2025

Conversation

jwnimmer-tri
Copy link
Collaborator

@jwnimmer-tri jwnimmer-tri commented Dec 23, 2024

Follow-up from #14387, and towards #5842 and #21968. We must stop passing unique_ptr as an argument from Python down to C++... passing unique_ptr in an argument of a call from Python into C++ is made possible by Drake-specific hacks to pybind11, and we need to get rid of the hacks.


This change is Reviewable

@jwnimmer-tri jwnimmer-tri added priority: low release notes: feature This pull request contains a new feature labels Dec 23, 2024
@jwnimmer-tri jwnimmer-tri force-pushed the framework-shared branch 3 times, most recently from f257f5a to 75c535c Compare December 26, 2024 15:54
Copy link
Collaborator Author

@jwnimmer-tri jwnimmer-tri left a comment

Choose a reason for hiding this comment

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

+@rpoyner-tri and +@sherm1 for all reviews, please. (Rico with feature hat on pydrake changes, Sherm with feature hat on non-pydrake changes.)

Reviewed 15 of 15 files at r1, 1 of 1 files at r2, all commit messages.
Reviewable status: LGTM missing from assignees rpoyner-tri(platform),sherm1(platform)

Copy link
Member

@sherm1 sherm1 left a comment

Choose a reason for hiding this comment

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

Feature :lgtm: for the non-pybind stuff

Reviewed 10 of 15 files at r1, 1 of 1 files at r2, all commit messages.
Reviewable status: 1 unresolved discussion, LGTM missing from assignee rpoyner-tri(platform)


systems/framework/test/diagram_builder_test.cc line 135 at r2 (raw file):

}

// Integration test of the Digram <=> System <=> SystemBase interaction when the

typo: Digram -> Diagram

jwnimmer-tri

This comment was marked as resolved.

Copy link
Contributor

@rpoyner-tri rpoyner-tri left a comment

Choose a reason for hiding this comment

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

Feature pybind :lgtm: with some documentation whingeing.

Reviewed 10 of 15 files at r1, 1 of 1 files at r2, 2 of 3 files at r3, 2 of 2 files at r4, all commit messages.
Reviewable status: 6 unresolved discussions


-- commits line 14 at r4:
nit I don't understand this talk of customizing cleanup -- does this represent some (squashed out) intermediate changes that culminated in the aliasing shared_ptr ctor?


-- commits line 15 at r4:
typo

Suggestion:

unique_ptr 

bindings/pydrake/systems/controllers_py.cc line 192 at r4 (raw file):

                          int plant_input_port_index) {
          return std::make_unique<Class>(
              // The ref_cycle is responsible for object lifetime, so we'll give

minor Having stared at (the many copies of) this comment for a while, I will say I find it misleading. While the ref_cycle plays a role in the overall memory management, the reason we can/must use the aliasing constructor here is a bit more local. We accept the plant (in this case) via alias (reference), so we must not pretend to grant (even shared) ownership to the c++ machinery. All we have is an alias, so that's all that we can pass on.

In the "good old days", we took ownership from the pybind wrapper of plant by custom-fork unique_ptr argument code, so we could pass it to c++.

The fact that the ownership chain ultimately terminates in the ref_cycle is not unimportant, but seems like a side issue.


bindings/pydrake/systems/controllers_py.cc line 192 at r4 (raw file):

                          int plant_input_port_index) {
          return std::make_unique<Class>(
              // The ref_cycle is responsible for object lifetime, so we'll give

nit There are 7 copies (I think) of this comment and its accompanying documentation. Is it worth factoring out, just so we can anchor the documentation in one place?


systems/framework/test/diagram_builder_test.cc line 135 at r4 (raw file):

}

// Integration test of the Diagram <=> System <=> SystemBase interaction when

nit maybe <==> instead of <=> to avoid confusion with spaceship?


systems/framework/diagram_builder.h line 106 at r4 (raw file):

  /// shared_ptr overload.}
  template<class S>
  S* AddSystem(std::unique_ptr<S> system) {

nit Why did we retain a unique_ptr overload here but not elsewhere?

Clearly promoting unique to shared on an argument is not a breaking change (go go implicit conversion ctors).

Copy link
Collaborator Author

@jwnimmer-tri jwnimmer-tri left a comment

Choose a reason for hiding this comment

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

Reviewed 2 of 2 files at r4, 1 of 1 files at r5, all commit messages.
Reviewable status: 3 unresolved discussions


-- commits line 14 at r4:

... intermediate changes ...

Right! In earlier drafts I had deleters that captured the py::object until I realized that existing annotations (ref_cycle or keep_alive) were better.


systems/framework/diagram_builder.h line 106 at r4 (raw file):

Previously, rpoyner-tri (Rick Poyner (rico)) wrote…

nit Why did we retain a unique_ptr overload here but not elsewhere?

Clearly promoting unique to shared on an argument is not a breaking change (go go implicit conversion ctors).

I was worried about overload resolution.

For all of the other edited functions with overloads, selecting which overload to use is fully determined by the other arguments (either their types like double vs Eigen, or their arity).

In this case, we have the crappy Args... overloads also in play, which I was worried would lead to ambiguity (build errors) under certain conditions.

I'll play around a bit more and see if I can convince myself it's 100% backcompat or not.


bindings/pydrake/systems/controllers_py.cc line 192 at r4 (raw file):

All we have is an alias, so that's all that we can pass on.

Not quite true. Inside the lambda we could recover a py::object handle to the plant which would then convey (shared) ownership to us, and then we could capture that owned py::object as the managed object of the shared_ptr<> that we pass down to C++, thus completing the ownership chain.

That would be correctly, but an uncollectable cycle. Because we are is-a Diagram, we must use the ref_cycle to align with how Diagram and DiagramBuilder treat lifetimes. Once we have that in place, then it's safe to not give any more ownership hints down to C++.

So the reasoning is very Diagram-specific.

I'll see if I can find a better phrasing, but suggestions are welcome, too.

Copy link
Collaborator Author

@jwnimmer-tri jwnimmer-tri left a comment

Choose a reason for hiding this comment

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

Reviewable status: 3 unresolved discussions


systems/framework/diagram_builder.h line 106 at r4 (raw file):

Previously, jwnimmer-tri (Jeremy Nimmer) wrote…

I was worried about overload resolution.

For all of the other edited functions with overloads, selecting which overload to use is fully determined by the other arguments (either their types like double vs Eigen, or their arity).

In this case, we have the crappy Args... overloads also in play, which I was worried would lead to ambiguity (build errors) under certain conditions.

I'll play around a bit more and see if I can convince myself it's 100% backcompat or not.

Yup. If we remove the unique_ptr overloads, there are tons of compile errors right out of the gate. There might be a way to use concepts or something to fix the overload ambiguity, but for now I'm okay with leave it alone here.

AddSystem and AddNamedSystem functions are overloaded to allow passing
the system via mutable shared_ptr (in addition to mutable unique_ptr).

Architecturally, the diagram still maintains exclusive ownership of
its children: each child still has a back-pointer to the Diagram (via
the SystemParentServiceInterface) so it's not possible to actually
"share" a subsystem across multiple diagrams. However, unique_ptr is
overly restrictive: it not only denotes exclusive ownership (which is
fine), but also demands that resource cleanup is `delete MyClass`
(which is insufficient for our Python bindings). We must use a smart
pointer type that allows customizing cleanup. We could imagine using
unique_ptr whose Deleter was a std::function, but it's simpler to just
use shared_ptr.

Relatedly, we also adjust the Diagram subclasses PidControlledSystem
and RgbdSensorDiscrete to accept the more general shared_ptr (instead
of unique_ptr) for their child systems and to use ref_cycle for their
pydrake bindings.
Copy link
Collaborator Author

@jwnimmer-tri jwnimmer-tri left a comment

Choose a reason for hiding this comment

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

Reviewed 5 of 5 files at r6, all commit messages.
Reviewable status: 3 unresolved discussions


bindings/pydrake/systems/controllers_py.cc line 192 at r4 (raw file):

Previously, jwnimmer-tri (Jeremy Nimmer) wrote…

All we have is an alias, so that's all that we can pass on.

Not quite true. Inside the lambda we could recover a py::object handle to the plant which would then convey (shared) ownership to us, and then we could capture that owned py::object as the managed object of the shared_ptr<> that we pass down to C++, thus completing the ownership chain.

That would be correctly, but an uncollectable cycle. Because we are is-a Diagram, we must use the ref_cycle to align with how Diagram and DiagramBuilder treat lifetimes. Once we have that in place, then it's safe to not give any more ownership hints down to C++.

So the reasoning is very Diagram-specific.

I'll see if I can find a better phrasing, but suggestions are welcome, too.

Better now?


bindings/pydrake/systems/controllers_py.cc line 192 at r4 (raw file):

Previously, rpoyner-tri (Rick Poyner (rico)) wrote…

nit There are 7 copies (I think) of this comment and its accompanying documentation. Is it worth factoring out, just so we can anchor the documentation in one place?

Better now?

Copy link
Contributor

@rpoyner-tri rpoyner-tri left a comment

Choose a reason for hiding this comment

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

Reviewed 1 of 1 files at r5, 5 of 5 files at r6, all commit messages.
Reviewable status: :shipit: complete! all discussions resolved, LGTM from assignees rpoyner-tri(platform),sherm1(platform)


systems/framework/diagram_builder.h line 106 at r4 (raw file):

Previously, jwnimmer-tri (Jeremy Nimmer) wrote…

Yup. If we remove the unique_ptr overloads, there are tons of compile errors right out of the gate. There might be a way to use concepts or something to fix the overload ambiguity, but for now I'm okay with leave it alone here.

Ah, I missed the whole overloads with templates fiesta here. Yup, good enough.

@jwnimmer-tri jwnimmer-tri merged commit e10ce53 into RobotLocomotion:master Jan 2, 2025
8 of 9 checks passed
@jwnimmer-tri jwnimmer-tri deleted the framework-shared branch January 2, 2025 21:35
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
priority: low release notes: feature This pull request contains a new feature
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants