diff --git a/rosidl_generator_cpp/resource/idl__struct.hpp.em b/rosidl_generator_cpp/resource/idl__struct.hpp.em index b8f985617..58e42a3ed 100644 --- a/rosidl_generator_cpp/resource/idl__struct.hpp.em +++ b/rosidl_generator_cpp/resource/idl__struct.hpp.em @@ -30,6 +30,7 @@ include_directives = set() #include #include #include +#include #include #include diff --git a/rosidl_generator_cpp/resource/msg__struct.hpp.em b/rosidl_generator_cpp/resource/msg__struct.hpp.em index 867b8c624..6ed5d0eab 100644 --- a/rosidl_generator_cpp/resource/msg__struct.hpp.em +++ b/rosidl_generator_cpp/resource/msg__struct.hpp.em @@ -4,6 +4,7 @@ from rosidl_generator_cpp import create_init_alloc_and_member_lists from rosidl_generator_cpp import escape_string from rosidl_generator_cpp import escape_wstring from rosidl_generator_cpp import msg_type_to_cpp +from rosidl_generator_cpp import member_to_cpp from rosidl_generator_cpp import MSG_TYPE_TO_CPP from rosidl_generator_cpp import generate_zero_string from rosidl_generator_cpp import generate_default_string @@ -207,7 +208,7 @@ non_defaulted_zero_initialized_members = [ // field types and members @[for member in message.structure.members]@ using _@(member.name)_type = - @(msg_type_to_cpp(member.type)); + @(member_to_cpp(member)); _@(member.name)_type @(member.name); @[end for]@ @@ -215,7 +216,7 @@ non_defaulted_zero_initialized_members = [ // setters for named parameter idiom @[ for member in message.structure.members]@ Type & set__@(member.name)( - const @(msg_type_to_cpp(member.type)) & _arg) + const @(member_to_cpp(member)) & _arg) { this->@(member.name) = _arg; return *this; diff --git a/rosidl_generator_cpp/resource/msg__traits.hpp.em b/rosidl_generator_cpp/resource/msg__traits.hpp.em index 34e431cd1..d6436b1d1 100644 --- a/rosidl_generator_cpp/resource/msg__traits.hpp.em +++ b/rosidl_generator_cpp/resource/msg__traits.hpp.em @@ -1,5 +1,6 @@ @# Included from rosidl_generator_cpp/resource/idl__traits.hpp.em @{ +from rosidl_generator_cpp import msg_type_to_cpp from rosidl_parser.definition import ACTION_FEEDBACK_SUFFIX from rosidl_parser.definition import ACTION_GOAL_SUFFIX from rosidl_parser.definition import ACTION_RESULT_SUFFIX @@ -13,6 +14,7 @@ from rosidl_parser.definition import EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME from rosidl_parser.definition import NamespacedType from rosidl_parser.definition import AbstractSequence from rosidl_parser.definition import UnboundedSequence +from rosidl_parser.definition import OPTIONAL_ANNOTATION message_namespace = '::'.join(message.structure.namespaced_type.namespaces) message_typename = '::'.join(message.structure.namespaced_type.namespaced_name()) @@ -90,27 +92,39 @@ inline void to_flow_style_yaml( @[ end if]@ // member: @(member.name) - { + do { +@[ if member.has_annotation(OPTIONAL_ANNOTATION)]@ + auto value = msg.@(member.name).value(); + if (msg.@(member.name) == std::nullopt) { + out << "@(member.name): null"; +@[ if i < len(message.structure.members) - 1]@ + out << ", "; +@[ end if]@ + break; + } +@[ else]@ + auto value = msg.@(member.name); +@[ end if]@ @[ if isinstance(member.type, BasicType)]@ out << "@(member.name): "; @[ if member.type.typename in ('octet', 'char', 'wchar')]@ - rosidl_generator_traits::character_value_to_yaml(msg.@(member.name), out); + rosidl_generator_traits::character_value_to_yaml(value, out); @[ else]@ - rosidl_generator_traits::value_to_yaml(msg.@(member.name), out); + rosidl_generator_traits::value_to_yaml(value, out); @[ end if]@ @[ elif isinstance(member.type, AbstractGenericString)]@ out << "@(member.name): "; - rosidl_generator_traits::value_to_yaml(msg.@(member.name), out); + rosidl_generator_traits::value_to_yaml(value, out); @[ elif isinstance(member.type, NamespacedType)]@ out << "@(member.name): "; - to_flow_style_yaml(msg.@(member.name), out); + to_flow_style_yaml(value, out); @[ elif isinstance(member.type, (AbstractSequence, Array))]@ - if (msg.@(member.name).size() == 0) { + if (value.size() == 0) { out << "@(member.name): []"; } else { out << "@(member.name): ["; - size_t pending_items = msg.@(member.name).size(); - for (auto item : msg.@(member.name)) { + size_t pending_items = value.size(); + for (auto item : value) { @[ if isinstance(member.type.value_type, BasicType)]@ @[ if member.type.value_type.typename in ('octet', 'char', 'wchar')]@ rosidl_generator_traits::character_value_to_yaml(item, out); @@ -132,7 +146,7 @@ inline void to_flow_style_yaml( @[ if i < len(message.structure.members) - 1]@ out << ", "; @[ end if]@ - } + } while (false); @[ end for]@ out << "}"; @[end if]@ @@ -152,31 +166,40 @@ inline void to_block_style_yaml( @[ end if]@ // member: @(member.name) - { + do { if (indentation > 0) { out << std::string(indentation, ' '); } +@[ if member.has_annotation(OPTIONAL_ANNOTATION)]@ + auto value = msg.@(member.name).value(); + if (msg.@(member.name) == std::nullopt) { + out << "@(member.name): null\n"; + break; + } +@[ else]@ + auto value = msg.@(member.name); +@[ end if]@ @[ if isinstance(member.type, BasicType)]@ out << "@(member.name): "; @[ if member.type.typename in ('octet', 'char', 'wchar')]@ - rosidl_generator_traits::character_value_to_yaml(msg.@(member.name), out); + rosidl_generator_traits::character_value_to_yaml(value, out); @[ else]@ - rosidl_generator_traits::value_to_yaml(msg.@(member.name), out); + rosidl_generator_traits::value_to_yaml(value, out); @[ end if]@ out << "\n"; @[ elif isinstance(member.type, AbstractGenericString)]@ out << "@(member.name): "; - rosidl_generator_traits::value_to_yaml(msg.@(member.name), out); + rosidl_generator_traits::value_to_yaml(value, out); out << "\n"; @[ elif isinstance(member.type, NamespacedType)]@ out << "@(member.name):\n"; - to_block_style_yaml(msg.@(member.name), out, indentation + 2); + to_block_style_yaml(value, out, indentation + 2); @[ elif isinstance(member.type, (AbstractSequence, Array))]@ - if (msg.@(member.name).size() == 0) { + if (value.size() == 0) { out << "@(member.name): []\n"; } else { out << "@(member.name):\n"; - for (auto item : msg.@(member.name)) { + for (auto item : value) { if (indentation > 0) { out << std::string(indentation, ' '); } @@ -199,7 +222,7 @@ inline void to_block_style_yaml( } } @[ end if]@ - } + } while(false); @[ end for]@ @[end if]@ } // NOLINT(readability/fn_size) diff --git a/rosidl_generator_cpp/rosidl_generator_cpp/__init__.py b/rosidl_generator_cpp/rosidl_generator_cpp/__init__.py index a87092274..36a86fa24 100644 --- a/rosidl_generator_cpp/rosidl_generator_cpp/__init__.py +++ b/rosidl_generator_cpp/rosidl_generator_cpp/__init__.py @@ -14,17 +14,23 @@ from ast import literal_eval from typing import List +from typing import Optional +from typing import Union from rosidl_parser.definition import AbstractGenericString from rosidl_parser.definition import AbstractNestedType from rosidl_parser.definition import AbstractSequence from rosidl_parser.definition import AbstractString +from rosidl_parser.definition import AbstractType from rosidl_parser.definition import AbstractWString from rosidl_parser.definition import Array from rosidl_parser.definition import BasicType from rosidl_parser.definition import BoundedSequence from rosidl_parser.definition import FLOATING_POINT_TYPES +from rosidl_parser.definition import Member +from rosidl_parser.definition import Message from rosidl_parser.definition import NamespacedType +from rosidl_parser.definition import OPTIONAL_ANNOTATION from rosidl_parser.definition import UnboundedSequence from rosidl_pycommon import generate_files @@ -76,7 +82,7 @@ def prefix_with_bom_if_necessary(content: str) -> str: } -def msg_type_only_to_cpp(type_): +def msg_type_only_to_cpp(type_: AbstractType) -> str: """ Convert a message type into the C++ declaration, ignoring array types. @@ -103,7 +109,7 @@ def msg_type_only_to_cpp(type_): return cpp_type -def msg_type_to_cpp(type_): +def msg_type_to_cpp(type_: AbstractType) -> str: """ Convert a message type into the C++ declaration, along with the array type. @@ -134,7 +140,16 @@ def msg_type_to_cpp(type_): return cpp_type -def value_to_cpp(type_, value): +def member_to_cpp(member: Member) -> str: + """Convert a member into the C++ declaration. This exists to add optional support.""" + cpp_type = msg_type_to_cpp(member.type) + + if member.has_annotation(OPTIONAL_ANNOTATION): + return f'std::optional<{cpp_type}>' + return cpp_type + + +def value_to_cpp(type_: AbstractType, value) -> str: """ Convert a python value into a string representing that value in C++. @@ -149,7 +164,9 @@ def value_to_cpp(type_, value): """ assert not isinstance(type_, NamespacedType), \ "Could not convert non-primitive type '%s' to CPP" % (type_) - assert value is not None, "Value for type '%s' must not be None" % (type_) + + if value is None: + return 'std::nullopt' if not isinstance(type_, AbstractNestedType): return primitive_value_to_cpp(type_, value) @@ -171,7 +188,7 @@ def value_to_cpp(type_, value): return cpp_value -def primitive_value_to_cpp(type_, value): +def primitive_value_to_cpp(type_: Union[BasicType, AbstractGenericString], value) -> str: """ Convert a python value into a string representing that value in C++. @@ -246,30 +263,30 @@ def default_value_from_type(type_): return 0 -def escape_string(s): +def escape_string(s: str) -> str: s = s.replace('\\', '\\\\') s = s.replace('"', '\\"') return s -def escape_wstring(s): +def escape_wstring(s: str) -> str: return escape_string(s) -def create_init_alloc_and_member_lists(message): +def create_init_alloc_and_member_lists(message: Message): # A Member object represents the information we need to know to initialize # a single member of the class. class Member: - def __init__(self, name): + def __init__(self, name: str): self.name = name - self.default_value = None - self.zero_value = None + self.default_value: Optional[str] = None + self.zero_value: Optional[str] = None self.zero_need_array_override = False - self.type = None + self.type: Optional[AbstractType] = None self.num_prealloc = 0 - def same_default_and_zero_value(self, other): + def same_default_and_zero_value(self, other) -> bool: return self.default_value == other.default_value and \ self.zero_value == other.zero_value @@ -280,10 +297,10 @@ def same_default_and_zero_value(self, other): # value). class CommonMemberSet: - def __init__(self): - self.members = [] + def __init__(self) -> None: + self.members: list[Member] = [] - def add_member(self, member): + def add_member(self, member: Member) -> bool: if not self.members or self.members[-1].same_default_and_zero_value(member): self.members.append(member) return True @@ -296,12 +313,13 @@ def add_member(self, member): # initializion in the allocator constructor # member_list - The list of members that we will generate initialization code # for in the body of the constructors - init_list = [] - alloc_list = [] - member_list = [] + init_list: list[str] = [] + alloc_list: list[str] = [] + member_list: list[CommonMemberSet] = [] for field in message.structure.members: member = Member(field.name) member.type = field.type + if isinstance(field.type, Array): alloc_list.append(field.name + '(_alloc)') if isinstance(field.type.value_type, BasicType) or \ @@ -346,6 +364,10 @@ def add_member(self, member): commonset.add_member(member) member_list.append(commonset) + # Override zero_value if optional + if field.has_annotation(OPTIONAL_ANNOTATION): + member.zero_value = value_to_cpp(field.type, None) + return init_list, alloc_list, member_list diff --git a/rosidl_generator_tests/CMakeLists.txt b/rosidl_generator_tests/CMakeLists.txt index bd858f870..01f0d571d 100644 --- a/rosidl_generator_tests/CMakeLists.txt +++ b/rosidl_generator_tests/CMakeLists.txt @@ -35,6 +35,7 @@ if(BUILD_TESTING) ${test_interface_files_MSG_FILES} ${test_interface_files_SRV_FILES} msg/BasicIdl.idl + msg/Optional.msg msg/SmallConstant.msg ADD_LINTER_TESTS SKIP_INSTALL diff --git a/rosidl_generator_tests/msg/Optional.msg b/rosidl_generator_tests/msg/Optional.msg new file mode 100644 index 000000000..adec5452a --- /dev/null +++ b/rosidl_generator_tests/msg/Optional.msg @@ -0,0 +1,11 @@ +@optional +float32 optional_float + +@optional +float32 default_optional_float 32.0 + +# @optional +# string optional_string + +# @optional +# short optional_short_array[3] \ No newline at end of file diff --git a/rosidl_generator_tests/test/rosidl_generator_cpp/test_msg_builder.cpp b/rosidl_generator_tests/test/rosidl_generator_cpp/test_msg_builder.cpp index 229668c27..4dedc8a77 100644 --- a/rosidl_generator_tests/test/rosidl_generator_cpp/test_msg_builder.cpp +++ b/rosidl_generator_tests/test/rosidl_generator_cpp/test_msg_builder.cpp @@ -14,11 +14,14 @@ #include +#include + #include "rosidl_generator_tests/msg/detail/arrays__builder.hpp" #include "rosidl_generator_tests/msg/detail/basic_types__builder.hpp" #include "rosidl_generator_tests/msg/detail/empty__builder.hpp" #include "rosidl_generator_tests/msg/detail/multi_nested__builder.hpp" #include "rosidl_generator_tests/msg/detail/nested__builder.hpp" +#include "rosidl_generator_tests/msg/detail/optional_idl__builder.hpp" TEST(Test_msg_initialization, build) { ::rosidl_generator_tests::msg::BasicTypes basic = @@ -200,4 +203,16 @@ TEST(Test_msg_initialization, build) { rosidl_generator_tests::msg::Empty empty = rosidl_generator_tests::build(); ASSERT_EQ(0, empty.structure_needs_at_least_one_member); + + rosidl_generator_tests::msg::OptionalIdl optional = + rosidl_generator_tests::build() + .optional_float(0.1f) + .default_optional_float(2.0); + // .optional_short_array({{-100, -200, -100}}); + // .optional_string("init"); + + ASSERT_EQ(0.1f, optional.optional_float); + ASSERT_EQ(2.0, optional.default_optional_float); + // ASSERT_EQ({-100, -200, -100}, optional.default_optional_float); + // ASSERT_EQ("init", optional.optional_string) } diff --git a/rosidl_generator_tests/test/rosidl_generator_cpp/test_msg_initialization.cpp b/rosidl_generator_tests/test/rosidl_generator_cpp/test_msg_initialization.cpp index 668aed0b0..3d4121143 100644 --- a/rosidl_generator_tests/test/rosidl_generator_cpp/test_msg_initialization.cpp +++ b/rosidl_generator_tests/test/rosidl_generator_cpp/test_msg_initialization.cpp @@ -17,9 +17,11 @@ #include #include +#include #include "rosidl_generator_tests/msg/defaults.hpp" #include "rosidl_generator_tests/msg/bounded_sequences.hpp" +#include "rosidl_generator_tests/msg/optional_idl.hpp" template struct ScopeExit @@ -171,6 +173,14 @@ TEST(Test_msg_initialization, defaults_only_constructor) { ASSERT_EQ(0LL, basic.int64_value); } +TEST(Test_msg_initialization, optional_msg_initialization) { + rosidl_generator_tests::msg::OptionalIdl optional_msg; + ASSERT_EQ(std::nullopt, optional_msg.optional_float); + ASSERT_EQ(32.0, optional_msg.default_optional_float); + // ASSERT_EQ(std::nullopt, optional_msg.optional_short_array); + // ASSERT_EQ(std::nullopt, optional_msg.optional_string); +} + // This is a test to ensure that when the user passes SKIP to the constructor, // it does no initialization. TEST(Test_msg_initialization, skip_constructor) { diff --git a/rosidl_generator_tests/test/rosidl_generator_cpp/test_traits.cpp b/rosidl_generator_tests/test/rosidl_generator_cpp/test_traits.cpp index 7e16d450c..32202dab0 100644 --- a/rosidl_generator_tests/test/rosidl_generator_cpp/test_traits.cpp +++ b/rosidl_generator_tests/test/rosidl_generator_cpp/test_traits.cpp @@ -22,6 +22,7 @@ #include "rosidl_generator_tests/msg/empty.hpp" #include "rosidl_generator_tests/msg/bounded_sequences.hpp" #include "rosidl_generator_tests/msg/nested.hpp" +#include "rosidl_generator_tests/msg/optional_idl.hpp" #include "rosidl_generator_tests/msg/strings.hpp" #include "rosidl_generator_tests/msg/w_strings.hpp" #include "rosidl_generator_tests/srv/empty.hpp" @@ -256,6 +257,18 @@ alignment_check: 0 } } +TEST(Test_rosidl_generator_traits, optional_msgs) { + { + const rosidl_generator_tests::msg::OptionalIdl optional_msg; + EXPECT_STREQ( + "optional_float: null\ndefault_optional_float: 32.0000\n", + to_yaml(optional_msg).c_str()); + EXPECT_STREQ( + "{optional_float: null, default_optional_float: 32.0000}", + to_yaml(optional_msg, true).c_str()); + } +} + TEST(Test_rosidl_generator_traits, to_yaml_flow_style) { constexpr bool use_flow_style = true; { diff --git a/rosidl_parser/rosidl_parser/definition.py b/rosidl_parser/rosidl_parser/definition.py index 5873ba470..24036a94a 100644 --- a/rosidl_parser/rosidl_parser/definition.py +++ b/rosidl_parser/rosidl_parser/definition.py @@ -141,6 +141,8 @@ ACTION_RESULT_SERVICE_SUFFIX: Final = '_GetResult' ACTION_FEEDBACK_MESSAGE_SUFFIX: Final = '_FeedbackMessage' +OPTIONAL_ANNOTATION: Final = 'optional' + class AbstractType: """The abstract base class for all types."""