Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions examples/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,7 @@ CompileExample("t15_nodes_mocking")
CompileExample("t16_global_blackboard")
CompileExample("t17_blackboard_backup")
CompileExample("t18_waypoints")
CompileExample("t19_polymorphic_ports")

CompileExample("ex01_wrap_legacy")
CompileExample("ex02_runtime_ports")
Expand Down
162 changes: 162 additions & 0 deletions examples/t19_polymorphic_ports.cpp
Original file line number Diff line number Diff line change
@@ -0,0 +1,162 @@
#include "behaviortree_cpp/bt_factory.h"

using namespace BT;

/* This tutorial shows how to use polymorphic ports.
*
* When nodes produce and consume shared_ptr<T> via ports,
* you may want a node that outputs shared_ptr<Derived> to feed
* into a node that expects shared_ptr<Base>.
*
* By registering the inheritance relationship with
* factory.registerPolymorphicCast<Derived, Base>(), the library
* handles the upcast automatically — both at tree-creation time
* (port type validation) and at runtime (getInput / get).
*
* Transitive casts are supported: if you register A->B and B->C,
* then A->C works automatically.
*/

//--------------------------------------------------------------
// A simple class hierarchy
//--------------------------------------------------------------

class Animal
{
public:
using Ptr = std::shared_ptr<Animal>;
virtual ~Animal() = default;

virtual std::string name() const
{
return "Animal";
}
};

class Cat : public Animal
{
public:
using Ptr = std::shared_ptr<Cat>;

std::string name() const override
{
return "Cat";
}
};

class Sphynx : public Cat
{
public:
using Ptr = std::shared_ptr<Sphynx>;

std::string name() const override
{
return "Sphynx";
}
};

//--------------------------------------------------------------
// Nodes that produce derived types
//--------------------------------------------------------------

class CreateCat : public SyncActionNode
{
public:
CreateCat(const std::string& name, const NodeConfig& config)
: SyncActionNode(name, config)
{}

NodeStatus tick() override
{
setOutput("animal", std::make_shared<Cat>());
return NodeStatus::SUCCESS;
}

static PortsList providedPorts()
{
return { OutputPort<Cat::Ptr>("animal") };
}
};

class CreateSphynx : public SyncActionNode
{
public:
CreateSphynx(const std::string& name, const NodeConfig& config)
: SyncActionNode(name, config)
{}

NodeStatus tick() override
{
setOutput("animal", std::make_shared<Sphynx>());
return NodeStatus::SUCCESS;
}

static PortsList providedPorts()
{
return { OutputPort<Sphynx::Ptr>("animal") };
}
};

//--------------------------------------------------------------
// A node that consumes the base type
//--------------------------------------------------------------

class SayHi : public SyncActionNode
{
public:
SayHi(const std::string& name, const NodeConfig& config) : SyncActionNode(name, config)
{}

NodeStatus tick() override
{
auto animal = getInput<Animal::Ptr>("animal").value();
std::cout << "Hi! I am a " << animal->name() << std::endl;
return NodeStatus::SUCCESS;
}

static PortsList providedPorts()
{
return { InputPort<Animal::Ptr>("animal") };
}
};

//--------------------------------------------------------------

// clang-format off
static const char* xml_text = R"(
<root BTCPP_format="4" >
<BehaviorTree ID="MainTree">
<Sequence>
<CreateCat animal="{pet}" />
<SayHi animal="{pet}" />
<CreateSphynx animal="{pet2}" />
<SayHi animal="{pet2}" />
</Sequence>
</BehaviorTree>
</root>
)";
// clang-format on

int main()
{
BehaviorTreeFactory factory;

// Register the inheritance relationships.
// This is what makes Cat::Ptr and Sphynx::Ptr assignable to Animal::Ptr ports.
factory.registerPolymorphicCast<Cat, Animal>();
factory.registerPolymorphicCast<Sphynx, Cat>();

factory.registerNodeType<CreateCat>("CreateCat");
factory.registerNodeType<CreateSphynx>("CreateSphynx");
factory.registerNodeType<SayHi>("SayHi");

auto tree = factory.createTreeFromText(xml_text);
tree.tickWhileRunning();

/* Expected output:
*
* Hi! I am a Cat
* Hi! I am a Sphynx
*/
return 0;
}
84 changes: 79 additions & 5 deletions include/behaviortree_cpp/blackboard.h
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
#include "behaviortree_cpp/contrib/json.hpp"
#include "behaviortree_cpp/exceptions.h"
#include "behaviortree_cpp/utils/locked_reference.hpp"
#include "behaviortree_cpp/utils/polymorphic_cast_registry.hpp"
#include "behaviortree_cpp/utils/safe_any.hpp"

#include <memory>
Expand Down Expand Up @@ -149,6 +150,34 @@ class Blackboard

const Blackboard* rootBlackboard() const;

/**
* @brief Set the polymorphic cast registry for this blackboard.
*
* The registry enables polymorphic shared_ptr conversions during get().
* This is typically set automatically when creating trees via BehaviorTreeFactory.
*/
void setPolymorphicCastRegistry(std::shared_ptr<PolymorphicCastRegistry> registry)
{
polymorphic_registry_ = std::move(registry);
}

/**
* @brief Get the polymorphic cast registry (may be null).
*/
[[nodiscard]] const PolymorphicCastRegistry* polymorphicCastRegistry() const
{
return polymorphic_registry_.get();
}

/**
* @brief Cast Any value with polymorphic fallback for shared_ptr types.
*
* First attempts a direct cast. If that fails and T is a shared_ptr type,
* tries a polymorphic cast via the registry. Returns Expected with error on failure.
*/
template <typename T>
[[nodiscard]] Expected<T> tryCastWithPolymorphicFallback(const Any* any) const;

