From e3b0cd7c2a8f1d6ed3c8b29d47d880c8982b796e Mon Sep 17 00:00:00 2001 From: David Yackzan <dwyackzan@gmail.com> Date: Mon, 23 Sep 2024 19:04:34 -0700 Subject: [PATCH 1/5] Support vector<Any> -> vector<typename T::value_type> conversion Don't check port type alignment for vector<Any> --- include/behaviortree_cpp/tree_node.h | 26 ++++++++++++++++++++++++++ src/xml_parsing.cpp | 4 +++- 2 files changed, 29 insertions(+), 1 deletion(-) diff --git a/include/behaviortree_cpp/tree_node.h b/include/behaviortree_cpp/tree_node.h index 537176519..1d36d3fd1 100644 --- a/include/behaviortree_cpp/tree_node.h +++ b/include/behaviortree_cpp/tree_node.h @@ -30,6 +30,12 @@ namespace BT { +// Helper trait to check if a type is a std::vector +template <typename T> +struct is_vector : std::false_type {}; + +template <typename T, typename A> +struct is_vector<std::vector<T, A>> : std::true_type {}; /// This information is used mostly by the XMLParser. struct TreeNodeManifest @@ -521,6 +527,26 @@ inline Expected<Timestamp> TreeNode::getInputStamped(const std::string& key, if(!entry->value.empty()) { + // Support vector<Any> -> vector<typename T::value_type> conversion. + // Only want to compile this path when T is a vector type. + if constexpr (is_vector<T>::value) + { + if (!std::is_same_v<T, std::vector<Any>> && any_value.type() == typeid(std::vector<Any>)) + { + // If the object was originally placed on the blackboard as a vector<Any>, attempt to unwrap the vector + // elements according to the templated type. + auto any_vec = any_value.cast<std::vector<Any>>(); + if (!any_vec.empty() && any_vec.front().type() != typeid(typename T::value_type)) + { + return nonstd::make_unexpected("Invalid cast requested from vector<Any> to vector<typename T::value_type>." + " Element type does not align."); + } + destination = T(); + std::transform(any_vec.begin(), any_vec.end(), std::back_inserter(destination), + [](Any &element) { return element.cast<typename T::value_type>(); }); + return Timestamp{ entry->sequence_id, entry->stamp }; + } + } if(!std::is_same_v<T, std::string> && any_value.isString()) { destination = parseString<T>(any_value.cast<std::string>()); diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index 2e950e4d9..96ffbd8d7 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -786,8 +786,10 @@ TreeNode::Ptr XMLParser::PImpl::createNodeFromXML(const XMLElement* element, // special case related to convertFromString bool const string_input = (prev_info->type() == typeid(std::string)); + // special case related to unwrapping vector<Any> objects. + bool const vec_any_input = (prev_info->type() == typeid(std::vector<Any>)); - if(port_type_mismatch && !string_input) + if(port_type_mismatch && !string_input && !vec_any_input) { blackboard->debugMessage(); From f8493e56da135b4494a0347daed493cef16efb86 Mon Sep 17 00:00:00 2001 From: David Yackzan <dwyackzan@gmail.com> Date: Thu, 3 Oct 2024 08:48:31 -0600 Subject: [PATCH 2/5] Convert vector to vector<Any> before placing on the blackboard Also update checks to allow mismatch when a port was declared as a vector<T> and we have an input port that takes it in as a vector<Any> --- include/behaviortree_cpp/blackboard.h | 22 +++++++++++++++++++++- include/behaviortree_cpp/tree_node.h | 21 +++++++++++++-------- src/blackboard.cpp | 9 +++++++-- src/xml_parsing.cpp | 9 +++++++-- 4 files changed, 48 insertions(+), 13 deletions(-) diff --git a/include/behaviortree_cpp/blackboard.h b/include/behaviortree_cpp/blackboard.h index 1c3aa96c6..d254b58a4 100644 --- a/include/behaviortree_cpp/blackboard.h +++ b/include/behaviortree_cpp/blackboard.h @@ -4,6 +4,7 @@ #include <memory> #include <unordered_map> #include <mutex> +#include <regex> #include "behaviortree_cpp/basic_types.h" #include "behaviortree_cpp/contrib/json.hpp" @@ -25,6 +26,19 @@ struct StampedValue Timestamp stamp; }; +// Helper trait to check if templated type is a std::vector +template <typename T> +struct is_vector : std::false_type {}; + +template <typename T, typename A> +struct is_vector<std::vector<T, A>> : std::true_type {}; + +// Helper function to check if a demangled type string is a std::vector<..> +inline bool isVector(std::string type_name) +{ + return std::regex_match(type_name, std::regex(R"(^std::vector<.*>$)")); +} + /** * @brief The Blackboard is the mechanism used by BehaviorTrees to exchange * typed data. @@ -257,8 +271,14 @@ inline void Blackboard::set(const std::string& key, const T& value) std::type_index previous_type = entry.info.type(); + // Allow mismatch if going from vector -> vector<Any>. + auto prev_type_demangled = BT::demangle(entry.value.type()); + bool previous_is_vector = BT::isVector(prev_type_demangled); + bool new_is_vector_any = new_value.type() == typeid(std::vector<Any>); + // check type mismatch - if(previous_type != std::type_index(typeid(T)) && previous_type != new_value.type()) + if(previous_type != std::type_index(typeid(T)) && previous_type != new_value.type() && + !(previous_is_vector && new_is_vector_any)) { bool mismatching = true; if(std::is_constructible<StringView, T>::value) diff --git a/include/behaviortree_cpp/tree_node.h b/include/behaviortree_cpp/tree_node.h index 1d36d3fd1..1b4dc0fdc 100644 --- a/include/behaviortree_cpp/tree_node.h +++ b/include/behaviortree_cpp/tree_node.h @@ -30,13 +30,6 @@ namespace BT { -// Helper trait to check if a type is a std::vector -template <typename T> -struct is_vector : std::false_type {}; - -template <typename T, typename A> -struct is_vector<std::vector<T, A>> : std::true_type {}; - /// This information is used mostly by the XMLParser. struct TreeNodeManifest { @@ -619,7 +612,19 @@ inline Result TreeNode::setOutput(const std::string& key, const T& value) } remapped_key = stripBlackboardPointer(remapped_key); - config().blackboard->set(static_cast<std::string>(remapped_key), value); + + if constexpr(is_vector<T>::value && !std::is_same_v<T, std::vector<Any>>) + { + // If the object is a vector but not a vector<Any>, convert it to vector<Any> before placing it on the blackboard. + auto any_vec = std::vector<Any>(); + std::transform(value.begin(), value.end(), std::back_inserter(any_vec), + [](const auto &element) { return BT::Any(element); }); + config().blackboard->set(static_cast<std::string>(remapped_key), any_vec); + } + else + { + config().blackboard->set(static_cast<std::string>(remapped_key), value); + } return {}; } diff --git a/src/blackboard.cpp b/src/blackboard.cpp index 0f1f304db..ad1ea99ff 100644 --- a/src/blackboard.cpp +++ b/src/blackboard.cpp @@ -217,13 +217,18 @@ std::shared_ptr<Blackboard::Entry> Blackboard::createEntryImpl(const std::string if(storage_it != storage_.end()) { const auto& prev_info = storage_it->second->info; + auto prev_type_demangled = BT::demangle(prev_info.type()); + // Allow mismatch if going from vector -> vector<Any>. + bool previous_is_vector = BT::isVector(prev_type_demangled); + bool new_is_vector_any = info.type() == typeid(std::vector<Any>); + if(prev_info.type() != info.type() && prev_info.isStronglyTyped() && - info.isStronglyTyped()) + info.isStronglyTyped() && !(previous_is_vector && new_is_vector_any)) { auto msg = StrCat("Blackboard entry [", key, "]: once declared, the type of a port" " shall not change. Previously declared type [", - BT::demangle(prev_info.type()), "], current type [", + prev_type_demangled, "], current type [", BT::demangle(info.type()), "]"); throw LogicError(msg); diff --git a/src/xml_parsing.cpp b/src/xml_parsing.cpp index 96ffbd8d7..83a94ee12 100644 --- a/src/xml_parsing.cpp +++ b/src/xml_parsing.cpp @@ -786,10 +786,15 @@ TreeNode::Ptr XMLParser::PImpl::createNodeFromXML(const XMLElement* element, // special case related to convertFromString bool const string_input = (prev_info->type() == typeid(std::string)); - // special case related to unwrapping vector<Any> objects. + // special case related to unwrapping vector<Any> -> vector<T> objects. bool const vec_any_input = (prev_info->type() == typeid(std::vector<Any>)); + // special case related to wrapping vector<T> -> vector<Any> objects. + auto prev_type_demangled = demangle(prev_info->type()); + bool previous_is_vector = BT::isVector(prev_type_demangled); + bool new_is_vector_any = port_info.type() == typeid(std::vector<Any>); - if(port_type_mismatch && !string_input && !vec_any_input) + if(port_type_mismatch && !string_input && + !vec_any_input & !(previous_is_vector && new_is_vector_any)) { blackboard->debugMessage(); From 915732d54022984ba2ac906febbd8d8d117d67e2 Mon Sep 17 00:00:00 2001 From: David Yackzan <dwyackzan@gmail.com> Date: Thu, 24 Oct 2024 13:43:41 -0600 Subject: [PATCH 3/5] Update include/behaviortree_cpp/blackboard.h Co-authored-by: Nathan Brooks <nbbrooks@gmail.com> --- include/behaviortree_cpp/blackboard.h | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/include/behaviortree_cpp/blackboard.h b/include/behaviortree_cpp/blackboard.h index d254b58a4..1fed49c9f 100644 --- a/include/behaviortree_cpp/blackboard.h +++ b/include/behaviortree_cpp/blackboard.h @@ -34,7 +34,7 @@ template <typename T, typename A> struct is_vector<std::vector<T, A>> : std::true_type {}; // Helper function to check if a demangled type string is a std::vector<..> -inline bool isVector(std::string type_name) +inline bool isVector(const std::string& type_name) { return std::regex_match(type_name, std::regex(R"(^std::vector<.*>$)")); } From 9ce8b86edda0d3f25ad5c030aa3fcd8037ad0080 Mon Sep 17 00:00:00 2001 From: David Yackzan <dwyackzan@gmail.com> Date: Wed, 6 Nov 2024 07:39:48 -0700 Subject: [PATCH 4/5] Fix formatting with pre-commit --- include/behaviortree_cpp/blackboard.h | 8 ++++++-- include/behaviortree_cpp/tree_node.h | 18 +++++++++++------- 2 files changed, 17 insertions(+), 9 deletions(-) diff --git a/include/behaviortree_cpp/blackboard.h b/include/behaviortree_cpp/blackboard.h index 1fed49c9f..5189f148c 100644 --- a/include/behaviortree_cpp/blackboard.h +++ b/include/behaviortree_cpp/blackboard.h @@ -28,10 +28,14 @@ struct StampedValue // Helper trait to check if templated type is a std::vector template <typename T> -struct is_vector : std::false_type {}; +struct is_vector : std::false_type +{ +}; template <typename T, typename A> -struct is_vector<std::vector<T, A>> : std::true_type {}; +struct is_vector<std::vector<T, A>> : std::true_type +{ +}; // Helper function to check if a demangled type string is a std::vector<..> inline bool isVector(const std::string& type_name) diff --git a/include/behaviortree_cpp/tree_node.h b/include/behaviortree_cpp/tree_node.h index 1b4dc0fdc..597572ea4 100644 --- a/include/behaviortree_cpp/tree_node.h +++ b/include/behaviortree_cpp/tree_node.h @@ -522,21 +522,25 @@ inline Expected<Timestamp> TreeNode::getInputStamped(const std::string& key, { // Support vector<Any> -> vector<typename T::value_type> conversion. // Only want to compile this path when T is a vector type. - if constexpr (is_vector<T>::value) + if constexpr(is_vector<T>::value) { - if (!std::is_same_v<T, std::vector<Any>> && any_value.type() == typeid(std::vector<Any>)) + if(!std::is_same_v<T, std::vector<Any>> && + any_value.type() == typeid(std::vector<Any>)) { // If the object was originally placed on the blackboard as a vector<Any>, attempt to unwrap the vector // elements according to the templated type. auto any_vec = any_value.cast<std::vector<Any>>(); - if (!any_vec.empty() && any_vec.front().type() != typeid(typename T::value_type)) + if(!any_vec.empty() && + any_vec.front().type() != typeid(typename T::value_type)) { - return nonstd::make_unexpected("Invalid cast requested from vector<Any> to vector<typename T::value_type>." + return nonstd::make_unexpected("Invalid cast requested from vector<Any> to " + "vector<typename T::value_type>." " Element type does not align."); } destination = T(); - std::transform(any_vec.begin(), any_vec.end(), std::back_inserter(destination), - [](Any &element) { return element.cast<typename T::value_type>(); }); + std::transform( + any_vec.begin(), any_vec.end(), std::back_inserter(destination), + [](Any& element) { return element.cast<typename T::value_type>(); }); return Timestamp{ entry->sequence_id, entry->stamp }; } } @@ -618,7 +622,7 @@ inline Result TreeNode::setOutput(const std::string& key, const T& value) // If the object is a vector but not a vector<Any>, convert it to vector<Any> before placing it on the blackboard. auto any_vec = std::vector<Any>(); std::transform(value.begin(), value.end(), std::back_inserter(any_vec), - [](const auto &element) { return BT::Any(element); }); + [](const auto& element) { return BT::Any(element); }); config().blackboard->set(static_cast<std::string>(remapped_key), any_vec); } else From 66079688543b16efc2485119b9195476ce4960b1 Mon Sep 17 00:00:00 2001 From: David Yackzan <dwyackzan@gmail.com> Date: Wed, 6 Nov 2024 08:22:05 -0700 Subject: [PATCH 5/5] Add unit test passing a vector through ports --- tests/gtest_ports.cpp | 131 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 131 insertions(+) diff --git a/tests/gtest_ports.cpp b/tests/gtest_ports.cpp index 8ba750919..24a5dbd85 100644 --- a/tests/gtest_ports.cpp +++ b/tests/gtest_ports.cpp @@ -705,3 +705,134 @@ TEST(PortTest, DefaultWronglyOverriden) // This is correct ASSERT_NO_THROW(auto tree = factory.createTreeFromText(xml_txt_correct)); } + +class OutputVectorStringNode : public SyncActionNode +{ +public: + OutputVectorStringNode(const std::string& name, const NodeConfig& config) + : SyncActionNode(name, config) + {} + static PortsList providedPorts() + { + return { InputPort<std::string>("string1", "val1", "First string"), + InputPort<std::string>("string2", "val2", "Second string"), + OutputPort<std::vector<std::string>>("string_vector", "{string_vector}", + "Vector of strings.") }; + } + + NodeStatus tick() override + { + auto string1 = getInput<std::string>("string1"); + auto string2 = getInput<std::string>("string2"); + + std::vector<std::string> out = { string1.value(), string2.value() }; + setOutput("string_vector", out); + return NodeStatus::SUCCESS; + } +}; + +class InputVectorStringNode : public SyncActionNode +{ +public: + InputVectorStringNode(const std::string& name, const NodeConfig& config) + : SyncActionNode(name, config) + {} + static PortsList providedPorts() + { + return { InputPort<std::vector<std::string>>("string_vector", "{string_vector}", + "Vector of strings.") }; + } + + NodeStatus tick() override + { + std::vector<std::string> expected_vec = { "val1", "val2" }; + std::vector<std::string> actual_vec; + + if(!getInput<std::vector<std::string>>("string_vector", actual_vec)) + { + return NodeStatus::FAILURE; + } + if(expected_vec == actual_vec) + { + return NodeStatus::SUCCESS; + } + else + { + return NodeStatus::FAILURE; + } + } +}; + +class InputVectorDoubleNode : public SyncActionNode +{ +public: + InputVectorDoubleNode(const std::string& name, const NodeConfig& config) + : SyncActionNode(name, config) + {} + static PortsList providedPorts() + { + return { InputPort<std::vector<double>>("double_vector", "{double_vector}", + "Vector of doubles.") }; + } + + NodeStatus tick() override + { + std::vector<double> expected_vec = { 1.0, 2.0 }; + std::vector<double> actual_vec; + + if(!getInput<std::vector<double>>("double_vector", actual_vec)) + { + return NodeStatus::FAILURE; + } + if(expected_vec == actual_vec) + { + return NodeStatus::SUCCESS; + } + else + { + return NodeStatus::FAILURE; + } + } +}; + +TEST(PortTest, VectorAny) +{ + BT::BehaviorTreeFactory factory; + factory.registerNodeType<OutputVectorStringNode>("OutputVectorStringNode"); + factory.registerNodeType<InputVectorStringNode>("InputVectorStringNode"); + factory.registerNodeType<InputVectorDoubleNode>("InputVectorDoubleNode"); + + std::string xml_txt_good = R"( + <root BTCPP_format="4" > + <BehaviorTree> + <Sequence name="root_sequence"> + <OutputVectorStringNode/> + <InputVectorStringNode/> + </Sequence> + </BehaviorTree> + </root>)"; + + std::string xml_txt_bad = R"( + <root BTCPP_format="4" > + <BehaviorTree> + <Sequence name="root_sequence"> + <OutputVectorStringNode/> + <InputVectorDoubleNode double_vector="{string_vector}"/> + </Sequence> + </BehaviorTree> + </root>)"; + + // Test that setting and retrieving a vector<string> works. + BT::Tree tree; + ASSERT_NO_THROW(tree = factory.createTreeFromText(xml_txt_good)); + + BT::NodeStatus status; + ASSERT_NO_THROW(status = tree.tickOnce()); + ASSERT_EQ(status, NodeStatus::SUCCESS); + + // Test that setting a port as a vector<string> and attempting to retrie it as a vector<double> fails. + ASSERT_NO_THROW(tree = factory.createTreeFromText(xml_txt_bad)); + + ASSERT_NO_THROW(status = tree.tickOnce()); + ASSERT_EQ(status, NodeStatus::FAILURE); +}