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