diff --git a/rosidl_adapter/rosidl_adapter/parser.py b/rosidl_adapter/rosidl_adapter/parser.py index 836a3b751..286819426 100644 --- a/rosidl_adapter/rosidl_adapter/parser.py +++ b/rosidl_adapter/rosidl_adapter/parser.py @@ -12,10 +12,12 @@ # See the License for the specific language governing permissions and # limitations under the License. +import fnmatch import os import re import sys import textwrap +import yaml PACKAGE_NAME_MESSAGE_TYPE_SEPARATOR = '/' COMMENT_DELIMITER = '#' @@ -470,6 +472,21 @@ def parse_message_string(pkg_name, msg_name, message_string): # replace tabs with spaces message_string = message_string.replace('\t', ' ') + # Try to load type bounds config file from the environment + fixed_size_config = None + type_fixed_size_file = None + try: + type_fixed_size_file = os.environ['ROS2_TYPE_FIXED_SIZE_FILE'] + except KeyError: + pass + + if type_fixed_size_file: + try: + with open(type_fixed_size_file, 'r', encoding='utf-8') as config_file: + fixed_size_config = yaml.safe_load(config_file) + except IOError as exc: + print(f'Cannot open {type_fixed_size_file}. {exc}') + current_comments = [] message_comments, lines = extract_file_level_comments(message_string) for line in lines: @@ -542,6 +559,15 @@ def parse_message_string(pkg_name, msg_name, message_string): comment_lines += current_comments current_comments = [] + # Add fixed_size annotations if configured in file + for based_type in ['string']: + __add_fixed_size_annotations( + pkg_name=pkg_name, + msg_name=msg_name, + last_element=last_element, + field_base_type=based_type, + fixed_size_config=fixed_size_config) + msg = MessageSpecification(pkg_name, msg_name, fields, constants) msg.annotations['comment'] = message_comments @@ -555,6 +581,58 @@ def parse_message_string(pkg_name, msg_name, message_string): return msg +def __add_fixed_size_annotations(pkg_name, msg_name, last_element, field_base_type, fixed_size_config): + elem_based_type = str(last_element.type).split('[')[0] + if fixed_size_config is None or field_base_type != elem_based_type: + return + + fixed_size = None + skip_field = False + field_full_name = f'{pkg_name}/{msg_name}/{last_element.name}' + + # Check if config for strings + if field_base_type not in fixed_size_config: + skip_field = True + + # Check if field is blocklisted + elif 'skip' in fixed_size_config[field_base_type]: + for elem in fixed_size_config[field_base_type]['skip']: + if fnmatch.fnmatch(field_full_name, elem): + skip_field = True + break + + # Check for specific config for field + if not skip_field: + + # Check if config for package + if pkg_name not in fixed_size_config[field_base_type]: + pass + + # Check if config for type + elif msg_name not in fixed_size_config[field_base_type][pkg_name]: + pass + + # Check if config for field + elif str(last_element.name) in fixed_size_config[field_base_type][pkg_name][msg_name]: + fixed_size = fixed_size_config[field_base_type][pkg_name][msg_name][str(last_element.name)] + + # Load default fixed_size if present + if fixed_size is None and 'default_fixed_size' in fixed_size_config[field_base_type]: + fixed_size = fixed_size_config[field_base_type]['default_fixed_size'] + + if fixed_size is not None: + if fixed_size % 4 != 0: + print( + f'ERROR: Configured fixed size for {field_full_name} is {fixed_size}: ' + + 'NOT a multiple of 4.' + ) + sys.exit(1) + last_element.annotations.setdefault( + 'cdr_plain', + f'@cdr_plain(fixed_size={fixed_size})' + ) + + def process_comments(instance): if 'comment' in instance.annotations: lines = instance.annotations['comment'] diff --git a/rosidl_adapter/rosidl_adapter/resource/struct.idl.em b/rosidl_adapter/rosidl_adapter/resource/struct.idl.em index 04d89f471..302333214 100644 --- a/rosidl_adapter/rosidl_adapter/resource/struct.idl.em +++ b/rosidl_adapter/rosidl_adapter/resource/struct.idl.em @@ -80,6 +80,10 @@ else: @[ end if]@ @[ end for]@ ) +@[ end if]@ +@[ if field.annotations.get('cdr_plain')]@ + @(field.annotations['cdr_plain'])@ + @[ end if]@ @[ if field.default_value is not None]@ @@default (value=@(to_idl_literal(get_idl_type(field.type), field.default_value))) diff --git a/rosidl_generator_cpp/resource/idl__struct.hpp.em b/rosidl_generator_cpp/resource/idl__struct.hpp.em index c4b7c289e..753b2bfcc 100644 --- a/rosidl_generator_cpp/resource/idl__struct.hpp.em +++ b/rosidl_generator_cpp/resource/idl__struct.hpp.em @@ -33,6 +33,7 @@ include_directives = set() #include #include "rosidl_runtime_cpp/bounded_vector.hpp" +#include "rosidl_runtime_cpp/cdr_compatible_fixed_capacity_string.hpp" #include "rosidl_runtime_cpp/message_initialization.hpp" @####################################################################### diff --git a/rosidl_generator_cpp/resource/msg__struct.hpp.em b/rosidl_generator_cpp/resource/msg__struct.hpp.em index 6d1c1363b..a7720a549 100644 --- a/rosidl_generator_cpp/resource/msg__struct.hpp.em +++ b/rosidl_generator_cpp/resource/msg__struct.hpp.em @@ -123,7 +123,7 @@ def generate_default_string(membset): # For more info, see https://github.com/ros2/rosidl/issues/309 # TODO(jacobperron): Investigate reason for build warnings on Windows # TODO(jacobperron): Write test case for this path of execution - strlist.append('std::fill(this->%s.begin(), this->%s.end(), %s);' % (msg_type_to_cpp(member.type), msg_type_only_to_cpp(member.type), member.name, member.name, member.default_value[0])) + strlist.append('std::fill(this->%s.begin(), this->%s.end(), %s);' % (msg_type_to_cpp(member), msg_type_only_to_cpp(member), member.name, member.name, member.default_value[0])) else: for index, val in enumerate(member.default_value): strlist.append('this->%s[%d] = %s;' % (member.name, index, val)) @@ -141,12 +141,12 @@ def generate_zero_string(membset, fill_args): if member.num_prealloc > 0: strlist.append('this->%s.resize(%d);' % (member.name, member.num_prealloc)) if member.zero_need_array_override: - strlist.append('this->%s.fill(%s{%s});' % (member.name, msg_type_only_to_cpp(member.type), fill_args)) + strlist.append('this->%s.fill(%s{%s});' % (member.name, msg_type_only_to_cpp(member.real_member), fill_args)) else: # Specifying type for std::fill because of MSVC 14.12 warning about casting 'const int' to smaller types (C4244) # For more info, see https://github.com/ros2/rosidl/issues/309 # TODO(jacobperron): Investigate reason for build warnings on Windows - strlist.append('std::fill(this->%s.begin(), this->%s.end(), %s);' % (msg_type_to_cpp(member.type), msg_type_only_to_cpp(member.type), member.name, member.name, member.zero_value[0])) + strlist.append('std::fill(this->%s.begin(), this->%s.end(), %s);' % (msg_type_to_cpp(member.real_member), msg_type_only_to_cpp(member.real_member), member.name, member.name, member.zero_value[0])) else: strlist.append('this->%s = %s;' % (member.name, member.zero_value)) return strlist @@ -247,7 +247,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)); + @(msg_type_to_cpp(member)); _@(member.name)_type @(member.name); @[end for]@ @@ -255,7 +255,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 @(msg_type_to_cpp(member)) & _arg) { this->@(member.name) = _arg; return *this; diff --git a/rosidl_generator_cpp/rosidl_generator_cpp/__init__.py b/rosidl_generator_cpp/rosidl_generator_cpp/__init__.py index b076fd3ed..560d77956 100644 --- a/rosidl_generator_cpp/rosidl_generator_cpp/__init__.py +++ b/rosidl_generator_cpp/rosidl_generator_cpp/__init__.py @@ -19,6 +19,7 @@ from rosidl_parser.definition import AbstractSequence from rosidl_parser.definition import AbstractString from rosidl_parser.definition import AbstractWString +from rosidl_parser.definition import Annotatable from rosidl_parser.definition import Array from rosidl_parser.definition import BasicType from rosidl_parser.definition import BoundedSequence @@ -70,12 +71,20 @@ def prefix_with_bom_if_necessary(content): 'int64': 'int64_t', 'string': 'std::basic_string, ' + 'typename std::allocator_traits::template rebind_alloc>', + 'fixed_size_string': 'rosidl_runtime_cpp::CDRCompatibleFixedCapacityString<%u>', 'wstring': 'std::basic_string, typename ' + 'std::allocator_traits::template rebind_alloc>', } -def msg_type_only_to_cpp(type_): +def get_member_fixed_size_annotation(member): + try: + return member.get_annotation_values('cdr_plain')[0]["fixed_size"] + except: + return 0 + + +def msg_type_only_to_cpp(member): """ Convert a message type into the C++ declaration, ignoring array types. @@ -85,12 +94,17 @@ def msg_type_only_to_cpp(type_): @param type_: The message type @type type_: rosidl_parser.Type """ + type_ = member.type if isinstance(type_, AbstractNestedType): type_ = type_.value_type if isinstance(type_, BasicType): cpp_type = MSG_TYPE_TO_CPP[type_.typename] elif isinstance(type_, AbstractString): - cpp_type = MSG_TYPE_TO_CPP['string'] + fixed_size = get_member_fixed_size_annotation(member) + if fixed_size < 1: + cpp_type = MSG_TYPE_TO_CPP['string'] + else: + cpp_type = MSG_TYPE_TO_CPP['fixed_size_string'] % fixed_size elif isinstance(type_, AbstractWString): cpp_type = MSG_TYPE_TO_CPP['wstring'] elif isinstance(type_, NamespacedType): @@ -102,7 +116,7 @@ def msg_type_only_to_cpp(type_): return cpp_type -def msg_type_to_cpp(type_): +def msg_type_to_cpp(member_): """ Convert a message type into the C++ declaration, along with the array type. @@ -113,22 +127,22 @@ def msg_type_to_cpp(type_): @param type_: The message type @type type_: rosidl_parser.Type """ - cpp_type = msg_type_only_to_cpp(type_) + cpp_type = msg_type_only_to_cpp(member_) - if isinstance(type_, AbstractNestedType): - if isinstance(type_, UnboundedSequence): + if isinstance(member_.type, AbstractNestedType): + if isinstance(member_.type, UnboundedSequence): return \ ('std::vector<%s, typename std::allocator_traits::template ' + 'rebind_alloc<%s>>') % (cpp_type, cpp_type) - elif isinstance(type_, BoundedSequence): + elif isinstance(member_.type, BoundedSequence): return \ ('rosidl_runtime_cpp::BoundedVector<%s, %u, typename std::allocator_traits' + '::template rebind_alloc<%s>>') % (cpp_type, - type_.maximum_size, + member_.type.maximum_size, cpp_type) else: - assert isinstance(type_, Array) - return 'std::array<%s, %u>' % (cpp_type, type_.size) + assert isinstance(member_.type, Array) + return 'std::array<%s, %u>' % (cpp_type, member_.type.size) else: return cpp_type @@ -256,13 +270,14 @@ def create_init_alloc_and_member_lists(message): # a single member of the class. class Member: - def __init__(self, name): - self.name = name + def __init__(self, real_member): + self.name = real_member.name self.default_value = None self.zero_value = None self.zero_need_array_override = False self.type = None self.num_prealloc = 0 + self.real_member = real_member def same_default_and_zero_value(self, other): return self.default_value == other.default_value and \ @@ -295,7 +310,7 @@ def add_member(self, member): alloc_list = [] member_list = [] for field in message.structure.members: - member = Member(field.name) + member = Member(field) member.type = field.type if isinstance(field.type, Array): alloc_list.append(field.name + '(_alloc)') diff --git a/rosidl_parser/test/msg/MyMessage.idl b/rosidl_parser/test/msg/MyMessage.idl index f2d3cfa61..d90b4c6b1 100644 --- a/rosidl_parser/test/msg/MyMessage.idl +++ b/rosidl_parser/test/msg/MyMessage.idl @@ -41,6 +41,7 @@ module rosidl_parser { uint32 uint32_value; int64 int64_value; uint64 uint64_value; + @cdr_plain(fixed_size=64) string string_value; string<5> bounded_string_value; wstring wstring_value; diff --git a/rosidl_parser/test/test_parser.py b/rosidl_parser/test/test_parser.py index 90fc38696..7ca74e5c4 100644 --- a/rosidl_parser/test/test_parser.py +++ b/rosidl_parser/test/test_parser.py @@ -221,6 +221,13 @@ def test_message_parser_annotations(message_idl_file): assert 'max' in structure.members[3].annotations[1].value assert structure.members[3].annotations[1].value['max'] == 10 + assert len(structure.members[22].annotations) == 1 + + assert structure.members[22].annotations[0].name == 'cdr_plain' + assert len(structure.members[22].annotations[0].value) == 1 + assert 'fixed_size' in structure.members[22].annotations[0].value + assert structure.members[22].annotations[0].value['fixed_size'] == 64 + assert isinstance(structure.members[32].type, BasicType) assert structure.members[32].type.typename == 'float' assert structure.members[32].name == 'int_and_frac_with_positive_scientific' diff --git a/rosidl_runtime_cpp/include/rosidl_runtime_cpp/cdr_compatible_fixed_capacity_string.hpp b/rosidl_runtime_cpp/include/rosidl_runtime_cpp/cdr_compatible_fixed_capacity_string.hpp new file mode 100644 index 000000000..99acb5eef --- /dev/null +++ b/rosidl_runtime_cpp/include/rosidl_runtime_cpp/cdr_compatible_fixed_capacity_string.hpp @@ -0,0 +1,244 @@ +// Copyright 2023 Proyectos y Sistemas de Mantenimiento SL (eProsima). +// +// Licensed under the Apache License, Version 2.0 (the "License"); +// you may not use this file except in compliance with the License. +// You may obtain a copy of the License at +// +// http://www.apache.org/licenses/LICENSE-2.0 +// +// Unless required by applicable law or agreed to in writing, software +// distributed under the License is distributed on an "AS IS" BASIS, +// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. +// See the License for the specific language governing permissions and +// limitations under the License. + +#ifndef ROSIDL_RUNTIME_CPP__CDR_COMPATIBLE_FIXED_CAPACITY_STRING_HPP_ +#define ROSIDL_RUNTIME_CPP__CDR_COMPATIBLE_FIXED_CAPACITY_STRING_HPP_ + +#include +#include +#include +#include +#include + +namespace rosidl_runtime_cpp +{ + +/// A fixed-size, POD type, CDR binary compatible string. +/** + * Meets the same requirements as std::string. + * + * \param Capacity Maximum number of characters including the NUL terminator + */ +template +class CDRCompatibleFixedCapacityString +{ +public: + + static_assert(Capacity > 0, "Should have space for at least the NUL terminator"); + static_assert(0 == (Capacity % sizeof(uint32_t)), "Needs alignment as uint32_t"); + + // Default constructor + CDRCompatibleFixedCapacityString() + { + clear(); + } + + // Implicit conversion from C string + CDRCompatibleFixedCapacityString(const char * const str) + : CDRCompatibleFixedCapacityString() + { + if (nullptr == str) { + throw std::invalid_argument( + "Constructing CDRCompatibleFixedCapacityString with null pointer"); + } + + append_value(str); + } + + // Implicit conversion from C++ string + CDRCompatibleFixedCapacityString(const std::string& str) + : CDRCompatibleFixedCapacityString() + { + append_value(str.c_str(), str.length()); + } + + // Implicit conversion to C++ string + operator std::string() const { return std::string(m_string); } + + inline constexpr bool empty() const noexcept { return '\0' == m_string[0u]; } + + constexpr char* data() noexcept { return m_string; } + + constexpr const char* data() const noexcept { return m_string; } + + constexpr const char* c_str() const noexcept { return m_string; } + + inline constexpr size_t capacity() const { return m_capacity - 1u; } + + inline constexpr size_t max_size() const { return m_capacity - 1u; } + + size_t size() const noexcept { return length(); } + + size_t length() const noexcept + { + return ::strnlen(m_string, this->capacity()); + } + + char& operator[](size_t idx) + { + if (idx >= m_capacity) { + throw std::out_of_range("idx >= m_capacity"); + } + return m_string[idx]; + } + + char operator[](size_t idx) const + { + if (idx >= m_capacity) { + throw std::out_of_range("idx >= m_capacity"); + } + return m_string[idx]; + } + + void clear() noexcept { ::memset(m_string, 0, m_capacity); } + + int32_t compare( + const size_t my_pos, + const size_t my_count, + const char* const other, + const size_t other_count = std::string::npos) const + { + if (nullptr == other) { + throw std::invalid_argument("other is nullptr"); + } + + const size_t my_len = length(); + if (my_pos > my_len) { + throw std::out_of_range("my_pos > my_len"); + } + + const size_t my_used_count = + (std::string::npos == my_count) ? my_len : my_count; + size_t other_len = other_count; + if (std::string::npos == other_count) { + other_len = ::strlen(other); + } + + const size_t my_remaining_len = std::min(my_used_count, my_len - my_pos); + int32_t retval = ::strncmp(data() + my_pos, other, std::min(my_remaining_len, other_len)); + + if (retval == 0) { + if (my_remaining_len < other_len) { + retval = -1; + } + if (my_remaining_len > other_len) { + retval = 1; + } + } + return retval; + } + + inline int32_t compare(const char* const s) const + { + return this->compare(0U, std::string::npos, s); + } + + bool operator==(const std::string& rhs) const + { + return this->compare(rhs.c_str()) == 0; + } + + bool operator!=(const std::string& rhs) const + { + return !operator==(rhs); + } + + bool operator==(const char* const rhs) const + { + return this->compare(rhs) == 0; + } + + bool operator!=(const char* const rhs) const + { + return !operator==(rhs); + } + + CDRCompatibleFixedCapacityString& operator+=(const char* const rhs) + { + this->append_value(rhs); + return *this; + } + + template + CDRCompatibleFixedCapacityString& operator+=(const Stringable& rhs) + { + this->append_value(rhs.c_str()); + return *this; + } + + private: + + void append_value(const char* const str) { append_value(str, ::strlen(str)); } + + void append_value(const char * const str, size_t len) + { + size_t my_len = this->length(); + size_t new_len = my_len + len; + + if (new_len > capacity()) { + // TODO(MiguelCompany): Make this configurable + throw std::overflow_error("Appending string will overflow capacity"); + } + + ::memmove(&m_string[my_len], str, len); + m_string[new_len] = '\0'; + } + + // NOTE: In order for this to be binary compatible with its CDR representation, the following + // two fields shall be the only attributes in this class. + + uint32_t m_capacity = Capacity; + char m_string[Capacity]; +}; + +template +typename ::std::basic_ostream& operator<<( + std::basic_ostream& out_stream, + const CDRCompatibleFixedCapacityString& str) +{ + return out_stream.write(str.c_str(), + static_cast(str.size())); +} + +template +inline bool operator==(const Stringable& lhs, + const CDRCompatibleFixedCapacityString& rhs) +{ + return 0 == rhs.compare(lhs.c_str()); +} + +template +inline bool operator!=(const T& lhs, + const CDRCompatibleFixedCapacityString& rhs) +{ + return 0 != rhs.compare(lhs.c_str()); +} + +template +inline std::string operator+( + const char (&lhs)[N], const CDRCompatibleFixedCapacityString& rhs) +{ + return std::string(lhs) + rhs.c_str(); +} + +template +inline std::string operator+( + const std::string& lhs, const CDRCompatibleFixedCapacityString& rhs) +{ + return lhs + rhs.c_str(); +} + +} // namespace rosidl_runtime_cpp + +#endif // ROSIDL_RUNTIME_CPP__CDR_COMPATIBLE_FIXED_CAPACITY_STRING_HPP_