diff --git a/README.md b/README.md index e2d1ef3cb..e4f7d18b0 100644 --- a/README.md +++ b/README.md @@ -13,6 +13,8 @@ See [documentation](https://docs.ros.org/en/rolling/Concepts/About-Internal-Inte * Generate the ROS interfaces in C * [rosidl_generator_cpp](./rosidl_generator_cpp) * Generate the ROS interfaces in C++ +* [rosidl_generator_type_description](./rosidl_generator_type_desrciption) + * Generate SHA256 hash values and ROS 2 interface descriptions for use by other generators * [rosidl_parser](./rosidl_parser) * Parser for `.idl` ROS interface files * [rosidl_runtime_c](./rosidl_runtime_c) diff --git a/rosidl_cmake/cmake/rosidl_write_generator_arguments.cmake b/rosidl_cmake/cmake/rosidl_write_generator_arguments.cmake index 33a3733ea..2c4d83cdf 100644 --- a/rosidl_cmake/cmake/rosidl_write_generator_arguments.cmake +++ b/rosidl_cmake/cmake/rosidl_write_generator_arguments.cmake @@ -32,6 +32,8 @@ function(rosidl_write_generator_arguments output_file) set(OPTIONAL_MULTI_VALUE_KEYWORDS "ROS_INTERFACE_DEPENDENCIES" # since the dependencies can be empty "TARGET_DEPENDENCIES" + "TYPE_HASH_TUPLES" + "INCLUDE_PATHS" "ADDITIONAL_FILES") cmake_parse_arguments( diff --git a/rosidl_generator_c/cmake/register_c.cmake b/rosidl_generator_c/cmake/register_c.cmake index e61455d12..7f6b84b17 100644 --- a/rosidl_generator_c/cmake/register_c.cmake +++ b/rosidl_generator_c/cmake/register_c.cmake @@ -14,6 +14,7 @@ macro(rosidl_generator_c_extras BIN GENERATOR_FILES TEMPLATE_DIR) find_package(ament_cmake_core QUIET REQUIRED) + find_package(rosidl_generator_type_description QUIET REQUIRED) ament_register_extension( "rosidl_generate_idl_interfaces" "rosidl_generator_c" diff --git a/rosidl_generator_c/cmake/rosidl_generator_c_generate_interfaces.cmake b/rosidl_generator_c/cmake/rosidl_generator_c_generate_interfaces.cmake index 11652ea9b..f38ab14a2 100644 --- a/rosidl_generator_c/cmake/rosidl_generator_c_generate_interfaces.cmake +++ b/rosidl_generator_c/cmake/rosidl_generator_c_generate_interfaces.cmake @@ -83,6 +83,7 @@ rosidl_write_generator_arguments( OUTPUT_DIR "${_output_path}" TEMPLATE_DIR "${rosidl_generator_c_TEMPLATE_DIR}" TARGET_DEPENDENCIES ${target_dependencies} + TYPE_HASH_TUPLES "${${rosidl_generate_interfaces_TARGET}__HASH_TUPLES}" ) find_package(Python3 REQUIRED COMPONENTS Interpreter) @@ -142,6 +143,9 @@ target_link_libraries(${rosidl_generate_interfaces_TARGET}${_target_suffix} PUBL rosidl_runtime_c::rosidl_runtime_c rosidl_typesupport_interface::rosidl_typesupport_interface rcutils::rcutils) +add_dependencies( + ${rosidl_generate_interfaces_TARGET}${_target_suffix} + ${rosidl_generate_interfaces_TARGET}__rosidl_generator_type_description) # Make top level generation target depend on this generated library add_dependencies( diff --git a/rosidl_generator_c/package.xml b/rosidl_generator_c/package.xml index 63a94991a..4a9d4765a 100644 --- a/rosidl_generator_c/package.xml +++ b/rosidl_generator_c/package.xml @@ -24,6 +24,7 @@ python3 rosidl_pycommon + rosidl_generator_type_description rosidl_typesupport_interface rcutils @@ -31,6 +32,7 @@ rosidl_cli rosidl_parser rcutils + rosidl_generator_type_description ament_lint_auto ament_lint_common diff --git a/rosidl_generator_c/resource/idl__struct.h.em b/rosidl_generator_c/resource/idl__struct.h.em index 94bf39bf4..9d6b2d1c4 100644 --- a/rosidl_generator_c/resource/idl__struct.h.em +++ b/rosidl_generator_c/resource/idl__struct.h.em @@ -11,6 +11,8 @@ @# - content (IdlContent, list of elements, e.g. Messages or Services) @####################################################################### @{ +from rosidl_generator_c import idl_structure_type_to_c_typename +from rosidl_generator_c import type_hash_to_c_definition from rosidl_pycommon import convert_camel_case_to_lower_case_underscore include_parts = [package_name] + list(interface_path.parents[0].parts) + [ 'detail', convert_camel_case_to_lower_case_underscore(interface_path.stem)] @@ -32,6 +34,8 @@ extern "C" #include #include +#include "rosidl_runtime_c/type_hash.h" + @####################################################################### @# Handle message @####################################################################### @@ -43,7 +47,7 @@ from rosidl_parser.definition import Message TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=message, include_directives=include_directives) + message=message, include_directives=include_directives, type_hash=type_hash) }@ @[end for]@ @@ -55,25 +59,34 @@ TEMPLATE( from rosidl_parser.definition import Service }@ @[for service in content.get_elements_of_type(Service)]@ + +@{ hash_var = idl_structure_type_to_c_typename(service.namespaced_type) + '__TYPE_VERSION_HASH' }@ +// Note: this define is for MSVC, where the static const var can't be used in downstream aggregate initializers +#define @(hash_var)__INIT @(type_hash_to_c_definition(type_hash['service'], line_final_backslash=True)) +static const rosidl_type_hash_t @(hash_var) = @(hash_var)__INIT; + @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=service.request_message, include_directives=include_directives) + message=service.request_message, include_directives=include_directives, + type_hash=type_hash['request_message']) }@ @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=service.response_message, include_directives=include_directives) + message=service.response_message, include_directives=include_directives, + type_hash=type_hash['response_message']) }@ @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=service.event_message, include_directives=include_directives) + message=service.event_message, include_directives=include_directives, + type_hash=type_hash['event_message']) }@ @[end for]@ @@ -85,74 +98,99 @@ TEMPLATE( from rosidl_parser.definition import Action }@ @[for action in content.get_elements_of_type(Action)]@ + +@{ hash_var = idl_structure_type_to_c_typename(action.namespaced_type) + '__TYPE_VERSION_HASH' }@ +#define @(hash_var)__INIT @(type_hash_to_c_definition(type_hash['action'], line_final_backslash=True)) +static const rosidl_type_hash_t @(hash_var) = @(hash_var)__INIT; + @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=action.goal, include_directives=include_directives) + message=action.goal, include_directives=include_directives, + type_hash=type_hash['goal']) }@ @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=action.result, include_directives=include_directives) + message=action.result, include_directives=include_directives, + type_hash=type_hash['result']) }@ @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=action.feedback, include_directives=include_directives) + message=action.feedback, include_directives=include_directives, + type_hash=type_hash['feedback']) }@ +@{ hash_var = idl_structure_type_to_c_typename(action.send_goal_service.namespaced_type) + '__TYPE_VERSION_HASH' }@ +// Note: this define is for MSVC, where the static const var can't be used in downstream aggregate initializers +#define @(hash_var)__INIT @(type_hash_to_c_definition(type_hash['send_goal_service']['service'], line_final_backslash=True)) +static const rosidl_type_hash_t @(hash_var) = @(hash_var)__INIT; + @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=action.send_goal_service.request_message, include_directives=include_directives) + message=action.send_goal_service.request_message, include_directives=include_directives, + type_hash=type_hash['send_goal_service']['request_message']) }@ @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=action.send_goal_service.response_message, include_directives=include_directives) + message=action.send_goal_service.response_message, include_directives=include_directives, + type_hash=type_hash['send_goal_service']['response_message']) }@ @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=action.send_goal_service.event_message, include_directives=include_directives) + message=action.send_goal_service.event_message, include_directives=include_directives, + type_hash=type_hash['send_goal_service']['event_message']) }@ +@{ hash_var = idl_structure_type_to_c_typename(action.get_result_service.namespaced_type) + '__TYPE_VERSION_HASH' }@ +// Note: this define is for MSVC, where the static const var can't be used in downstream aggregate initializers +#define @(hash_var)__INIT @(type_hash_to_c_definition(type_hash['get_result_service']['service'], line_final_backslash=True)) +static const rosidl_type_hash_t @(hash_var) = @(hash_var)__INIT; + @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=action.get_result_service.request_message, include_directives=include_directives) + message=action.get_result_service.request_message, include_directives=include_directives, + type_hash=type_hash['get_result_service']['request_message']) }@ @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=action.get_result_service.response_message, include_directives=include_directives) + message=action.get_result_service.response_message, include_directives=include_directives, + type_hash=type_hash['get_result_service']['response_message']) }@ @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=action.get_result_service.event_message, include_directives=include_directives) + message=action.get_result_service.event_message, include_directives=include_directives, + type_hash=type_hash['get_result_service']['event_message']) }@ @{ TEMPLATE( 'msg__struct.h.em', package_name=package_name, interface_path=interface_path, - message=action.feedback_message, include_directives=include_directives) + message=action.feedback_message, include_directives=include_directives, + type_hash=type_hash['feedback_message']) }@ @[end for]@ diff --git a/rosidl_generator_c/resource/msg__struct.h.em b/rosidl_generator_c/resource/msg__struct.h.em index f0b99e166..e75077af9 100644 --- a/rosidl_generator_c/resource/msg__struct.h.em +++ b/rosidl_generator_c/resource/msg__struct.h.em @@ -21,6 +21,7 @@ from rosidl_generator_c import idl_structure_type_sequence_to_c_typename from rosidl_generator_c import idl_structure_type_to_c_include_prefix from rosidl_generator_c import idl_structure_type_to_c_typename from rosidl_generator_c import interface_path_to_string +from rosidl_generator_c import type_hash_to_c_definition from rosidl_generator_c import value_to_c }@ @#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< @@ -62,6 +63,12 @@ for member in message.structure.members: @#>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>>> @#<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<<< +// Type Version Hash for interface +@{ hash_var = idl_structure_type_to_c_typename(message.structure.namespaced_type) + '__TYPE_VERSION_HASH' }@ +// Note: this define is for MSVC, where the static const var can't be used in downstream aggregate initializers +#define @(hash_var)__INIT @(type_hash_to_c_definition(type_hash['message'], line_final_backslash=True)) +static const rosidl_type_hash_t @(hash_var) = @(hash_var)__INIT; + // Constants defined in the message @[for constant in message.constants]@ diff --git a/rosidl_generator_c/rosidl_generator_c/__init__.py b/rosidl_generator_c/rosidl_generator_c/__init__.py index 17071b67f..9be5d720e 100644 --- a/rosidl_generator_c/rosidl_generator_c/__init__.py +++ b/rosidl_generator_c/rosidl_generator_c/__init__.py @@ -12,6 +12,8 @@ # See the License for the specific language governing permissions and # limitations under the License. +from rosidl_generator_type_description import parse_rihs_string +from rosidl_generator_type_description import RIHS01_HASH_VALUE_SIZE from rosidl_parser.definition import AbstractGenericString from rosidl_parser.definition import AbstractSequence from rosidl_parser.definition import AbstractString @@ -219,3 +221,28 @@ def escape_string(s): def escape_wstring(s): return escape_string(s) + + +def type_hash_to_c_definition(hash_string, *, line_final_backslash=False): + """Generate empy for rosidl_type_hash_t instance with 8 bytes per line for readability.""" + bytes_per_row = 8 + rows = 4 + assert bytes_per_row * rows == RIHS01_HASH_VALUE_SIZE, 'This function is outdated.' + indent = 4 # Uncrustify prefers this indentation + version, value = parse_rihs_string(hash_string) + assert version == 1, 'This function only knows how to generate RIHS01 definitions.' + + result = f'{{{version}, {{' + if line_final_backslash: + result += ' \\' + result += '\n' + for row in range(rows): + result += ' ' * (indent + 1) + for i in range(row * bytes_per_row, (row + 1) * bytes_per_row): + result += f' 0x{value[i * 2]}{value[i * 2 + 1]},' + if line_final_backslash: + result += ' \\' + result += '\n' + result += ' ' * indent + result += '}}' + return result diff --git a/rosidl_generator_cpp/cmake/register_cpp.cmake b/rosidl_generator_cpp/cmake/register_cpp.cmake index df16961d3..6c0eaba86 100644 --- a/rosidl_generator_cpp/cmake/register_cpp.cmake +++ b/rosidl_generator_cpp/cmake/register_cpp.cmake @@ -14,6 +14,7 @@ macro(rosidl_generator_cpp_extras BIN GENERATOR_FILES TEMPLATE_DIR) find_package(ament_cmake_core QUIET REQUIRED) + find_package(rosidl_generator_type_description QUIET REQUIRED) ament_register_extension( "rosidl_generate_idl_interfaces" "rosidl_generator_cpp" diff --git a/rosidl_generator_cpp/cmake/rosidl_generator_cpp_generate_interfaces.cmake b/rosidl_generator_cpp/cmake/rosidl_generator_cpp_generate_interfaces.cmake index 9e8f9f546..526ce76c2 100644 --- a/rosidl_generator_cpp/cmake/rosidl_generator_cpp_generate_interfaces.cmake +++ b/rosidl_generator_cpp/cmake/rosidl_generator_cpp_generate_interfaces.cmake @@ -75,6 +75,7 @@ rosidl_write_generator_arguments( OUTPUT_DIR "${_output_path}" TEMPLATE_DIR "${rosidl_generator_cpp_TEMPLATE_DIR}" TARGET_DEPENDENCIES ${target_dependencies} + TYPE_HASH_TUPLES "${${rosidl_generate_interfaces_TARGET}__HASH_TUPLES}" ) find_package(Python3 REQUIRED COMPONENTS Interpreter) @@ -99,6 +100,9 @@ add_custom_target( DEPENDS ${_generated_headers} ) +add_dependencies( + ${rosidl_generate_interfaces_TARGET}__cpp + ${rosidl_generate_interfaces_TARGET}__rosidl_generator_type_description) set(_target_suffix "__rosidl_generator_cpp") add_library(${rosidl_generate_interfaces_TARGET}${_target_suffix} INTERFACE) diff --git a/rosidl_generator_cpp/package.xml b/rosidl_generator_cpp/package.xml index ae6c1cc36..34900b24e 100644 --- a/rosidl_generator_cpp/package.xml +++ b/rosidl_generator_cpp/package.xml @@ -28,6 +28,7 @@ ament_index_python rosidl_cli + rosidl_generator_type_description rosidl_parser ament_lint_auto diff --git a/rosidl_generator_cpp/resource/action__struct.hpp.em b/rosidl_generator_cpp/resource/action__struct.hpp.em index 020455074..9c89bbdc8 100644 --- a/rosidl_generator_cpp/resource/action__struct.hpp.em +++ b/rosidl_generator_cpp/resource/action__struct.hpp.em @@ -1,5 +1,6 @@ @# Included from rosidl_generator_cpp/resource/idl__struct.hpp.em @{ +from rosidl_generator_c import type_hash_to_c_definition from rosidl_parser.definition import ACTION_FEEDBACK_MESSAGE_SUFFIX from rosidl_parser.definition import ACTION_FEEDBACK_SUFFIX from rosidl_parser.definition import ACTION_GOAL_SERVICE_SUFFIX @@ -17,42 +18,48 @@ action_name = '::'.join(action.namespaced_type.namespaced_name()) TEMPLATE( 'msg__struct.hpp.em', package_name=package_name, interface_path=interface_path, - message=action.goal, include_directives=include_directives) + message=action.goal, include_directives=include_directives, + type_hash=type_hash['goal']) }@ @{ TEMPLATE( 'msg__struct.hpp.em', package_name=package_name, interface_path=interface_path, - message=action.result, include_directives=include_directives) + message=action.result, include_directives=include_directives, + type_hash=type_hash['result']) }@ @{ TEMPLATE( 'msg__struct.hpp.em', package_name=package_name, interface_path=interface_path, - message=action.feedback, include_directives=include_directives) + message=action.feedback, include_directives=include_directives, + type_hash=type_hash['feedback']) }@ @{ TEMPLATE( 'srv__struct.hpp.em', package_name=package_name, interface_path=interface_path, - service=action.send_goal_service, include_directives=include_directives) + service=action.send_goal_service, include_directives=include_directives, + type_hash=type_hash['send_goal_service']) }@ @{ TEMPLATE( 'srv__struct.hpp.em', package_name=package_name, interface_path=interface_path, - service=action.get_result_service, include_directives=include_directives) + service=action.get_result_service, include_directives=include_directives, + type_hash=type_hash['get_result_service']) }@ @{ TEMPLATE( 'msg__struct.hpp.em', package_name=package_name, interface_path=interface_path, - message=action.feedback_message, include_directives=include_directives) + message=action.feedback_message, include_directives=include_directives, + type_hash=type_hash['feedback_message']) }@ @[for header_file in action_includes]@ @@ -72,6 +79,8 @@ namespace @(ns) @[end for]@ struct @(action.namespaced_type.name) { + static constexpr const rosidl_type_hash_t TYPE_VERSION_HASH = @(type_hash_to_c_definition(type_hash['action'])); + /// The goal message defined in the action definition. using Goal = @(action_name)@(ACTION_GOAL_SUFFIX); /// The result message defined in the action definition. diff --git a/rosidl_generator_cpp/resource/idl__struct.hpp.em b/rosidl_generator_cpp/resource/idl__struct.hpp.em index e29a1f89e..6e1615ccc 100644 --- a/rosidl_generator_cpp/resource/idl__struct.hpp.em +++ b/rosidl_generator_cpp/resource/idl__struct.hpp.em @@ -29,6 +29,7 @@ include_directives = set() #include #include +#include "rosidl_runtime_c/type_hash.h" #include "rosidl_runtime_cpp/bounded_vector.hpp" #include "rosidl_runtime_cpp/message_initialization.hpp" @@ -43,7 +44,7 @@ from rosidl_parser.definition import Message TEMPLATE( 'msg__struct.hpp.em', package_name=package_name, interface_path=interface_path, - message=message, include_directives=include_directives) + message=message, include_directives=include_directives, type_hash=type_hash) }@ @[end for]@ @@ -59,7 +60,7 @@ from rosidl_parser.definition import Service TEMPLATE( 'srv__struct.hpp.em', package_name=package_name, interface_path=interface_path, service=service, - include_directives=include_directives) + include_directives=include_directives, type_hash=type_hash) }@ @[end for]@ @@ -75,7 +76,7 @@ from rosidl_parser.definition import Action TEMPLATE( 'action__struct.hpp.em', package_name=package_name, interface_path=interface_path, action=action, - include_directives=include_directives) + include_directives=include_directives, type_hash=type_hash) }@ @[end for]@ diff --git a/rosidl_generator_cpp/resource/msg__struct.hpp.em b/rosidl_generator_cpp/resource/msg__struct.hpp.em index 02097f15e..e66a922d3 100644 --- a/rosidl_generator_cpp/resource/msg__struct.hpp.em +++ b/rosidl_generator_cpp/resource/msg__struct.hpp.em @@ -1,5 +1,6 @@ @# Included from rosidl_generator_cpp/resource/idl__struct.hpp.em @{ +from rosidl_generator_c import type_hash_to_c_definition 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 @@ -101,6 +102,9 @@ struct @(message.structure.namespaced_type.name)_ { using Type = @(message.structure.namespaced_type.name)_; + // Type Version Hash for interface + constexpr static const rosidl_type_hash_t TYPE_VERSION_HASH = @(type_hash_to_c_definition(type_hash['message'])); + @{ # The creation of the constructors for messages is a bit complicated. The goal # is to have a constructor where the user can control how the fields of the @@ -357,6 +361,9 @@ u@ using @(message.structure.namespaced_type.name) = @(message_typename)_>; +template +constexpr const rosidl_type_hash_t @(message.structure.namespaced_type.name)_::TYPE_VERSION_HASH; + // constant definitions @[for c in message.constants]@ @[ if c.name in msvc_common_macros]@ diff --git a/rosidl_generator_cpp/resource/srv__struct.hpp.em b/rosidl_generator_cpp/resource/srv__struct.hpp.em index 1466c9980..54bc65993 100644 --- a/rosidl_generator_cpp/resource/srv__struct.hpp.em +++ b/rosidl_generator_cpp/resource/srv__struct.hpp.em @@ -1,23 +1,29 @@ @# Included from rosidl_generator_cpp/resource/idl__struct.hpp.em @{ +from rosidl_generator_c import type_hash_to_c_definition +}@ +@{ TEMPLATE( 'msg__struct.hpp.em', package_name=package_name, interface_path=interface_path, - message=service.request_message, include_directives=include_directives) + message=service.request_message, include_directives=include_directives, + type_hash=type_hash['request_message']) }@ @{ TEMPLATE( 'msg__struct.hpp.em', package_name=package_name, interface_path=interface_path, - message=service.response_message, include_directives=include_directives) + message=service.response_message, include_directives=include_directives, + type_hash=type_hash['response_message']) }@ @{ TEMPLATE( 'msg__struct.hpp.em', package_name=package_name, interface_path=interface_path, - message=service.event_message, include_directives=include_directives) + message=service.event_message, include_directives=include_directives, + type_hash=type_hash['event_message']) }@ @[for ns in service.namespaced_type.namespaces]@ @@ -31,6 +37,8 @@ struct @(service.namespaced_type.name) @{ service_typename = '::'.join(service.namespaced_type.namespaced_name()) }@ + static constexpr const rosidl_type_hash_t TYPE_VERSION_HASH = @(type_hash_to_c_definition(type_hash['service'])); + using Request = @(service_typename)_Request; using Response = @(service_typename)_Response; using Event = @(service_typename)_Event; diff --git a/rosidl_generator_tests/CMakeLists.txt b/rosidl_generator_tests/CMakeLists.txt index b428bfc7b..90784dabd 100644 --- a/rosidl_generator_tests/CMakeLists.txt +++ b/rosidl_generator_tests/CMakeLists.txt @@ -19,15 +19,18 @@ find_package(ament_cmake REQUIRED) if(BUILD_TESTING) find_package(ament_cmake_gtest REQUIRED) + find_package(ament_cmake_pytest REQUIRED) find_package(ament_lint_auto REQUIRED) find_package(rosidl_cmake REQUIRED) find_package(rosidl_generator_cpp REQUIRED) + find_package(rosidl_generator_type_description REQUIRED) find_package(rosidl_runtime_c REQUIRED) find_package(rosidl_runtime_cpp REQUIRED) find_package(test_interface_files REQUIRED) ament_lint_auto_find_test_dependencies() rosidl_generate_interfaces(${PROJECT_NAME} + ${test_interface_files_ACTION_FILES} ${test_interface_files_MSG_FILES} ${test_interface_files_SRV_FILES} ADD_LINTER_TESTS @@ -108,6 +111,10 @@ if(BUILD_TESTING) ${c_generator_target} rosidl_runtime_c::rosidl_runtime_c ) + + ament_add_pytest_test(test_hash_generator test/rosidl_generator_type_description + ENV GENERATED_TEST_FILE_DIR=${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_type_description/${PROJECT_NAME} + ) endif() ament_package() diff --git a/rosidl_generator_tests/package.xml b/rosidl_generator_tests/package.xml index 6b94bc6cc..449bf8ef8 100644 --- a/rosidl_generator_tests/package.xml +++ b/rosidl_generator_tests/package.xml @@ -18,12 +18,17 @@ ament_cmake + action_msgs ament_cmake_gtest + ament_cmake_pytest ament_lint_auto ament_lint_common + ament_index_python + python3-jsonschema rosidl_cmake rosidl_generator_c rosidl_generator_cpp + rosidl_generator_type_description rosidl_runtime_c rosidl_runtime_cpp test_interface_files diff --git a/rosidl_generator_tests/test/rosidl_generator_type_description/test_type_hash.py b/rosidl_generator_tests/test/rosidl_generator_type_description/test_type_hash.py new file mode 100644 index 000000000..4fde93d05 --- /dev/null +++ b/rosidl_generator_tests/test/rosidl_generator_type_description/test_type_hash.py @@ -0,0 +1,41 @@ +# Copyright 2023 Open Source Robotics Foundation, Inc. +# +# 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. + +import json +import os +from pathlib import Path + +from ament_index_python import get_package_share_directory +import jsonschema + + +def test_type_hash(): + """Test all rosidl_generator_type_description output files against defined schemas.""" + schema_path = ( + Path(get_package_share_directory('rosidl_generator_type_description')) / 'resource' / + 'HashedTypeDescription.schema.json') + with schema_path.open('r') as schema_file: + schema = json.load(schema_file) + + generated_files_dir = Path(os.environ['GENERATED_TEST_FILE_DIR']) + validated_files = 0 + for namespace in generated_files_dir.iterdir(): + for p in namespace.iterdir(): + assert p.is_file() + assert p.suffix == '.json' + with p.open('r') as f: + instance = json.load(f) + jsonschema.validate(instance=instance, schema=schema) + validated_files += 1 + assert validated_files, 'Needed to validate at least one JSON output.' diff --git a/rosidl_generator_type_description/CMakeLists.txt b/rosidl_generator_type_description/CMakeLists.txt new file mode 100644 index 000000000..2a3949a92 --- /dev/null +++ b/rosidl_generator_type_description/CMakeLists.txt @@ -0,0 +1,29 @@ +cmake_minimum_required(VERSION 3.12) + +project(rosidl_generator_type_description) + +find_package(ament_cmake_python REQUIRED) +find_package(ament_cmake_ros REQUIRED) + +ament_index_register_resource("rosidl_generator_packages") +ament_python_install_package(${PROJECT_NAME}) + +if(BUILD_TESTING) + find_package(ament_lint_auto REQUIRED) + find_package(ament_cmake_pytest REQUIRED) + ament_lint_auto_find_test_dependencies() + ament_add_pytest_test(pytest_type_hash_generator test) +endif() + +ament_package( + CONFIG_EXTRAS "${PROJECT_NAME}-extras.cmake.in" +) + +install( + PROGRAMS bin/${PROJECT_NAME} + DESTINATION lib/${PROJECT_NAME} +) +install( + DIRECTORY cmake resource + DESTINATION share/${PROJECT_NAME} +) diff --git a/rosidl_generator_type_description/README.md b/rosidl_generator_type_description/README.md new file mode 100644 index 000000000..318d2a05d --- /dev/null +++ b/rosidl_generator_type_description/README.md @@ -0,0 +1,14 @@ +# rosidl_generator_type_description + +This generator serializes ROS 2 interface descriptions (message, service, action) to a common format and uses SHA256 to hash that representation into a unique hash for each type. + +The SHA256 hashes generated by this package must match those generated by `rcl_calculate_type_version_hash`. The `.json` files generated must, therefore, match the result of `rcl_type_description_to_hashable_json`. + +## Generated files + +This generator creates one output file per interface, `interface_name.json`. + +This file follows the schema [`HashedTypedDescription`](./resource/HashedTypeDescription.schema.json). +It contains a tree of hashes for the top-level interface and any of its generated subinterfaces (such as request and response messages for a service), as well as fully-expanded descriptions of the interface type. +This description is a representation of `type_description_interfaces/msg/TypeDescription`, including all recursively-referenced types. +This way, dependent descriptions may use this interface and recurse no further to know the full set of referenced types it needs to know about. diff --git a/rosidl_generator_type_description/bin/rosidl_generator_type_description b/rosidl_generator_type_description/bin/rosidl_generator_type_description new file mode 100755 index 000000000..6c0175e37 --- /dev/null +++ b/rosidl_generator_type_description/bin/rosidl_generator_type_description @@ -0,0 +1,50 @@ +#!/usr/bin/env python3 +# Copyright 2023 Open Source Robotics Foundation, Inc. +# +# 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. + +import argparse +import os +import sys + +try: + from rosidl_generator_type_description import generate_type_hash +except ImportError: + # modifying sys.path and importing the Python package with the same + # name as this script does not work on Windows + rosidl_generator_type_description_root = os.path.dirname(os.path.dirname(__file__)) + rosidl_generator_type_description_module = os.path.join( + rosidl_generator_type_description_root, 'rosidl_generator_type_description', '__init__.py') + if not os.path.exists(rosidl_generator_type_description_module): + raise + from importlib.machinery import SourceFileLoader + + loader = SourceFileLoader('rosidl_generator_type_description', rosidl_generator_type_description_module) + rosidl_generator_type_description = loader.load_module() + generate_type_hash = rosidl_generator_type_description.generate_type_hash + + +def main(argv=sys.argv[1:]): + parser = argparse.ArgumentParser( + description='Generate hashable representations and hashes of ROS interfaces.', + formatter_class=argparse.ArgumentDefaultsHelpFormatter) + parser.add_argument( + '--generator-arguments-file', + required=True, + help='The location of the file containing the generator arguments') + args = parser.parse_args(argv) + generate_type_hash(args.generator_arguments_file) + + +if __name__ == '__main__': + sys.exit(main()) diff --git a/rosidl_generator_type_description/cmake/rosidl_generator_type_description_generate_interfaces.cmake b/rosidl_generator_type_description/cmake/rosidl_generator_type_description_generate_interfaces.cmake new file mode 100644 index 000000000..2cf1c7635 --- /dev/null +++ b/rosidl_generator_type_description/cmake/rosidl_generator_type_description_generate_interfaces.cmake @@ -0,0 +1,91 @@ +# Copyright 2023 Open Source Robotics Foundation, Inc. +# +# 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. + +find_package(Python3 REQUIRED COMPONENTS Interpreter) + +set(_output_path "${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_type_description/${PROJECT_NAME}") +set(_generated_files "") +set(_generated_tuples "") + +# Create list of generated files +foreach(_abs_idl_file ${rosidl_generate_interfaces_ABS_IDL_FILES}) + get_filename_component(_parent_folder "${_abs_idl_file}" DIRECTORY) + get_filename_component(_parent_folder "${_parent_folder}" NAME) + get_filename_component(_idl_name "${_abs_idl_file}" NAME) + get_filename_component(_idl_stem "${_idl_name}" NAME_WE) + set(_json_file "${_output_path}/${_parent_folder}/${_idl_stem}.json") + list(APPEND _generated_files "${_json_file}") + list(APPEND _generated_tuples "${_parent_folder}/${_idl_name}:${_json_file}") +endforeach() + +# Find dependency packages' generated files +set(_dependency_files "") +set(_dependency_paths "") +foreach(_pkg_name ${rosidl_generate_interfaces_DEPENDENCY_PACKAGE_NAMES}) + set(_include_path "${${_pkg_name}_DIR}/..") + normalize_path(_include_path "${_include_path}") + list(APPEND _dependency_paths "${_pkg_name}:${_include_path}") +endforeach() + +# Export __HASH_TUPLES variable for use by dependent generators +set(${rosidl_generate_interfaces_TARGET}__HASH_TUPLES ${_generated_tuples}) + +# Validate that all dependencies exist +set(target_dependencies + "${rosidl_generator_type_description_BIN}" + ${rosidl_generator_type_description_GENERATOR_FILES} + ${rosidl_generate_interfaces_ABS_IDL_FILES} + ${_dependency_files}) +foreach(dep ${target_dependencies}) + if(NOT EXISTS "${dep}") + message(FATAL_ERROR "Target dependency '${dep}' does not exist") + endif() +endforeach() + +set(_generator_arguments_file "${CMAKE_CURRENT_BINARY_DIR}/rosidl_generator_type_description__arguments.json") +rosidl_write_generator_arguments( + "${_generator_arguments_file}" + PACKAGE_NAME "${PROJECT_NAME}" + IDL_TUPLES "${rosidl_generate_interfaces_IDL_TUPLES}" + OUTPUT_DIR "${_output_path}" + INCLUDE_PATHS "${_dependency_paths}" +) + +# Create custom command and target to generate the hash output +add_custom_command( + COMMAND Python3::Interpreter + ARGS + ${rosidl_generator_type_description_BIN} + --generator-arguments-file "${_generator_arguments_file}" + OUTPUT ${_generated_files} + DEPENDS ${target_dependencies} + COMMENT "Generating type hashes for ROS interfaces" + VERBATIM +) + +set(_target "${rosidl_generate_interfaces_TARGET}__rosidl_generator_type_description") +add_custom_target(${_target} DEPENDS ${_generated_files}) + +# Make top level generation target depend on this generated library +add_dependencies(${rosidl_generate_interfaces_TARGET} ${_target}) + +if(NOT rosidl_generate_interfaces_SKIP_INSTALL) + foreach(_generated_file ${_generated_files}) + get_filename_component(_parent_folder "${_generated_file}" DIRECTORY) + get_filename_component(_parent_folder "${_parent_folder}" NAME) + install( + FILES ${_generated_file} + DESTINATION "share/${PROJECT_NAME}/${_parent_folder}") + endforeach() +endif() diff --git a/rosidl_generator_type_description/package.xml b/rosidl_generator_type_description/package.xml new file mode 100644 index 000000000..4886a4fe7 --- /dev/null +++ b/rosidl_generator_type_description/package.xml @@ -0,0 +1,32 @@ + + + + rosidl_generator_type_description + 3.4.0 + Generate hashes and descriptions of ROS 2 interface types, per REP-2011. + + Emerson Knapp + + Apache License 2.0 + + Emerson Knapp + + ament_cmake_python + ament_cmake_ros + + ament_cmake_core + python3 + + ament_index_python + rosidl_cli + rosidl_parser + + ament_lint_auto + ament_lint_common + + rosidl_generator_packages + + + ament_cmake + + diff --git a/rosidl_generator_type_description/resource/HashedTypeDescription.schema.json b/rosidl_generator_type_description/resource/HashedTypeDescription.schema.json new file mode 100644 index 000000000..14f17a6c0 --- /dev/null +++ b/rosidl_generator_type_description/resource/HashedTypeDescription.schema.json @@ -0,0 +1,114 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "HashedTypeDescription.schema.json", + "title": "HashedTypeDescription", + "description": "Contains hashes and full type description for a ROS 2 interface. TypeDescription, IndividualTypeDescription, Field, and FieldType are exact representations of type_description_interfaces/msg types, see their .msg files for semantic comments.", + "type": "object", + "properties": { + "hashes": { + "type": "object", + "oneOf": [ + { "$ref": "#/$defs/MessageHash" }, + { "$ref": "#/$defs/ServiceHashes" }, + { "$ref": "#/$defs/ActionHashes" } + ] + }, + "type_description_msg": { "$ref": "#/$defs/TypeDescription" } + }, + "required": ["hashes", "type_description_msg"], + "additionalProperties": false, + "$defs": { + "MessageHash": { + "type": "object", + "properties": { + "message": { "type": "string" } + }, + "required": [ "message" ], + "additionalProperties": false + }, + "ServiceHashes": { + "type": "object", + "properties": { + "service": { "type": "string" }, + "request_message": { "$ref": "#/$defs/MessageHash" }, + "response_message": { "$ref": "#/$defs/MessageHash" }, + "event_message": { "$ref": "#/$defs/MessageHash" } + }, + "required": [ + "service", + "request_message", + "response_message", + "event_message" + ], + "additionalProperties": false + }, + "ActionHashes": { + "type": "object", + "properties": { + "action": { "type": "string" }, + "goal": { "$ref": "#/$defs/MessageHash" }, + "result": { "$ref": "#/$defs/MessageHash" }, + "feedback": { "$ref": "#/$defs/MessageHash" }, + "send_goal_service": { "$ref": "#/$defs/ServiceHashes" }, + "get_result_service": { "$ref": "#/$defs/ServiceHashes" }, + "feedback_message": { "$ref": "#/$defs/MessageHash" } + }, + "required": [ + "action", + "goal", + "result", + "feedback", + "send_goal_service", + "get_result_service", + "feedback_message" + ], + "additionalProperties": false + }, + "TypeDescription": { + "type": "object", + "$comment": "For hashing: All whitespace must be excluded, which this schema cannot enforce.", + "properties": { + "type_description": {"$ref": "#/$defs/IndividualTypeDescription"}, + "referenced_type_descriptions": { + "$comment": "For hashing: Referenced type descriptions must be alphabetized, which this schema cannot enforce.", + "type": "array", + "items": { "$ref": "#/$defs/IndividualTypeDescription" } + } + }, + "required": ["type_description", "referenced_type_descriptions"], + "additionalProperties": false + }, + "IndividualTypeDescription": { + "type": "object", + "properties": { + "type_name": {"type": "string", "maxLength": 255}, + "fields": { + "type": "array", + "items": { "$ref": "#/$defs/Field" } + } + }, + "required": ["type_name", "fields"], + "additionalProperties": false + }, + "Field": { + "type": "object", + "properties": { + "name": {"type": "string"}, + "type": {"$ref": "#/$defs/FieldType"} + }, + "required": ["name", "type"], + "additionalProperties": false + }, + "FieldType": { + "type": "object", + "properties": { + "type_id": {"type": "integer", "minimum": 0, "maximum": 255}, + "capacity": {"type": "integer", "minimum": 0}, + "string_capacity": {"type": "integer", "minimum": 0}, + "nested_type_name": {"type": "string", "maxLength": 255} + }, + "required": ["type_id", "capacity", "string_capacity", "nested_type_name"], + "additionalProperties": false + } + } +} diff --git a/rosidl_generator_type_description/rosidl_generator_type_description-extras.cmake.in b/rosidl_generator_type_description/rosidl_generator_type_description-extras.cmake.in new file mode 100644 index 000000000..eefe59a1b --- /dev/null +++ b/rosidl_generator_type_description/rosidl_generator_type_description-extras.cmake.in @@ -0,0 +1,16 @@ +find_package(ament_cmake_core QUIET REQUIRED) + +ament_register_extension( + "rosidl_generate_idl_interfaces" + "rosidl_generator_type_description" + "rosidl_generator_type_description_generate_interfaces.cmake") + +set(rosidl_generator_type_description_BIN + "${rosidl_generator_type_description_DIR}/../../../lib/rosidl_generator_type_description/rosidl_generator_type_description") +normalize_path(rosidl_generator_type_description_BIN + "${rosidl_generator_type_description_BIN}") + +set(rosidl_generator_type_description_GENERATOR_FILES + "${rosidl_generator_type_description_DIR}/../../../@PYTHON_INSTALL_DIR@/rosidl_generator_type_description/__init__.py") +normalize_path(rosidl_generator_type_description_GENERATOR_FILES + "${rosidl_generator_type_description_GENERATOR_FILES}") diff --git a/rosidl_generator_type_description/rosidl_generator_type_description/__init__.py b/rosidl_generator_type_description/rosidl_generator_type_description/__init__.py new file mode 100644 index 000000000..117a6c951 --- /dev/null +++ b/rosidl_generator_type_description/rosidl_generator_type_description/__init__.py @@ -0,0 +1,477 @@ +# Copyright 2023 Open Source Robotics Foundation, Inc. +# +# 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. + +import hashlib +import json +from pathlib import Path +import re +import sys +from typing import List, Tuple + +from rosidl_parser import definition +from rosidl_parser.parser import parse_idl_file + +# RIHS: ROS Interface Hashing Standard, per REP-2011 +# NOTE: These values and implementations must be updated if +# - type_description_interfaces messsages change, or +# - the hashing algorithm for type descriptions changes +# Both changes require an increment of the RIHS version +RIHS01_PREFIX = 'RIHS01_' +RIHS01_HASH_VALUE_SIZE = 32 +RIHS01_PATTERN = re.compile(r'RIHS([0-9a-f]{2})_([0-9a-f]{64})') + + +def generate_type_hash(generator_arguments_file: str) -> List[str]: + with open(generator_arguments_file, 'r') as f: + args = json.load(f) + package_name = args['package_name'] + output_dir = Path(args['output_dir']) + idl_tuples = args['idl_tuples'] + include_paths = args.get('include_paths', []) + + # Lookup for directory containing dependency .json files + include_map = { + package_name: output_dir + } + for include_tuple in include_paths: + include_parts = include_tuple.split(':', 1) + assert len(include_parts) == 2 + include_package_name, include_base_path = include_parts + include_map[include_package_name] = Path(include_base_path) + + generated_files = [] + hashers = {} + + # Initialize all local types first so they can be referenced by other local types + for idl_tuple in idl_tuples: + idl_parts = idl_tuple.rsplit(':', 1) + assert len(idl_parts) == 2 + locator = definition.IdlLocator(*idl_parts) + try: + idl_file = parse_idl_file(locator) + except Exception as e: + print('Error processing idl file: ' + + str(locator.get_absolute_path()), file=sys.stderr) + raise(e) + + idl_rel_path = Path(idl_parts[1]) + generate_to_dir = (output_dir / idl_rel_path).parent + generate_to_dir.mkdir(parents=True, exist_ok=True) + + hasher = InterfaceHasher.from_idl(idl_file) + hashers[hasher.namespaced_type.namespaced_name()] = hasher + + # Generate output files + for hasher in hashers.values(): + generated_files += hasher.write_unified_json(output_dir, hashers, include_map) + + return generated_files + + +def parse_rihs_string(rihs_str: str) -> Tuple[int, str]: + """Parse RIHS string, return (version, value) tuple.""" + match = RIHS01_PATTERN.match(rihs_str) + if not match: + raise ValueError(f'Type hash string {rihs_str} does not match expected RIHS format.') + version, value = match.group(1, 2) + return (int(version), value) + + +# This mapping must match the constants defined in type_description_interfaces/msgs/FieldType.msg +# NOTE: Nonexplicit integer types are not defined in FieldType (short, long, long long). +# If a ROS IDL uses these, this generator will throw a KeyError. +FIELD_VALUE_TYPE_NAMES = { + None: 'FIELD_TYPE_NOT_SET', + 'nested_type': 'FIELD_TYPE_NESTED_TYPE', + 'int8': 'FIELD_TYPE_INT8', + 'uint8': 'FIELD_TYPE_UINT8', + 'int16': 'FIELD_TYPE_INT16', + 'uint16': 'FIELD_TYPE_UINT16', + 'int32': 'FIELD_TYPE_INT32', + 'uint32': 'FIELD_TYPE_UINT32', + 'int64': 'FIELD_TYPE_INT64', + 'uint64': 'FIELD_TYPE_UINT64', + 'float': 'FIELD_TYPE_FLOAT', + 'double': 'FIELD_TYPE_DOUBLE', + 'long': 'LONG_DOUBLE', + 'char': 'FIELD_TYPE_CHAR', + 'wchar': 'FIELD_TYPE_WCHAR', + 'boolean': 'FIELD_TYPE_BOOLEAN', + 'octet': 'FIELD_TYPE_BYTE', + definition.UnboundedString: 'FIELD_TYPE_STRING', + definition.UnboundedWString: 'FIELD_TYPE_WSTRING', + # NOTE: rosidl_parser does not define fixed string types + definition.BoundedString: 'FIELD_TYPE_BOUNDED_STRING', + definition.BoundedWString: 'FIELD_TYPE_BOUNDED_WSTRING', +} + +NESTED_FIELD_TYPE_SUFFIXES = { + definition.Array: '_ARRAY', + definition.BoundedSequence: '_BOUNDED_SEQUENCE', + definition.UnboundedSequence: '_UNBOUNDED_SEQUENCE', +} + +# Copied directly from FieldType.msg, with simple string manipulation to create a dict +FIELD_TYPE_IDS = { + 'FIELD_TYPE_NOT_SET': 0, + + # Nested type defined in other .msg/.idl files. + 'FIELD_TYPE_NESTED_TYPE': 1, + + # Basic Types + # Integer Types + 'FIELD_TYPE_INT8': 2, + 'FIELD_TYPE_UINT8': 3, + 'FIELD_TYPE_INT16': 4, + 'FIELD_TYPE_UINT16': 5, + 'FIELD_TYPE_INT32': 6, + 'FIELD_TYPE_UINT32': 7, + 'FIELD_TYPE_INT64': 8, + 'FIELD_TYPE_UINT64': 9, + + # Floating-Point Types + 'FIELD_TYPE_FLOAT': 10, + 'FIELD_TYPE_DOUBLE': 11, + 'FIELD_TYPE_LONG_DOUBLE': 12, + + # Char and WChar Types + 'FIELD_TYPE_CHAR': 13, + 'FIELD_TYPE_WCHAR': 14, + + # Boolean Type + 'FIELD_TYPE_BOOLEAN': 15, + + # Byte/Octet Type + 'FIELD_TYPE_BYTE': 16, + + # String Types + 'FIELD_TYPE_STRING': 17, + 'FIELD_TYPE_WSTRING': 18, + + # Fixed String Types + 'FIELD_TYPE_FIXED_STRING': 19, + 'FIELD_TYPE_FIXED_WSTRING': 20, + + # Bounded String Types + 'FIELD_TYPE_BOUNDED_STRING': 21, + 'FIELD_TYPE_BOUNDED_WSTRING': 22, + + # Fixed Sized Array Types + 'FIELD_TYPE_NESTED_TYPE_ARRAY': 49, + 'FIELD_TYPE_INT8_ARRAY': 50, + 'FIELD_TYPE_UINT8_ARRAY': 51, + 'FIELD_TYPE_INT16_ARRAY': 52, + 'FIELD_TYPE_UINT16_ARRAY': 53, + 'FIELD_TYPE_INT32_ARRAY': 54, + 'FIELD_TYPE_UINT32_ARRAY': 55, + 'FIELD_TYPE_INT64_ARRAY': 56, + 'FIELD_TYPE_UINT64_ARRAY': 57, + 'FIELD_TYPE_FLOAT_ARRAY': 58, + 'FIELD_TYPE_DOUBLE_ARRAY': 59, + 'FIELD_TYPE_LONG_DOUBLE_ARRAY': 60, + 'FIELD_TYPE_CHAR_ARRAY': 61, + 'FIELD_TYPE_WCHAR_ARRAY': 62, + 'FIELD_TYPE_BOOLEAN_ARRAY': 63, + 'FIELD_TYPE_BYTE_ARRAY': 64, + 'FIELD_TYPE_STRING_ARRAY': 65, + 'FIELD_TYPE_WSTRING_ARRAY': 66, + 'FIELD_TYPE_FIXED_STRING_ARRAY': 67, + 'FIELD_TYPE_FIXED_WSTRING_ARRAY': 68, + 'FIELD_TYPE_BOUNDED_STRING_ARRAY': 69, + 'FIELD_TYPE_BOUNDED_WSTRING_ARRAY': 70, + + # Bounded Sequence Types + 'FIELD_TYPE_NESTED_TYPE_BOUNDED_SEQUENCE': 97, + 'FIELD_TYPE_INT8_BOUNDED_SEQUENCE': 98, + 'FIELD_TYPE_UINT8_BOUNDED_SEQUENCE': 99, + 'FIELD_TYPE_INT16_BOUNDED_SEQUENCE': 100, + 'FIELD_TYPE_UINT16_BOUNDED_SEQUENCE': 101, + 'FIELD_TYPE_INT32_BOUNDED_SEQUENCE': 102, + 'FIELD_TYPE_UINT32_BOUNDED_SEQUENCE': 103, + 'FIELD_TYPE_INT64_BOUNDED_SEQUENCE': 104, + 'FIELD_TYPE_UINT64_BOUNDED_SEQUENCE': 105, + 'FIELD_TYPE_FLOAT_BOUNDED_SEQUENCE': 106, + 'FIELD_TYPE_DOUBLE_BOUNDED_SEQUENCE': 107, + 'FIELD_TYPE_LONG_DOUBLE_BOUNDED_SEQUENCE': 108, + 'FIELD_TYPE_CHAR_BOUNDED_SEQUENCE': 109, + 'FIELD_TYPE_WCHAR_BOUNDED_SEQUENCE': 110, + 'FIELD_TYPE_BOOLEAN_BOUNDED_SEQUENCE': 111, + 'FIELD_TYPE_BYTE_BOUNDED_SEQUENCE': 112, + 'FIELD_TYPE_STRING_BOUNDED_SEQUENCE': 113, + 'FIELD_TYPE_WSTRING_BOUNDED_SEQUENCE': 114, + 'FIELD_TYPE_FIXED_STRING_BOUNDED_SEQUENCE': 115, + 'FIELD_TYPE_FIXED_WSTRING_BOUNDED_SEQUENCE': 116, + 'FIELD_TYPE_BOUNDED_STRING_BOUNDED_SEQUENCE': 117, + 'FIELD_TYPE_BOUNDED_WSTRING_BOUNDED_SEQUENCE': 118, + + # Unbounded Sequence Types + 'FIELD_TYPE_NESTED_TYPE_UNBOUNDED_SEQUENCE': 145, + 'FIELD_TYPE_INT8_UNBOUNDED_SEQUENCE': 146, + 'FIELD_TYPE_UINT8_UNBOUNDED_SEQUENCE': 147, + 'FIELD_TYPE_INT16_UNBOUNDED_SEQUENCE': 148, + 'FIELD_TYPE_UINT16_UNBOUNDED_SEQUENCE': 149, + 'FIELD_TYPE_INT32_UNBOUNDED_SEQUENCE': 150, + 'FIELD_TYPE_UINT32_UNBOUNDED_SEQUENCE': 151, + 'FIELD_TYPE_INT64_UNBOUNDED_SEQUENCE': 152, + 'FIELD_TYPE_UINT64_UNBOUNDED_SEQUENCE': 153, + 'FIELD_TYPE_FLOAT_UNBOUNDED_SEQUENCE': 154, + 'FIELD_TYPE_DOUBLE_UNBOUNDED_SEQUENCE': 155, + 'FIELD_TYPE_LONG_DOUBLE_UNBOUNDED_SEQUENCE': 156, + 'FIELD_TYPE_CHAR_UNBOUNDED_SEQUENCE': 157, + 'FIELD_TYPE_WCHAR_UNBOUNDED_SEQUENCE': 158, + 'FIELD_TYPE_BOOLEAN_UNBOUNDED_SEQUENCE': 159, + 'FIELD_TYPE_BYTE_UNBOUNDED_SEQUENCE': 160, + 'FIELD_TYPE_STRING_UNBOUNDED_SEQUENCE': 161, + 'FIELD_TYPE_WSTRING_UNBOUNDED_SEQUENCE': 162, + 'FIELD_TYPE_FIXED_STRING_UNBOUNDED_SEQUENCE': 163, + 'FIELD_TYPE_FIXED_WSTRING_UNBOUNDED_SEQUENCE': 164, + 'FIELD_TYPE_BOUNDED_STRING_UNBOUNDED_SEQUENCE': 165, + 'FIELD_TYPE_BOUNDED_WSTRING_UNBOUNDED_SEQUENCE': 166, +} + + +def field_type_type_name(ftype: definition.AbstractType) -> str: + value_type = ftype + name_suffix = '' + + if isinstance(ftype, definition.AbstractNestedType): + value_type = ftype.value_type + name_suffix = NESTED_FIELD_TYPE_SUFFIXES[type(ftype)] + + if isinstance(value_type, definition.BasicType): + value_type_name = FIELD_VALUE_TYPE_NAMES[value_type.typename] + elif isinstance(value_type, definition.AbstractGenericString): + value_type_name = FIELD_VALUE_TYPE_NAMES[type(value_type)] + elif ( + isinstance(value_type, definition.NamespacedType) or + isinstance(value_type, definition.NamedType) + ): + value_type_name = 'FIELD_TYPE_NESTED_TYPE' + else: + raise ValueError(f'Unknown value type {value_type}') + + return value_type_name + name_suffix + + +def field_type_type_id(ftype: definition.AbstractType) -> Tuple[str, int]: + return FIELD_TYPE_IDS[field_type_type_name(ftype)] + + +def field_type_capacity(ftype: definition.AbstractType) -> int: + if isinstance(ftype, definition.AbstractNestedType): + if ftype.has_maximum_size(): + try: + return ftype.maximum_size + except AttributeError: + return ftype.size + return 0 + + +def field_type_string_capacity(ftype: definition.AbstractType) -> int: + value_type = ftype + if isinstance(ftype, definition.AbstractNestedType): + value_type = ftype.value_type + + if isinstance(value_type, definition.AbstractGenericString): + if value_type.has_maximum_size(): + try: + return value_type.maximum_size + except AttributeError: + return value_type.size + return 0 + + +def field_type_nested_type_name(ftype: definition.AbstractType, joiner='/') -> str: + value_type = ftype + if isinstance(ftype, definition.AbstractNestedType): + value_type = ftype.value_type + if isinstance(value_type, definition.NamespacedType): + return joiner.join(value_type.namespaced_name()) + elif isinstance(value_type, definition.NamedType): + return value_type.name + return '' + + +def serialize_field_type(ftype: definition.AbstractType) -> dict: + return { + 'type_id': field_type_type_id(ftype), + 'capacity': field_type_capacity(ftype), + 'string_capacity': field_type_string_capacity(ftype), + 'nested_type_name': field_type_nested_type_name(ftype), + } + + +def serialize_field(member: definition.Member) -> dict: + return { + 'name': member.name, + 'type': serialize_field_type(member.type), + # skipping default_value + } + + +def serialize_individual_type_description( + namespaced_type: definition.NamespacedType, members: List[definition.Member] +) -> dict: + return { + 'type_name': '/'.join(namespaced_type.namespaced_name()), + 'fields': [serialize_field(member) for member in members] + } + + +class InterfaceHasher: + """Contains context about subinterfaces for a given interface description.""" + + @classmethod + def from_idl(cls, idl: definition.IdlFile): + for el in idl.content.elements: + if any(isinstance(el, type_) for type_ in [ + definition.Message, definition.Service, definition.Action + ]): + return InterfaceHasher(el) + raise ValueError('No interface found in IDL') + + def __init__(self, interface): + self.interface = interface + self.subinterfaces = {} + + # Determine top level interface, and member fields based on that + if isinstance(interface, definition.Message): + self.namespaced_type = interface.structure.namespaced_type + self.interface_type = 'message' + self.members = interface.structure.members + elif isinstance(interface, definition.Service): + self.namespaced_type = interface.namespaced_type + self.interface_type = 'service' + self.subinterfaces = { + 'request_message': InterfaceHasher(interface.request_message), + 'response_message': InterfaceHasher(interface.response_message), + 'event_message': InterfaceHasher(interface.event_message), + } + self.members = [ + definition.Member(hasher.namespaced_type, field_name) + for field_name, hasher in self.subinterfaces.items() + ] + elif isinstance(interface, definition.Action): + self.namespaced_type = interface.namespaced_type + self.interface_type = 'action' + self.subinterfaces = { + 'goal': InterfaceHasher(interface.goal), + 'result': InterfaceHasher(interface.result), + 'feedback': InterfaceHasher(interface.feedback), + 'send_goal_service': InterfaceHasher(interface.send_goal_service), + 'get_result_service': InterfaceHasher(interface.get_result_service), + 'feedback_message': InterfaceHasher(interface.feedback_message), + } + self.members = [ + definition.Member(hasher.namespaced_type, field_name) + for field_name, hasher in self.subinterfaces.items() + ] + + self.individual_type_description = serialize_individual_type_description( + self.namespaced_type, self.members) + + # Determine needed includes from member fields + self.includes = [] + for member in self.members: + if isinstance(member.type, definition.NamespacedType): + self.includes.append(member.type.namespaced_name()) + elif ( + isinstance(member.type, definition.AbstractNestedType) and + isinstance(member.type.value_type, definition.NamespacedType) + ): + self.includes.append(member.type.value_type.namespaced_name()) + + self.rel_path = Path(*self.namespaced_type.namespaced_name()[1:]) + self.include_path = Path(*self.namespaced_type.namespaced_name()) + + def write_unified_json( + self, output_dir: Path, local_hashers: dict, includes_map: dict + ) -> List[Path]: + generated_files = [] + referenced_types = {} + + for key, val in self.subinterfaces.items(): + generated_files += val.write_unified_json(output_dir, local_hashers, includes_map) + + def add_referenced_type(individual_type_description): + type_name = individual_type_description['type_name'] + if ( + type_name in referenced_types and + referenced_types[type_name] != individual_type_description + ): + raise Exception('Encountered two definitions of the same referenced type') + referenced_types[type_name] = individual_type_description + + process_includes = self.includes[:] + while process_includes: + process_type = process_includes.pop() + + # A type in this package may refer to types, and hasn't been unrolled yet, + # so process its includes breadth first + if process_type in local_hashers: + add_referenced_type(local_hashers[process_type].individual_type_description) + process_includes += local_hashers[process_type].includes + continue + + # All nonlocal descriptions will have all recursively referenced types baked in + p_path = Path(*process_type).with_suffix('.json') + pkg = p_path.parts[0] + pkg_dir = includes_map[pkg] + include_path = pkg_dir / p_path.relative_to(pkg) + with include_path.open('r') as include_file: + include_json = json.load(include_file) + + type_description_msg = include_json['type_description_msg'] + add_referenced_type(type_description_msg['type_description']) + for rt in type_description_msg['referenced_type_descriptions']: + add_referenced_type(rt) + + self.full_type_description = { + 'type_description': self.individual_type_description, + 'referenced_type_descriptions': sorted( + referenced_types.values(), key=lambda td: td['type_name']) + } + + hashed_type_description = { + 'hashes': self._calculate_hash_tree(), + 'type_description_msg': self.full_type_description, + } + + json_path = output_dir / self.rel_path.with_suffix('.json') + with json_path.open('w', encoding='utf-8') as json_file: + json_file.write(json.dumps(hashed_type_description, indent=2)) + return generated_files + [json_path] + + def _calculate_hash_tree(self) -> dict: + hashable_repr = json.dumps( + self.full_type_description, + skipkeys=False, + ensure_ascii=True, + check_circular=True, + allow_nan=False, + indent=None, + separators=(',', ': '), + sort_keys=False + ) + sha = hashlib.sha256() + sha.update(hashable_repr.encode('utf-8')) + type_hash = RIHS01_PREFIX + sha.hexdigest() + + type_hash_infos = { + self.interface_type: type_hash, + } + for key, val in self.subinterfaces.items(): + type_hash_infos[key] = val._calculate_hash_tree() + + return type_hash_infos diff --git a/rosidl_generator_type_description/test/test_serializers.py b/rosidl_generator_type_description/test/test_serializers.py new file mode 100644 index 000000000..868cde18a --- /dev/null +++ b/rosidl_generator_type_description/test/test_serializers.py @@ -0,0 +1,91 @@ +# Copyright 2023 Open Source Robotics Foundation, Inc. +# +# 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. + +from rosidl_generator_type_description import serialize_field_type +from rosidl_generator_type_description import serialize_individual_type_description + +from rosidl_parser import definition + + +def test_field_type_serializer(): + # Sanity check for the more complex capacity/string_capacity types and nesting + string_limit = 12 + array_size = 22 + test_type = definition.Array(definition.BoundedString(string_limit), array_size) + expected = { + 'type_id': 69, + 'capacity': array_size, + 'string_capacity': string_limit, + 'nested_type_name': '', + } + + result = serialize_field_type(test_type) + assert result == expected + + bounded_sequence_limit = 32 + test_type = definition.BoundedSequence(definition.UnboundedString(), bounded_sequence_limit) + expected = { + 'type_id': 113, + 'capacity': bounded_sequence_limit, + 'string_capacity': 0, + 'nested_type_name': '', + } + result = serialize_field_type(test_type) + assert result == expected + + test_type = definition.BoundedWString(string_limit) + expected = { + 'type_id': 22, + 'capacity': 0, + 'string_capacity': string_limit, + 'nested_type_name': '', + } + result = serialize_field_type(test_type) + assert result == expected + + +def test_nested_type_serializer(): + namespaced_type = definition.NamespacedType(['my_pkg', 'msg'], 'TestThing') + referenced_type = definition.NamespacedType(['other_pkg', 'msg'], 'RefThing') + nested_referenced_type = definition.UnboundedSequence(referenced_type) + members = [ + definition.Member(referenced_type, 'ref_thing'), + definition.Member(nested_referenced_type, 'ref_things') + ] + expected = { + 'type_name': 'my_pkg/msg/TestThing', + 'fields': [ + { + 'name': 'ref_thing', + 'type': { + 'type_id': 1, + 'capacity': 0, + 'string_capacity': 0, + 'nested_type_name': 'other_pkg/msg/RefThing', + }, + }, + { + 'name': 'ref_things', + 'type': { + 'type_id': 145, + 'capacity': 0, + 'string_capacity': 0, + 'nested_type_name': 'other_pkg/msg/RefThing', + }, + }, + ], + } + result = serialize_individual_type_description(namespaced_type, members) + + assert result == expected diff --git a/rosidl_pycommon/rosidl_pycommon/__init__.py b/rosidl_pycommon/rosidl_pycommon/__init__.py index 053d53c72..6fc6a1fe6 100644 --- a/rosidl_pycommon/rosidl_pycommon/__init__.py +++ b/rosidl_pycommon/rosidl_pycommon/__init__.py @@ -62,11 +62,26 @@ def generate_files( latest_target_timestamp = get_newest_modification_time(args['target_dependencies']) generated_files = [] + type_hashes_provided = 'type_hash_tuples' in args + type_hash_files = {} + for hash_tuple in args.get('type_hash_tuples', []): + hash_parts = hash_tuple.split(':', 1) + assert len(hash_parts) == 2 + type_hash_files[hash_parts[0]] = hash_parts[1] + for idl_tuple in args.get('idl_tuples', []): idl_parts = idl_tuple.rsplit(':', 1) assert len(idl_parts) == 2 locator = IdlLocator(*idl_parts) idl_rel_path = pathlib.Path(idl_parts[1]) + + if type_hashes_provided: + type_hash_file = type_hash_files[idl_parts[1]] + with open(type_hash_file, 'r') as f: + type_hash_infos = json.load(f)['hashes'] + else: + type_hash_infos = None + idl_stem = idl_rel_path.stem if not keep_case: idl_stem = convert_camel_case_to_lower_case_underscore(idl_stem) @@ -81,6 +96,7 @@ def generate_files( 'package_name': args['package_name'], 'interface_path': idl_rel_path, 'content': idl_file.content, + 'type_hash': type_hash_infos, } if additional_context is not None: data.update(additional_context) diff --git a/rosidl_runtime_c/CMakeLists.txt b/rosidl_runtime_c/CMakeLists.txt index 33baf9b25..5da904f1c 100644 --- a/rosidl_runtime_c/CMakeLists.txt +++ b/rosidl_runtime_c/CMakeLists.txt @@ -21,6 +21,7 @@ add_library(${PROJECT_NAME} "src/sequence_bound.c" "src/service_type_support.c" "src/string_functions.c" + "src/type_hash.c" "src/u16string_functions.c" ) target_include_directories(${PROJECT_NAME} PUBLIC @@ -98,6 +99,11 @@ if(BUILD_TESTING) target_compile_definitions(test_string_functions PUBLIC RCUTILS_ENABLE_FAULT_INJECTION) endif() + ament_add_gtest(test_type_hash test/test_type_hash.cpp) + if(TARGET test_type_hash) + target_link_libraries(test_type_hash ${PROJECT_NAME}) + endif() + ament_add_gtest(test_u16string_functions test/test_u16string_functions.cpp) if(TARGET test_u16string_functions) target_link_libraries(test_u16string_functions ${PROJECT_NAME}) diff --git a/rosidl_runtime_c/include/rosidl_runtime_c/type_hash.h b/rosidl_runtime_c/include/rosidl_runtime_c/type_hash.h new file mode 100644 index 000000000..dfe4c516e --- /dev/null +++ b/rosidl_runtime_c/include/rosidl_runtime_c/type_hash.h @@ -0,0 +1,86 @@ +// Copyright 2023 Open Source Robotics Foundation, Inc. +// +// 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_C__TYPE_HASH_H_ +#define ROSIDL_RUNTIME_C__TYPE_HASH_H_ + +#include + +#include "rcutils/allocator.h" +#include "rcutils/sha256.h" + +#include "rosidl_runtime_c/visibility_control.h" + +#define ROSIDL_TYPE_HASH_VERSION_UNSET 0 +#define ROSIDL_TYPE_HASH_SIZE RCUTILS_SHA256_BLOCK_SIZE + +#ifdef __cplusplus +extern "C" +{ +#endif + +/// A ROS 2 interface type hash per REP-2011 RIHS standard. +typedef struct rosidl_type_hash_s +{ + uint8_t version; + uint8_t value[ROSIDL_TYPE_HASH_SIZE]; +} rosidl_type_hash_t; + +/// Get a new zero-initialized type hash structure. +/** + * Note that the version equals ROSIDL_TYPE_HASH_VERSION_UNSET. + */ +ROSIDL_GENERATOR_C_PUBLIC +rosidl_type_hash_t +rosidl_get_zero_initialized_type_hash(void); + +/// Convert type hash to a standardized string representation. +/** + * Follows format RIHS{version}_{value}. + * + * \param[in] type_hash Type hash to convert to string + * \param[in] allocator Allocator to use for allocating string space + * \param[out] output_string Handle to a pointer that will be set + * to the newly allocated null-terminated string representation. + * \return RCUTILS_RET_INVALID_ARGUMENT if any pointer arguments are null or allocator invalid + * \return RCUTILS_RET_BAD_ALLOC if space could not be allocated for resulting string + * \return RCUTILS_RET_OK otherwise + */ +ROSIDL_GENERATOR_C_PUBLIC +rcutils_ret_t +rosidl_stringify_type_hash( + const rosidl_type_hash_t * type_hash, + rcutils_allocator_t allocator, + char ** output_string); + +/// Parse a stringified type hash to a struct. +/** + * \param[in] type_hash_string Null-terminated string with the hash representation + * \param[out] hash_out Preallocated structure to be filled with parsed hash information. + * hash_out->version will be 0 if no version could be parsed, + * but if a version could be determined this field will be set even if an error is returned + * \return RCTUILS_RET_INVALID_ARGUMENT on any null pointer argumunts, or malformed hash string. + * \return RCUTILS_RET_OK otherwise + */ +ROSIDL_GENERATOR_C_PUBLIC +rcutils_ret_t +rosidl_parse_type_hash_string( + const char * type_hash_string, + rosidl_type_hash_t * hash_out); + +#ifdef __cplusplus +} +#endif + +#endif // ROSIDL_RUNTIME_C__TYPE_HASH_H_ diff --git a/rosidl_runtime_c/src/type_hash.c b/rosidl_runtime_c/src/type_hash.c new file mode 100644 index 000000000..ce13112ec --- /dev/null +++ b/rosidl_runtime_c/src/type_hash.c @@ -0,0 +1,151 @@ +// Copyright 2023 Open Source Robotics Foundation, Inc. +// +// 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. + +#include + +#include "rosidl_runtime_c/type_hash.h" + +#include "rcutils/error_handling.h" + +static const char RIHS01_PREFIX[] = "RIHS01_"; +// Hash representation is hex string, two characters per byte +static const size_t RIHS_VERSION_IDX = 4; +static const size_t RIHS_PREFIX_LEN = 7; +static const size_t RIHS01_STRING_LEN = 71; // RIHS_PREFIX_LEN + (ROSIDL_TYPE_HASH_SIZE * 2); +static const uint8_t INVALID_NIBBLE = 0xff; + +/// Translate a single character hex digit to a nibble +static uint8_t _xatoi(char c) +{ + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return c - 'A' + 0xa; + } + if (c >= 'a' && c <= 'f') { + return c - 'a' + 0xa; + } + return INVALID_NIBBLE; +} + +rosidl_type_hash_t +rosidl_get_zero_initialized_type_hash(void) +{ + rosidl_type_hash_t zero_initialized_type_hash = {0}; + return zero_initialized_type_hash; +} + +rcutils_ret_t +rosidl_stringify_type_hash( + const rosidl_type_hash_t * type_hash, + rcutils_allocator_t allocator, + char ** output_string) +{ + RCUTILS_CHECK_ARGUMENT_FOR_NULL(type_hash, RCUTILS_RET_INVALID_ARGUMENT); + if (!rcutils_allocator_is_valid(&allocator)) { + RCUTILS_SET_ERROR_MSG("Invalid allocator"); + return RCUTILS_RET_INVALID_ARGUMENT; + } + RCUTILS_CHECK_ARGUMENT_FOR_NULL(output_string, RCUTILS_RET_INVALID_ARGUMENT); + + char * local_output = allocator.allocate(RIHS01_STRING_LEN + 1, allocator.state); + if (!local_output) { + *output_string = NULL; + RCUTILS_SET_ERROR_MSG("Unable to allocate space for type hash string."); + return RCUTILS_RET_BAD_ALLOC; + } + local_output[RIHS01_STRING_LEN] = '\0'; + memcpy(local_output, RIHS01_PREFIX, RIHS_PREFIX_LEN); + + uint8_t nibble = 0; + char * dest = NULL; + for (size_t i = 0; i < ROSIDL_TYPE_HASH_SIZE; i++) { + // Translate byte into two hex characters + dest = local_output + RIHS_PREFIX_LEN + (i * 2); + // First character is top half of byte + nibble = (type_hash->value[i] >> 4) & 0x0f; + if (nibble < 0xa) { + dest[0] = '0' + nibble; + } else { + dest[0] = 'a' + (nibble - 0xa); + } + // Second character is bottom half of byte + nibble = (type_hash->value[i] >> 0) & 0x0f; + if (nibble < 0xa) { + dest[1] = '0' + nibble; + } else { + dest[1] = 'a' + (nibble - 0xa); + } + } + + *output_string = local_output; + return RCUTILS_RET_OK; +} + +rcutils_ret_t +rosidl_parse_type_hash_string( + const char * type_hash_string, + rosidl_type_hash_t * hash_out) +{ + RCUTILS_CHECK_ARGUMENT_FOR_NULL(type_hash_string, RCUTILS_RET_INVALID_ARGUMENT); + RCUTILS_CHECK_ARGUMENT_FOR_NULL(hash_out, RCUTILS_RET_INVALID_ARGUMENT); + hash_out->version = 0; + size_t input_len = strlen(type_hash_string); + uint8_t hexbyte_top_nibble; + uint8_t hexbyte_bot_nibble; + + // Check prefix + if (input_len < RIHS_PREFIX_LEN) { + RCUTILS_SET_ERROR_MSG("Hash string not long enough to contain RIHS prefix."); + return RCUTILS_RET_INVALID_ARGUMENT; + } + if (0 != strncmp(type_hash_string, RIHS01_PREFIX, RIHS_VERSION_IDX)) { + RCUTILS_SET_ERROR_MSG("Hash string doesn't start with RIHS."); + return RCUTILS_RET_INVALID_ARGUMENT; + } + + // Parse version + hexbyte_top_nibble = _xatoi(type_hash_string[RIHS_VERSION_IDX]); + hexbyte_bot_nibble = _xatoi(type_hash_string[RIHS_VERSION_IDX + 1]); + if (hexbyte_top_nibble == INVALID_NIBBLE || hexbyte_bot_nibble == INVALID_NIBBLE) { + RCUTILS_SET_ERROR_MSG("RIHS version is not a 2-digit hex string."); + return RCUTILS_RET_INVALID_ARGUMENT; + } + hash_out->version = (hexbyte_top_nibble << 4) + hexbyte_bot_nibble; + + if (hash_out->version != 1) { + RCUTILS_SET_ERROR_MSG("Do not know how to parse RIHS version."); + return RCUTILS_RET_INVALID_ARGUMENT; + } + + // Check total length + if (input_len != RIHS01_STRING_LEN) { + RCUTILS_SET_ERROR_MSG("RIHS string is the incorrect size to contain a RIHS01 value."); + return RCUTILS_RET_INVALID_ARGUMENT; + } + + // Parse hash value + const char * value_str = type_hash_string + RIHS_PREFIX_LEN; + for (size_t i = 0; i < ROSIDL_TYPE_HASH_SIZE; i++) { + hexbyte_top_nibble = _xatoi(value_str[i * 2]); + hexbyte_bot_nibble = _xatoi(value_str[i * 2 + 1]); + if (hexbyte_top_nibble == INVALID_NIBBLE || hexbyte_bot_nibble == INVALID_NIBBLE) { + RCUTILS_SET_ERROR_MSG("Type hash string value contained non-hexdigit character."); + return RCUTILS_RET_INVALID_ARGUMENT; + } + hash_out->value[i] = (hexbyte_top_nibble << 4) + hexbyte_bot_nibble; + } + return RCUTILS_RET_OK; +} diff --git a/rosidl_runtime_c/test/test_type_hash.cpp b/rosidl_runtime_c/test/test_type_hash.cpp new file mode 100644 index 000000000..8183ea41d --- /dev/null +++ b/rosidl_runtime_c/test/test_type_hash.cpp @@ -0,0 +1,106 @@ +// Copyright 2023 Open Source Robotics Foundation, Inc. +// +// 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. + +#include "gtest/gtest.h" + +#include "rcutils/error_handling.h" +#include "rosidl_runtime_c/type_hash.h" + +TEST(type_hash, init_zero_hash) { + auto hash = rosidl_get_zero_initialized_type_hash(); + EXPECT_EQ(hash.version, 0); + for (size_t i = 0; i < sizeof(hash.value); i++) { + EXPECT_EQ(hash.value[i], 0); + } +} + +TEST(type_hash, stringify_basic) { + const std::string expected = + "RIHS01_000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; + rosidl_type_hash_t hash = rosidl_get_zero_initialized_type_hash(); + hash.version = 1; + for (uint8_t i = 0; i < ROSIDL_TYPE_HASH_SIZE; i++) { + hash.value[i] = i; + } + auto allocator = rcutils_get_default_allocator(); + char * hash_string = nullptr; + ASSERT_EQ(RCUTILS_RET_OK, rosidl_stringify_type_hash(&hash, allocator, &hash_string)); + ASSERT_TRUE(hash_string); + + std::string cpp_str(hash_string); + EXPECT_EQ(expected, hash_string); +} + +TEST(type_hash, parse_basic) { + const std::string test_value = + "RIHS01_000102030405060708090a0b0c0d0e0f101112131415161718191a1b1c1d1e1f"; + + rosidl_type_hash_t hash = rosidl_get_zero_initialized_type_hash(); + ASSERT_EQ(RCUTILS_RET_OK, rosidl_parse_type_hash_string(test_value.c_str(), &hash)); + EXPECT_EQ(1, hash.version); + for (size_t i = 0; i < sizeof(hash.value); i++) { + size_t expected_value = i; + EXPECT_EQ(expected_value, hash.value[i]) << "At byte " << i; + } +} + +TEST(type_hash, parse_bad_prefix) { + const std::string test_value = + "RRRR01_00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; + rosidl_type_hash_t hash = rosidl_get_zero_initialized_type_hash(); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, rosidl_parse_type_hash_string(test_value.c_str(), &hash)); + rcutils_reset_error(); +} + +TEST(type_hash, parse_no_version) { + const std::string test_value = + "RIHS_00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; + rosidl_type_hash_t hash = rosidl_get_zero_initialized_type_hash(); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, rosidl_parse_type_hash_string(test_value.c_str(), &hash)); + rcutils_reset_error(); +} + +TEST(type_hash, parse_too_short) { + const std::string test_value = + "RIHS01_00112233445566778899aabbccddeeff00112233445566778899aabbccddee"; + rosidl_type_hash_t hash = rosidl_get_zero_initialized_type_hash(); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, rosidl_parse_type_hash_string(test_value.c_str(), &hash)); + rcutils_reset_error(); +} + +TEST(type_hash, parse_too_long) { + const std::string test_value = + "RIHS01_00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff00"; + rosidl_type_hash_t hash = rosidl_get_zero_initialized_type_hash(); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, rosidl_parse_type_hash_string(test_value.c_str(), &hash)); + rcutils_reset_error(); +} + +TEST(type_hash, parse_bad_version) { + const std::string test_value = + "RIHS02_00112233445566778899aabbccddeeff00112233445566778899aabbccddeeff"; + rosidl_type_hash_t hash = rosidl_get_zero_initialized_type_hash(); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, rosidl_parse_type_hash_string(test_value.c_str(), &hash)); + EXPECT_EQ(hash.version, 2); + rcutils_reset_error(); +} + +TEST(type_hash, parse_bad_value) { + const std::string test_value = + "RIHS01_00112233445566778899aabbccddgeff00112233445566778899aabbccddeeff"; + rosidl_type_hash_t hash = rosidl_get_zero_initialized_type_hash(); + EXPECT_EQ(RCUTILS_RET_INVALID_ARGUMENT, rosidl_parse_type_hash_string(test_value.c_str(), &hash)); + EXPECT_EQ(hash.version, 1); + rcutils_reset_error(); +} diff --git a/rosidl_typesupport_introspection_c/include/rosidl_typesupport_introspection_c/message_introspection.h b/rosidl_typesupport_introspection_c/include/rosidl_typesupport_introspection_c/message_introspection.h index d5d220e08..12fc4811a 100644 --- a/rosidl_typesupport_introspection_c/include/rosidl_typesupport_introspection_c/message_introspection.h +++ b/rosidl_typesupport_introspection_c/include/rosidl_typesupport_introspection_c/message_introspection.h @@ -21,6 +21,7 @@ #include "rosidl_runtime_c/message_initialization.h" #include "rosidl_runtime_c/message_type_support_struct.h" +#include "rosidl_runtime_c/type_hash.h" #include "rosidl_typesupport_introspection_c/visibility_control.h" @@ -85,6 +86,8 @@ typedef struct rosidl_typesupport_introspection_c__MessageMembers_s const char * message_namespace_; /// The name of the interface, e.g. "Int16" const char * message_name_; + /// Hashed value of the interface description + const rosidl_type_hash_t type_hash_; /// The number of fields in the interface uint32_t member_count_; /// The size of the interface structure in memory diff --git a/rosidl_typesupport_introspection_c/resource/msg__type_support.c.em b/rosidl_typesupport_introspection_c/resource/msg__type_support.c.em index f0004338a..1e8beda4f 100644 --- a/rosidl_typesupport_introspection_c/resource/msg__type_support.c.em +++ b/rosidl_typesupport_introspection_c/resource/msg__type_support.c.em @@ -270,6 +270,7 @@ for index, member in enumerate(message.structure.members): static const rosidl_typesupport_introspection_c__MessageMembers @(function_prefix)__@(message.structure.namespaced_type.name)_message_members = { "@('__'.join([package_name] + list(interface_path.parents[0].parts)))", // message namespace "@(message.structure.namespaced_type.name)", // message name + @('__'.join(message.structure.namespaced_type.namespaced_name()))__TYPE_VERSION_HASH__INIT, @(len(message.structure.members)), // number of fields sizeof(@('__'.join([package_name] + list(interface_path.parents[0].parts) + [message.structure.namespaced_type.name]))), @(function_prefix)__@(message.structure.namespaced_type.name)_message_member_array, // message members diff --git a/rosidl_typesupport_introspection_cpp/include/rosidl_typesupport_introspection_cpp/message_introspection.hpp b/rosidl_typesupport_introspection_cpp/include/rosidl_typesupport_introspection_cpp/message_introspection.hpp index 2340f2c8c..cc60501f8 100644 --- a/rosidl_typesupport_introspection_cpp/include/rosidl_typesupport_introspection_cpp/message_introspection.hpp +++ b/rosidl_typesupport_introspection_cpp/include/rosidl_typesupport_introspection_cpp/message_introspection.hpp @@ -19,6 +19,7 @@ #include #include "rosidl_runtime_c/message_type_support_struct.h" +#include "rosidl_runtime_c/type_hash.h" #include "rosidl_runtime_cpp/message_initialization.hpp" @@ -92,6 +93,8 @@ typedef struct ROSIDL_TYPESUPPORT_INTROSPECTION_CPP_PUBLIC MessageMembers_s const char * message_namespace_; /// The name of the interface, e.g. "Int16" const char * message_name_; + /// Hashed value of the interface description + const rosidl_type_hash_t type_hash_; /// The number of fields in the interface uint32_t member_count_; /// The size of the interface structure in memory diff --git a/rosidl_typesupport_introspection_cpp/resource/msg__type_support.cpp.em b/rosidl_typesupport_introspection_cpp/resource/msg__type_support.cpp.em index 2ebcc9274..7c1cc636b 100644 --- a/rosidl_typesupport_introspection_cpp/resource/msg__type_support.cpp.em +++ b/rosidl_typesupport_introspection_cpp/resource/msg__type_support.cpp.em @@ -235,6 +235,7 @@ for index, member in enumerate(message.structure.members): static const ::rosidl_typesupport_introspection_cpp::MessageMembers @(message.structure.namespaced_type.name)_message_members = { "@('::'.join([package_name] + list(interface_path.parents[0].parts)))", // message namespace "@(message.structure.namespaced_type.name)", // message name + @('::'.join(message.structure.namespaced_type.namespaced_name()))::TYPE_VERSION_HASH, @(len(message.structure.members)), // number of fields sizeof(@('::'.join([package_name] + list(interface_path.parents[0].parts) + [message.structure.namespaced_type.name]))), @(message.structure.namespaced_type.name)_message_member_array, // message members