From 2aa60cee683bd094ad4292239381dbcd8a4a2ad6 Mon Sep 17 00:00:00 2001 From: Stefan Hoffmann Date: Tue, 21 Jun 2022 13:48:39 +0200 Subject: [PATCH] actions: extend idl generation to include interfaces for "action wrapper types" With "action wrapper types" this means the ROS messages and services that add the `GoalId`, `TimeStamp` or other fields on top of the user defined types for the action. The introduced methods in `IRosActionDefinition` are needed to work around the missing existential type support in C# (https://github.com/dotnet/csharplang/issues/5556). --- rcldotnet_common/CMakeLists.txt | 5 ++ rcldotnet_common/IRosActionDefinition.cs | 15 ++++ rcldotnet_common/IRosActionFeedbackMessage.cs | 26 ++++++ .../IRosActionGetResultRequest.cs | 23 +++++ .../IRosActionGetResultResponse.cs | 25 ++++++ rcldotnet_common/IRosActionSendGoalRequest.cs | 42 +++++++++ .../IRosActionSendGoalResponse.cs | 25 ++++++ rosidl_generator_dotnet/resource/action.cs.em | 90 ++++++++++++++++++- rosidl_generator_dotnet/resource/idl.cs.em | 23 +++-- rosidl_generator_dotnet/resource/msg.cs.em | 14 ++- rosidl_generator_dotnet/resource/srv.cs.em | 11 ++- .../rosidl_generator_dotnet/__init__.py | 5 ++ 12 files changed, 288 insertions(+), 16 deletions(-) create mode 100644 rcldotnet_common/IRosActionFeedbackMessage.cs create mode 100644 rcldotnet_common/IRosActionGetResultRequest.cs create mode 100644 rcldotnet_common/IRosActionGetResultResponse.cs create mode 100644 rcldotnet_common/IRosActionSendGoalRequest.cs create mode 100644 rcldotnet_common/IRosActionSendGoalResponse.cs diff --git a/rcldotnet_common/CMakeLists.txt b/rcldotnet_common/CMakeLists.txt index e97e99b2..eb2f4387 100644 --- a/rcldotnet_common/CMakeLists.txt +++ b/rcldotnet_common/CMakeLists.txt @@ -11,6 +11,11 @@ find_package(DotNETExtra REQUIRED) set(CS_SOURCES DllLoadUtils.cs IRosActionDefinition.cs + IRosActionFeedbackMessage.cs + IRosActionGetResultRequest.cs + IRosActionGetResultResponse.cs + IRosActionSendGoalRequest.cs + IRosActionSendGoalResponse.cs IRosMessage.cs IRosServiceDefinition.cs ) diff --git a/rcldotnet_common/IRosActionDefinition.cs b/rcldotnet_common/IRosActionDefinition.cs index 7fcf0104..470df3db 100644 --- a/rcldotnet_common/IRosActionDefinition.cs +++ b/rcldotnet_common/IRosActionDefinition.cs @@ -23,5 +23,20 @@ public interface IRosActionDefinition // must be implemented on deriving types, gets called via reflection // (static abstract interface members are not supported yet.) // public static abstract IntPtr __GetTypeSupport(); + + // public static abstract IRosActionSendGoalRequest __CreateSendGoalRequest(); + // public static abstract SafeHandle __CreateSendGoalRequestHandle(); + + // public static abstract IRosActionSendGoalResponse __CreateSendGoalResponse(); + // public static abstract SafeHandle __CreateSendGoalResponseHandle(); + + // public static abstract IRosActionGetResultRequest __CreateGetResultRequest(); + // public static abstract SafeHandle __CreateGetResultRequestHandle(); + + // public static abstract IRosActionGetResultResponse __CreateGetResultResponse(); + // public static abstract SafeHandle __CreateGetResultResponseHandle(); + + // public static abstract IRosActionFeedbackMessage __CreateFeedbackMessage(); + // public static abstract SafeHandle __CreateFeedbackMessageHandle(); } } diff --git a/rcldotnet_common/IRosActionFeedbackMessage.cs b/rcldotnet_common/IRosActionFeedbackMessage.cs new file mode 100644 index 00000000..819b11cf --- /dev/null +++ b/rcldotnet_common/IRosActionFeedbackMessage.cs @@ -0,0 +1,26 @@ +/* Copyright 2022 Stefan Hoffmann + * + * 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. + */ + +namespace ROS2 +{ + public interface IRosActionFeedbackMessage : IRosMessage + where TFeedback : IRosMessage, new() + { + // NOTICE: cyclic reference, see `IRosActionSendGoalRequest` + // unique_identifier_msgs.msg.UUID GoalId { get; set; } + + TFeedback Feedback { get; set; } + } +} diff --git a/rcldotnet_common/IRosActionGetResultRequest.cs b/rcldotnet_common/IRosActionGetResultRequest.cs new file mode 100644 index 00000000..25425a07 --- /dev/null +++ b/rcldotnet_common/IRosActionGetResultRequest.cs @@ -0,0 +1,23 @@ +/* Copyright 2022 Stefan Hoffmann + * + * 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. + */ + +namespace ROS2 +{ + public interface IRosActionGetResultRequest : IRosMessage + { + // NOTICE: cyclic reference, see `IRosActionSendGoalRequest` + // unique_identifier_msgs.msg.UUID GoalId { get; set; } + } +} diff --git a/rcldotnet_common/IRosActionGetResultResponse.cs b/rcldotnet_common/IRosActionGetResultResponse.cs new file mode 100644 index 00000000..283ab298 --- /dev/null +++ b/rcldotnet_common/IRosActionGetResultResponse.cs @@ -0,0 +1,25 @@ +/* Copyright 2022 Stefan Hoffmann + * + * 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. + */ + +namespace ROS2 +{ + public interface IRosActionGetResultResponse : IRosMessage + where TResult : IRosMessage, new() + { + sbyte Status { get; set; } + + TResult Result { get; set; } + } +} diff --git a/rcldotnet_common/IRosActionSendGoalRequest.cs b/rcldotnet_common/IRosActionSendGoalRequest.cs new file mode 100644 index 00000000..0bd87079 --- /dev/null +++ b/rcldotnet_common/IRosActionSendGoalRequest.cs @@ -0,0 +1,42 @@ +/* Copyright 2022 Stefan Hoffmann + * + * 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. + */ + +namespace ROS2 +{ + public interface IRosActionSendGoalRequest : IRosMessage + where TGoal : IRosMessage, new() + { + // NOTICE: This would cause a cyclic reference: + // + // - `unique_identifier_msgs.msg.UUID` in the `unique_identifier_msgs` + // assembly references `IRosMessage` in the `rcldotnet_common` + // assembly. + // - `IRosActionSendGoalRequest` in the `rcldotnet_common` + // assembly references `unique_identifier_msgs.msg.UUID` in the + // `unique_identifier_msgs` assembly. + // + // So we need a workaround: + // - Use reflection later on to get to this. + // - Or use types like `byte[]` (or ValueTuple for + // `builtin_interfaces.msg.Time`) and generate accessor methods that + // convert and use those types. + // - Or provide property `IRosMessage GoalIdRosMessage { get; set; }` + // and cast to the concrete type on usage. + // + // unique_identifier_msgs.msg.UUID GoalId { get; set; } + + TGoal Goal { get; set; } + } +} diff --git a/rcldotnet_common/IRosActionSendGoalResponse.cs b/rcldotnet_common/IRosActionSendGoalResponse.cs new file mode 100644 index 00000000..535d14b3 --- /dev/null +++ b/rcldotnet_common/IRosActionSendGoalResponse.cs @@ -0,0 +1,25 @@ +/* Copyright 2022 Stefan Hoffmann + * + * 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. + */ + +namespace ROS2 +{ + public interface IRosActionSendGoalResponse : IRosMessage + { + bool Accepted { get; set; } + + // NOTICE: cyclic reference, see `IRosActionSendGoalRequest` + // builtin_interfaces.msg.Time Stamp { get; set; } + } +} diff --git a/rosidl_generator_dotnet/resource/action.cs.em b/rosidl_generator_dotnet/resource/action.cs.em index 7c930458..36bdb2bf 100644 --- a/rosidl_generator_dotnet/resource/action.cs.em +++ b/rosidl_generator_dotnet/resource/action.cs.em @@ -1,6 +1,7 @@ @{ from rosidl_generator_dotnet import get_field_name from rosidl_generator_dotnet import get_dotnet_type +from rosidl_generator_dotnet import get_dotnet_type_for_message from rosidl_generator_dotnet import get_builtin_dotnet_type from rosidl_generator_dotnet import constant_value_to_dotnet @@ -14,9 +15,17 @@ from rosidl_parser.definition import BasicType from rosidl_parser.definition import NamespacedType type_name = action.namespaced_type.name -goal_type_name = action.goal.structure.namespaced_type.name -result_type_name = action.result.structure.namespaced_type.name -feedback_type_name = action.feedback.structure.namespaced_type.name + +goal_type_name = get_dotnet_type_for_message(action.goal) +result_type_name = get_dotnet_type_for_message(action.result) +feedback_type_name = get_dotnet_type_for_message(action.feedback) + +send_goal_request_type_name = get_dotnet_type_for_message(action.send_goal_service.request_message) +send_goal_response_type_name = get_dotnet_type_for_message(action.send_goal_service.response_message) +get_result_request_type_name = get_dotnet_type_for_message(action.get_result_service.request_message) +get_result_response_type_name = get_dotnet_type_for_message(action.get_result_service.response_message) +feedback_message_type_name = get_dotnet_type_for_message(action.feedback_message) + action_typename = '%s__%s' % ('__'.join(action.namespaced_type.namespaces), type_name) } namespace @('.'.join(action.namespaced_type.namespaces)) @@ -28,7 +37,10 @@ namespace @('.'.join(action.namespaced_type.namespaces)) @# static abstract interface members are currently in preview, so maybe we could use the feature in the future. @# (if hey add support to derive from static only interfaces in static classes) @# Another option is to not use generics for passing the typesupport, but lets try this until we hit some wall. - public sealed class @(type_name) : global::ROS2.IRosActionDefinition<@(goal_type_name), @(result_type_name), @(feedback_type_name)> + public sealed class @(type_name) : global::ROS2.IRosActionDefinition< + global::@(goal_type_name), + global::@(result_type_name), + global::@(feedback_type_name)> { private static readonly DllLoadUtils dllLoadUtils; @@ -57,5 +69,75 @@ namespace @('.'.join(action.namespaced_type.namespaces)) public static IntPtr __GetTypeSupport() { return native_get_typesupport(); } + +@# This method gets called via reflection as static abstract interface members are not supported yet. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::ROS2.IRosActionSendGoalRequest __CreateSendGoalRequest() + { + return new global::@(send_goal_request_type_name)(); + } + +@# This method gets called via reflection as static abstract interface members are not supported yet. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static SafeHandle __CreateSendGoalRequestHandle() + { + return global::@(send_goal_request_type_name).__CreateMessageHandle(); + } + +@# This method gets called via reflection as static abstract interface members are not supported yet. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::ROS2.IRosActionSendGoalResponse __CreateSendGoalResponse() + { + return new global::@(send_goal_response_type_name)(); + } + +@# This method gets called via reflection as static abstract interface members are not supported yet. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static SafeHandle __CreateSendGoalResponseHandle() + { + return global::@(send_goal_response_type_name).__CreateMessageHandle(); + } + +@# This method gets called via reflection as static abstract interface members are not supported yet. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::ROS2.IRosActionGetResultRequest __CreateGetResultRequest() + { + return new global::@(get_result_request_type_name)(); + } + +@# This method gets called via reflection as static abstract interface members are not supported yet. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static SafeHandle __CreateGetResultRequestHandle() + { + return global::@(get_result_request_type_name).__CreateMessageHandle(); + } + +@# This method gets called via reflection as static abstract interface members are not supported yet. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::ROS2.IRosActionGetResultResponse __CreateGetResultResponse() + { + return new global::@(get_result_response_type_name)(); + } + +@# This method gets called via reflection as static abstract interface members are not supported yet. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static SafeHandle __CreateGetResultResponseHandle() + { + return global::@(get_result_response_type_name).__CreateMessageHandle(); + } + +@# This method gets called via reflection as static abstract interface members are not supported yet. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static global::ROS2.IRosActionFeedbackMessage __CreateFeedbackMessage() + { + return new global::@(feedback_message_type_name)(); + } + +@# This method gets called via reflection as static abstract interface members are not supported yet. + [global::System.ComponentModel.EditorBrowsable(global::System.ComponentModel.EditorBrowsableState.Never)] + public static SafeHandle __CreateFeedbackMessageHandle() + { + return global::@(feedback_message_type_name).__CreateMessageHandle(); + } } } diff --git a/rosidl_generator_dotnet/resource/idl.cs.em b/rosidl_generator_dotnet/resource/idl.cs.em index 1f18e48d..18a9b314 100644 --- a/rosidl_generator_dotnet/resource/idl.cs.em +++ b/rosidl_generator_dotnet/resource/idl.cs.em @@ -1,6 +1,9 @@ // generated from rosidl_generator_dotnet/resource/idl.cs.em // with input from @(package_name):@(interface_path) // generated code does not contain a copyright notice +@{ +from rosidl_generator_dotnet import get_dotnet_type_for_message +}@ @ @####################################################################### @# EmPy template for generating .cs files @@ -87,13 +90,17 @@ TEMPLATE( @{ TEMPLATE( 'msg.cs.em', - package_name=package_name, interface_path=interface_path, message=action.send_goal_service.request_message) + package_name=package_name, interface_path=interface_path, message=action.send_goal_service.request_message, + action_interface='global::ROS2.IRosActionSendGoalRequest' % get_dotnet_type_for_message(action.goal) +) }@ @{ TEMPLATE( 'msg.cs.em', - package_name=package_name, interface_path=interface_path, message=action.send_goal_service.response_message) + package_name=package_name, interface_path=interface_path, message=action.send_goal_service.response_message, + action_interface='global::ROS2.IRosActionSendGoalResponse' +) }@ @{ @@ -105,13 +112,17 @@ TEMPLATE( @{ TEMPLATE( 'msg.cs.em', - package_name=package_name, interface_path=interface_path, message=action.get_result_service.request_message) + package_name=package_name, interface_path=interface_path, message=action.get_result_service.request_message, + action_interface='global::ROS2.IRosActionGetResultRequest' +) }@ @{ TEMPLATE( 'msg.cs.em', - package_name=package_name, interface_path=interface_path, message=action.get_result_service.response_message) + package_name=package_name, interface_path=interface_path, message=action.get_result_service.response_message, + action_interface='global::ROS2.IRosActionGetResultResponse' % get_dotnet_type_for_message(action.result) +) }@ @{ @@ -123,7 +134,9 @@ TEMPLATE( @{ TEMPLATE( 'msg.cs.em', - package_name=package_name, interface_path=interface_path, message=action.feedback_message) + package_name=package_name, interface_path=interface_path, message=action.feedback_message, + action_interface='global::ROS2.IRosActionFeedbackMessage' % get_dotnet_type_for_message(action.feedback) +) }@ @{ diff --git a/rosidl_generator_dotnet/resource/msg.cs.em b/rosidl_generator_dotnet/resource/msg.cs.em index 69350cde..d07c9c43 100644 --- a/rosidl_generator_dotnet/resource/msg.cs.em +++ b/rosidl_generator_dotnet/resource/msg.cs.em @@ -17,11 +17,17 @@ from rosidl_parser.definition import NamespacedType type_name = message.structure.namespaced_type.name msg_typename = '%s__%s' % ('__'.join(message.structure.namespaced_type.namespaces), type_name) + +if 'action_interface' not in locals(): + action_interface = None + +additional_interfaces_str = ', ' + action_interface if action_interface is not None else '' + } namespace @('.'.join(message.structure.namespaced_type.namespaces)) { -public class @(type_name) : global::ROS2.IRosMessage { +public class @(type_name) : global::ROS2.IRosMessage@(additional_interfaces_str) { private static readonly DllLoadUtils dllLoadUtils; @[for member in message.structure.members]@ @@ -277,7 +283,7 @@ public class @(type_name) : global::ROS2.IRosMessage { IntPtr pStr_@(get_field_name(type_name, member.name)) = native_read_field_@(member.name)(native_get_field_@(member.name)_message(messageHandle, i__local_variable)); @(get_field_name(type_name, member.name))[i__local_variable] = Marshal.PtrToStringAnsi(pStr_@(get_field_name(type_name, member.name))); @[ elif isinstance(member.type.value_type, AbstractWString)]@ - // TODO: Unicode types are not supported + // TODO: Unicode types are not supported @[ else]@ @(get_field_name(type_name, member.name))[i__local_variable] = new @(get_dotnet_type(member.type.value_type))(); @(get_field_name(type_name, member.name))[i__local_variable].__ReadFromHandle(native_get_field_@(member.name)_message(messageHandle, i__local_variable)); @@ -296,7 +302,7 @@ public class @(type_name) : global::ROS2.IRosMessage { IntPtr pStr_@(get_field_name(type_name, member.name)) = native_read_field_@(member.name)(native_get_field_@(member.name)_message(messageHandle, i__local_variable)); @(get_field_name(type_name, member.name)).Add(Marshal.PtrToStringAnsi(pStr_@(get_field_name(type_name, member.name)))); @[ elif isinstance(member.type.value_type, AbstractWString)]@ - // TODO: Unicode types are not supported + // TODO: Unicode types are not supported @[ else]@ @(get_field_name(type_name, member.name)).Add(new @(get_dotnet_type(member.type.value_type))()); @(get_field_name(type_name, member.name))[i__local_variable].__ReadFromHandle(native_get_field_@(member.name)_message(messageHandle, i__local_variable)); @@ -348,7 +354,7 @@ public class @(type_name) : global::ROS2.IRosMessage { @[ if isinstance(member.type.value_type, BasicType) or isinstance(member.type.value_type, AbstractString)]@ native_write_field_@(member.name)(native_get_field_@(member.name)_message(messageHandle, i__local_variable), value__local_variable); @[ elif isinstance(member.type.value_type, AbstractWString)] -// TODO: Unicode types are not supported +// TODO: Unicode types are not supported @[ else]@ value__local_variable.__WriteToHandle(native_get_field_@(member.name)_message(messageHandle, i__local_variable)); @[ end if]@ diff --git a/rosidl_generator_dotnet/resource/srv.cs.em b/rosidl_generator_dotnet/resource/srv.cs.em index bd541280..8231e40f 100644 --- a/rosidl_generator_dotnet/resource/srv.cs.em +++ b/rosidl_generator_dotnet/resource/srv.cs.em @@ -1,6 +1,7 @@ @{ from rosidl_generator_dotnet import get_field_name from rosidl_generator_dotnet import get_dotnet_type +from rosidl_generator_dotnet import get_dotnet_type_for_message from rosidl_generator_dotnet import get_builtin_dotnet_type from rosidl_generator_dotnet import constant_value_to_dotnet @@ -14,8 +15,10 @@ from rosidl_parser.definition import BasicType from rosidl_parser.definition import NamespacedType type_name = service.namespaced_type.name -request_type_name = service.request_message.structure.namespaced_type.name -response_type_name = service.response_message.structure.namespaced_type.name + +request_type_name = get_dotnet_type_for_message(service.request_message) +response_type_name = get_dotnet_type_for_message(service.response_message) + srv_typename = '%s__%s' % ('__'.join(service.namespaced_type.namespaces), type_name) } namespace @('.'.join(service.namespaced_type.namespaces)) @@ -27,7 +30,9 @@ namespace @('.'.join(service.namespaced_type.namespaces)) @# static abstract interface members are currently in preview, so maybe we could use the feature in the future. @# (if hey add support to derive from static only interfaces in static classes) @# Another option is to not use generics for passing the typesupport, but lets try this until we hit some wall. - public sealed class @(type_name) : global::ROS2.IRosServiceDefinition<@(request_type_name), @(response_type_name)> + public sealed class @(type_name) : global::ROS2.IRosServiceDefinition< + global::@(request_type_name), + global::@(response_type_name)> { private static readonly DllLoadUtils dllLoadUtils; diff --git a/rosidl_generator_dotnet/rosidl_generator_dotnet/__init__.py b/rosidl_generator_dotnet/rosidl_generator_dotnet/__init__.py index 95310ecc..39b67272 100644 --- a/rosidl_generator_dotnet/rosidl_generator_dotnet/__init__.py +++ b/rosidl_generator_dotnet/rosidl_generator_dotnet/__init__.py @@ -142,6 +142,11 @@ def get_dotnet_type(type_, use_primitives=True): return get_builtin_dotnet_type(type_.typename, use_primitives=use_primitives) +def get_dotnet_type_for_message(message): + return '%s.%s' % ( + '.'.join(message.structure.namespaced_type.namespaces), + message.structure.namespaced_type.name) + def msg_type_to_c(type_): if isinstance(type_, AbstractString): return 'char *'