private:
mutable std::mutex storage_mutex_;
mutable std::recursive_mutex entry_mutex_;
Expand All @@ -159,6 +188,9 @@ class Blackboard
std::shared_ptr<Entry> createEntryImpl(const std::string& key, const TypeInfo& info);

bool autoremapping_ = false;

// Optional registry for polymorphic shared_ptr conversions
std::shared_ptr<PolymorphicCastRegistry> polymorphic_registry_;
};

/**
Expand All @@ -177,6 +209,32 @@ void ImportBlackboardFromJSON(const nlohmann::json& json, Blackboard& blackboard

//------------------------------------------------------

template <typename T>
inline Expected<T> Blackboard::tryCastWithPolymorphicFallback(const Any* any) const
{
// Try direct cast first
auto result = any->tryCast<T>();
if(result)
{
return result.value();
}

// For shared_ptr types, try polymorphic cast via registry (Issue #943)
if constexpr(is_shared_ptr<T>::value)
{
if(polymorphic_registry_)
{
auto poly_result = any->tryCastWithRegistry<T>(*polymorphic_registry_);
if(poly_result)
{
return poly_result.value();
}
}
}

return nonstd::make_unexpected(result.error());
}

template <typename T>
inline T Blackboard::get(const std::string& key) const
{
Expand All @@ -188,7 +246,12 @@ inline T Blackboard::get(const std::string& key) const
throw RuntimeError("Blackboard::get() error. Entry [", key,
"] hasn't been initialized, yet");
}
return any_ref.get()->cast<T>();
auto result = tryCastWithPolymorphicFallback<T>(any);
if(!result)
{
throw std::runtime_error(result.error());
}
return result.value();
}
throw RuntimeError("Blackboard::get() error. Missing key [", key, "]");
}
Expand Down Expand Up @@ -325,11 +388,17 @@ inline bool Blackboard::get(const std::string& key, T& value) const
{
if(auto any_ref = getAnyLocked(key))
{
if(any_ref.get()->empty())
const auto& any = any_ref.get();
if(any->empty())
{
return false;
}
value = any_ref.get()->cast<T>();
auto result = tryCastWithPolymorphicFallback<T>(any);
if(!result)
{
throw std::runtime_error(result.error());
}
value = result.value();
return true;
}
return false;
Expand All @@ -346,8 +415,13 @@ inline Expected<Timestamp> Blackboard::getStamped(const std::string& key, T& val
return nonstd::make_unexpected(StrCat("Blackboard::getStamped() error. Entry [",
key, "] hasn't been initialized, yet"));
}
value = entry->value.cast<T>();
return Timestamp{ entry->sequence_id, entry->stamp };
auto result = tryCastWithPolymorphicFallback<T>(&entry->value);
if(result)
{
value = result.value();
return Timestamp{ entry->sequence_id, entry->stamp };
}
return nonstd::make_unexpected(result.error());
}
return nonstd::make_unexpected(
StrCat("Blackboard::getStamped() error. Missing key [", key, "]"));
Expand Down
40 changes: 40 additions & 0 deletions include/behaviortree_cpp/bt_factory.h
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
#include "behaviortree_cpp/behavior_tree.h"
#include "behaviortree_cpp/contrib/json.hpp"
#include "behaviortree_cpp/contrib/magic_enum.hpp"
#include "behaviortree_cpp/utils/polymorphic_cast_registry.hpp"

#include <filesystem>
#include <functional>
Expand Down Expand Up @@ -529,6 +530,45 @@ class BehaviorTreeFactory
[[nodiscard]] const std::unordered_map<std::string, SubstitutionRule>&
substitutionRules() const;

/**
* @brief Register a polymorphic cast relationship between Derived and Base types.
*
* This enables passing shared_ptr<Derived> to ports expecting shared_ptr<Base>
* without type mismatch errors. The relationship is automatically applied
* to all trees created from this factory.
*
* Example:
* factory.registerPolymorphicCast<Cat, Animal>();
* factory.registerPolymorphicCast<Sphynx, Cat>();
*
* @tparam Derived The derived class (must inherit from Base)
* @tparam Base The base class (must be polymorphic)
*/
template <typename Derived, typename Base>
void registerPolymorphicCast()
{
polymorphicCastRegistry().registerCast<Derived, Base>();
}

/**
* @brief Access the polymorphic cast registry.
*
* The registry is shared with all trees created from this factory,
* allowing trees to outlive the factory while maintaining access
* to polymorphic cast relationships.
*/
[[nodiscard]] PolymorphicCastRegistry& polymorphicCastRegistry();
[[nodiscard]] const PolymorphicCastRegistry& polymorphicCastRegistry() const;

/**
* @brief Get a shared pointer to the polymorphic cast registry.
*
* This allows trees and blackboards to hold a reference to the registry
* that outlives the factory.
*/
[[nodiscard]] std::shared_ptr<PolymorphicCastRegistry>
polymorphicCastRegistryPtr() const;

private:
struct PImpl;
std::unique_ptr<PImpl> _p;
Expand Down
8 changes: 7 additions & 1 deletion include/behaviortree_cpp/tree_node.h
Original file line number Diff line number Diff line change
Expand Up @@ -591,7 +591,13 @@ inline Expected<Timestamp> TreeNode::getInputStamped(const std::string& key,
}
else
{
destination = any_value.cast<T>();
auto result =
config().blackboard->tryCastWithPolymorphicFallback<T>(&any_value);
if(!result)
{
throw std::runtime_error(result.error());
}
destination = result.value();
}
return Timestamp{ entry->sequence_id, entry->stamp };
}
Expand Down
Loading
Loading