BehaviorTree
Core Library to create and execute Behavior Trees
Loading...
Searching...
No Matches
blackboard.h
1#pragma once
2
3#include "behaviortree_cpp/basic_types.h"
4#include "behaviortree_cpp/contrib/json.hpp"
5#include "behaviortree_cpp/exceptions.h"
6#include "behaviortree_cpp/utils/locked_reference.hpp"
7#include "behaviortree_cpp/utils/polymorphic_cast_registry.hpp"
8#include "behaviortree_cpp/utils/safe_any.hpp"
9
10#include <memory>
11#include <mutex>
12#include <shared_mutex>
13#include <string>
14#include <unordered_map>
15
16namespace BT
17{
18
19/// This type contains a pointer to Any, protected
20/// with a locked mutex as long as the object is in scope
21using AnyPtrLocked = LockedPtr<Any>;
22
23template <typename T>
24struct StampedValue
25{
26 T value;
27 Timestamp stamp;
28};
29
30/**
31 * @brief The Blackboard is the mechanism used by BehaviorTrees to exchange
32 * typed data.
33 */
34class Blackboard
35{
36public:
37 using Ptr = std::shared_ptr<Blackboard>;
38
39protected:
40 // This is intentionally protected. Use Blackboard::create instead
41 Blackboard(Blackboard::Ptr parent) : parent_bb_(parent)
42 {}
43
44public:
45 Blackboard(const Blackboard&) = delete;
46 Blackboard& operator=(const Blackboard&) = delete;
47 Blackboard(Blackboard&&) = delete;
48 Blackboard& operator=(Blackboard&&) = delete;
49
50 struct Entry
51 {
52 Any value;
53 TypeInfo info;
54 StringConverter string_converter;
55 mutable std::mutex entry_mutex;
56
57 uint64_t sequence_id = 0;
58 // timestamp since epoch
59 std::chrono::nanoseconds stamp = std::chrono::nanoseconds{ 0 };
60
61 Entry(const TypeInfo& _info) : info(_info)
62 {}
63
64 ~Entry() = default;
65 Entry(const Entry&) = delete;
66 Entry& operator=(const Entry&) = delete;
67 Entry(Entry&&) = delete;
68 Entry& operator=(Entry&&) = delete;
69 };
70
71 /** Use this static method to create an instance of the BlackBoard
72 * to share among all your NodeTrees.
73 */
74 static Blackboard::Ptr create(Blackboard::Ptr parent = {})
75 {
76 return std::shared_ptr<Blackboard>(new Blackboard(parent));
77 }
78
79 virtual ~Blackboard() = default;
80
81 void enableAutoRemapping(bool remapping);
82
83 [[nodiscard]] const std::shared_ptr<Entry> getEntry(const std::string& key) const;
84
85 [[nodiscard]] std::shared_ptr<Blackboard::Entry> getEntry(const std::string& key);
86
87 [[nodiscard]] AnyPtrLocked getAnyLocked(const std::string& key);
88
89 [[nodiscard]] AnyPtrLocked getAnyLocked(const std::string& key) const;
90
91 [[deprecated("Use getAnyLocked instead")]] const Any*
92 getAny(const std::string& key) const;
93
94 [[deprecated("Use getAnyLocked instead")]] Any* getAny(const std::string& key);
95
96 /** Return true if the entry with the given key was found.
97 * Note that this method may throw an exception if the cast to T failed.
98 */
99 template <typename T>
100 [[nodiscard]] bool get(const std::string& key, T& value) const;
101
102 template <typename T>
103 [[nodiscard]] Expected<Timestamp> getStamped(const std::string& key, T& value) const;
104
105 /**
106 * Version of get() that throws if it fails.
107 */
108 template <typename T>
109 [[nodiscard]] T get(const std::string& key) const;
110
111 template <typename T>
112 [[nodiscard]] Expected<StampedValue<T>> getStamped(const std::string& key) const;
113
114 /// Update the entry with the given key
115 template <typename T>
116 void set(const std::string& key, const T& value);
117
118 void unset(const std::string& key);
119
120 [[nodiscard]] const TypeInfo* entryInfo(const std::string& key);
121
122 void addSubtreeRemapping(StringView internal, StringView external);
123
124 void debugMessage() const;
125
126 [[nodiscard]] std::vector<StringView> getKeys() const;
127
128 [[deprecated("This command is unsafe. Consider using Backup/Restore instead")]] void
129 clear();
130
131 void createEntry(const std::string& key, const TypeInfo& info);
132
133 /**
134 * @brief cloneInto copies the values of the entries
135 * into another blackboard. Known limitations:
136 *
137 * - it doesn't update the remapping in dst
138 * - it doesn't change the parent blackboard os dst
139 *
140 * @param dst destination, i.e. blackboard to be updated
141 */
142 void cloneInto(Blackboard& dst) const;
143
144 Blackboard::Ptr parent();
145
146 // recursively look for parent Blackboard, until you find the root
147 Blackboard* rootBlackboard();
148
149 const Blackboard* rootBlackboard() const;
150
151 /**
152 * @brief Set the polymorphic cast registry for this blackboard.
153 *
154 * The registry enables polymorphic shared_ptr conversions during get().
155 * This is typically set automatically when creating trees via BehaviorTreeFactory.
156 */
157 void setPolymorphicCastRegistry(std::shared_ptr<PolymorphicCastRegistry> registry)
158 {
159 polymorphic_registry_ = std::move(registry);
160 }
161
162 /**
163 * @brief Get the polymorphic cast registry (may be null).
164 */
166 {
167 return polymorphic_registry_.get();
168 }
169
170 /**
171 * @brief Cast Any value with polymorphic fallback for shared_ptr types.
172 *
173 * First attempts a direct cast. If that fails and T is a shared_ptr type,
174 * tries a polymorphic cast via the registry. Returns Expected with error on failure.
175 */
176 template <typename T>
177 [[nodiscard]] Expected<T> tryCastWithPolymorphicFallback(const Any* any) const;
178
179private:
180 mutable std::shared_mutex storage_mutex_;
181 std::unordered_map<std::string, std::shared_ptr<Entry>> storage_;
182 std::weak_ptr<Blackboard> parent_bb_;
183 std::unordered_map<std::string, std::string> internal_to_external_;
184
185 std::shared_ptr<Entry> createEntryImpl(const std::string& key, const TypeInfo& info);
186
187 bool autoremapping_ = false;
188
189 // Optional registry for polymorphic shared_ptr conversions
190 std::shared_ptr<PolymorphicCastRegistry> polymorphic_registry_;
191};
192
193/**
194 * @brief ExportBlackboardToJSON will create a JSON
195 * that contains the current values of the blackboard.
196 * Complex types must be registered with JsonExporter::get()
197 */
199
200/**
201 * @brief ImportBlackboardFromJSON will append elements to the blackboard,
202 * using the values parsed from the JSON file created using ExportBlackboardToJSON.
203 * Complex types must be registered with JsonExporter::get()
204 */
205void ImportBlackboardFromJSON(const nlohmann::json& json, Blackboard& blackboard);
206
207//------------------------------------------------------
208
209template <typename T>
211{
212 // Try direct cast first
213 auto result = any->tryCast<T>();
214 if(result)
215 {
216 return result.value();
217 }
218
219 // For shared_ptr types, try polymorphic cast via registry (Issue #943)
220 if constexpr(is_shared_ptr<T>::value)
221 {
222 if(polymorphic_registry_)
223 {
224 auto poly_result = any->tryCastWithRegistry<T>(*polymorphic_registry_);
225 if(poly_result)
226 {
227 return poly_result.value();
228 }
229 }
230 }
231
232 return nonstd::make_unexpected(result.error());
233}
234
235template <typename T>
236inline T Blackboard::get(const std::string& key) const
237{
238 if(auto any_ref = getAnyLocked(key))
239 {
240 const auto& any = any_ref.get();
241 if(any->empty())
242 {
243 throw RuntimeError("Blackboard::get() error. Entry [", key,
244 "] hasn't been initialized, yet");
245 }
246 auto result = tryCastWithPolymorphicFallback<T>(any);
247 if(!result)
248 {
249 throw std::runtime_error(result.error());
250 }
251 return result.value();
252 }
253 throw RuntimeError("Blackboard::get() error. Missing key [", key, "]");
254}
255
256inline void Blackboard::unset(const std::string& key)
257{
258 std::unique_lock storage_lock(storage_mutex_);
259
260 // check local storage
261 auto it = storage_.find(key);
262 if(it == storage_.end())
263 {
264 // No entry, nothing to do.
265 return;
266 }
267
268 storage_.erase(it);
269}
270
271template <typename T>
272inline void Blackboard::set(const std::string& key, const T& value)
273{
274 if(StartWith(key, '@'))
275 {
276 rootBlackboard()->set(key.substr(1, key.size() - 1), value);
277 return;
278 }
279 std::shared_lock storage_lock(storage_mutex_);
280
281 // check local storage
282 auto it = storage_.find(key);
283 if(it == storage_.end())
284 {
285 // create a new entry
286 Any new_value(value);
287 storage_lock.unlock();
288 std::shared_ptr<Blackboard::Entry> entry;
289 // if a new generic port is created with a string, it's type should be AnyTypeAllowed
290 if constexpr(std::is_same_v<std::string, T>)
291 {
292 entry = createEntryImpl(key, PortInfo(PortDirection::INOUT));
293 }
294 else
295 {
296 PortInfo new_port(PortDirection::INOUT, new_value.type(),
297 GetAnyFromStringFunctor<T>());
298 entry = createEntryImpl(key, new_port);
299 }
300
301 // Lock entry_mutex before writing to prevent data races with
302 // concurrent readers (BUG-1/BUG-8 fix).
303 std::scoped_lock entry_lock(entry->entry_mutex);
304 entry->value = new_value;
305 entry->sequence_id++;
306 entry->stamp = std::chrono::steady_clock::now().time_since_epoch();
307 }
308 else
309 {
310 // this is not the first time we set this entry, we need to check
311 // if the type is the same or not.
312 // Copy shared_ptr to prevent use-after-free if another thread
313 // calls unset() while we hold the reference (BUG-2 fix).
314 auto entry_ptr = it->second;
315 storage_lock.unlock();
316 Entry& entry = *entry_ptr;
317
318 std::scoped_lock scoped_lock(entry.entry_mutex);
319
320 Any& previous_any = entry.value;
321 Any new_value(value);
322
323 // special case: entry exists but it is not strongly typed... yet
324 if(!entry.info.isStronglyTyped())
325 {
326 // Use the new type to create a new entry that is strongly typed.
327 entry.info = TypeInfo::Create<T>();
328 entry.sequence_id++;
329 entry.stamp = std::chrono::steady_clock::now().time_since_epoch();
330 previous_any = std::move(new_value);
331 return;
332 }
333
334 std::type_index previous_type = entry.info.type();
335
336 // check type mismatch
337 if(previous_type != std::type_index(typeid(T)) && previous_type != new_value.type())
338 {
339 bool mismatching = true;
340 if(std::is_constructible<StringView, T>::value)
341 {
342 Any any_from_string = entry.info.parseString(value);
343 if(any_from_string.empty() == false)
344 {
345 mismatching = false;
346 new_value = std::move(any_from_string);
347 }
348 }
349 // check if we are doing a safe cast between numbers
350 // for instance, it is safe to use int(100) to set
351 // a uint8_t port, but not int(-42) or int(300)
352 if constexpr(std::is_arithmetic_v<T>)
353 {
354 if(mismatching && isCastingSafe(previous_type, value))
355 {
356 mismatching = false;
357 }
358 }
359
360 if(mismatching)
361 {
362 debugMessage();
363
364 auto msg = StrCat("Blackboard::set(", key,
365 "): once declared, "
366 "the type of a port shall not change. "
367 "Previously declared type [",
368 BT::demangle(previous_type), "], current type [",
369 BT::demangle(typeid(T)), "]");
370 throw LogicError(msg);
371 }
372 }
373 // if doing set<BT::Any>, skip type check
374 if constexpr(std::is_same_v<Any, T>)
375 {
376 previous_any = new_value;
377 }
378 else
379 {
380 // copy only if the type is compatible
381 new_value.copyInto(previous_any);
382 }
383 entry.sequence_id++;
384 entry.stamp = std::chrono::steady_clock::now().time_since_epoch();
385 }
386}
387
388template <typename T>
389inline bool Blackboard::get(const std::string& key, T& value) const
390{
391 if(auto any_ref = getAnyLocked(key))
392 {
393 const auto& any = any_ref.get();
394 if(any->empty())
395 {
396 return false;
397 }
398 auto result = tryCastWithPolymorphicFallback<T>(any);
399 if(!result)
400 {
401 throw std::runtime_error(result.error());
402 }
403 value = result.value();
404 return true;
405 }
406 return false;
407}
408
409template <typename T>
410inline Expected<Timestamp> Blackboard::getStamped(const std::string& key, T& value) const
411{
412 if(auto entry = getEntry(key))
413 {
414 std::unique_lock lk(entry->entry_mutex);
415 if(entry->value.empty())
416 {
417 return nonstd::make_unexpected(StrCat("Blackboard::getStamped() error. Entry [",
418 key, "] hasn't been initialized, yet"));
419 }
420 auto result = tryCastWithPolymorphicFallback<T>(&entry->value);
421 if(result)
422 {
423 value = result.value();
424 return Timestamp{ entry->sequence_id, entry->stamp };
425 }
426 return nonstd::make_unexpected(result.error());
427 }
428 return nonstd::make_unexpected(
429 StrCat("Blackboard::getStamped() error. Missing key [", key, "]"));
430}
431
432template <typename T>
433inline Expected<StampedValue<T>> Blackboard::getStamped(const std::string& key) const
434{
435 StampedValue<T> out;
436 if(auto res = getStamped<T>(key, out.value))
437 {
438 out.stamp = *res;
439 return out;
440 }
441 else
442 {
443 return nonstd::make_unexpected(res.error());
444 }
445}
446
447} // namespace BT
Definition: safe_any.hpp:50
The Blackboard is the mechanism used by BehaviorTrees to exchange typed data.
Definition: blackboard.h:35
void set(const std::string &key, const T &value)
Update the entry with the given key.
Definition: blackboard.h:272
T get(const std::string &key) const
Definition: blackboard.h:236
const PolymorphicCastRegistry * polymorphicCastRegistry() const
Get the polymorphic cast registry (may be null).
Definition: blackboard.h:165
void setPolymorphicCastRegistry(std::shared_ptr< PolymorphicCastRegistry > registry)
Set the polymorphic cast registry for this blackboard.
Definition: blackboard.h:157
Expected< T > tryCastWithPolymorphicFallback(const Any *any) const
Cast Any value with polymorphic fallback for shared_ptr types.
Definition: blackboard.h:210
bool get(const std::string &key, T &value) const
Definition: blackboard.h:389
static Blackboard::Ptr create(Blackboard::Ptr parent={})
Definition: blackboard.h:74
void cloneInto(Blackboard &dst) const
cloneInto copies the values of the entries into another blackboard. Known limitations:
The LockedPtr class is used to share a pointer to an object and a mutex that protects the read/write ...
Definition: locked_reference.hpp:18
Registry for polymorphic shared_ptr cast relationships.
Definition: polymorphic_cast_registry.hpp:47
Definition: basic_types.h:404
Definition: basic_types.h:357
Definition: action_node.h:24
void ImportBlackboardFromJSON(const nlohmann::json &json, Blackboard &blackboard)
ImportBlackboardFromJSON will append elements to the blackboard, using the values parsed from the JSO...
nlohmann::json ExportBlackboardToJSON(const Blackboard &blackboard)
ExportBlackboardToJSON will create a JSON that contains the current values of the blackboard....
Definition: blackboard.h:51
Definition: blackboard.h:25
Definition: basic_types.h:340
Definition: safe_any.hpp:39