Skip to content

Build time generation of fixed-size strings #758

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

Draft
wants to merge 13 commits into
base: rolling
Choose a base branch
from
Draft
78 changes: 78 additions & 0 deletions rosidl_adapter/rosidl_adapter/parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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 = '#'
Expand Down Expand Up @@ -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:
Expand Down Expand Up @@ -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

Expand All @@ -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']
Expand Down
4 changes: 4 additions & 0 deletions rosidl_adapter/rosidl_adapter/resource/struct.idl.em
Original file line number Diff line number Diff line change
Expand Up @@ -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)))
Expand Down
1 change: 1 addition & 0 deletions rosidl_generator_cpp/resource/idl__struct.hpp.em
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,7 @@ include_directives = set()
#include <vector>

#include "rosidl_runtime_cpp/bounded_vector.hpp"
#include "rosidl_runtime_cpp/cdr_compatible_fixed_capacity_string.hpp"
#include "rosidl_runtime_cpp/message_initialization.hpp"

@#######################################################################
Expand Down
10 changes: 5 additions & 5 deletions rosidl_generator_cpp/resource/msg__struct.hpp.em
Original file line number Diff line number Diff line change
Expand Up @@ -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<typename %s::iterator, %s>(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<typename %s::iterator, %s>(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))
Expand All @@ -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<typename %s::iterator, %s>(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<typename %s::iterator, %s>(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
Expand Down Expand Up @@ -247,15 +247,15 @@ 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]@

@[if len(message.structure.members) != 1 or message.structure.members[0].name != EMPTY_STRUCTURE_REQUIRED_MEMBER_NAME]@
// 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;
Expand Down
41 changes: 28 additions & 13 deletions rosidl_generator_cpp/rosidl_generator_cpp/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -70,12 +71,20 @@ def prefix_with_bom_if_necessary(content):
'int64': 'int64_t',
'string': 'std::basic_string<char, std::char_traits<char>, ' +
'typename std::allocator_traits<ContainerAllocator>::template rebind_alloc<char>>',
'fixed_size_string': 'rosidl_runtime_cpp::CDRCompatibleFixedCapacityString<%u>',
'wstring': 'std::basic_string<char16_t, std::char_traits<char16_t>, typename ' +
'std::allocator_traits<ContainerAllocator>::template rebind_alloc<char16_t>>',
}


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.

Expand All @@ -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):
Expand All @@ -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.

Expand All @@ -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<ContainerAllocator>::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' +
'<ContainerAllocator>::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

Expand Down Expand Up @@ -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 \
Expand Down Expand Up @@ -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)')
Expand Down
1 change: 1 addition & 0 deletions rosidl_parser/test/msg/MyMessage.idl
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
7 changes: 7 additions & 0 deletions rosidl_parser/test/test_parser.py
Original file line number Diff line number Diff line change
Expand Up @@ -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'
Expand Down
Loading