|
| 1 | +# Port Connection and Validation Rules |
| 2 | + |
| 3 | +This document describes the rules that govern how ports can be connected in BehaviorTree.CPP, including type checking, type conversion, and special cases. |
| 4 | + |
| 5 | +## Overview |
| 6 | + |
| 7 | +BehaviorTree.CPP uses a type system for ports that enforces type safety while providing flexibility through several special rules. Type checking occurs primarily at **tree creation time** (when parsing XML), not at runtime. |
| 8 | + |
| 9 | +## Port Types Classification |
| 10 | + |
| 11 | +### 1. Strongly Typed Ports |
| 12 | + |
| 13 | +A port is **strongly typed** when declared with a specific type: |
| 14 | + |
| 15 | +```cpp |
| 16 | +InputPort<int>("my_port") |
| 17 | +OutputPort<double>("result") |
| 18 | +InputPort<Position2D>("goal") |
| 19 | +``` |
| 20 | + |
| 21 | +### 2. Generic/Weakly Typed Ports (AnyTypeAllowed) |
| 22 | + |
| 23 | +A port is **generic** (not strongly typed) when: |
| 24 | +- Declared without a type parameter: `InputPort<>("my_port")` |
| 25 | +- Declared with `AnyTypeAllowed`: `InputPort<AnyTypeAllowed>("my_port")` |
| 26 | +- Declared with `BT::Any`: `InputPort<BT::Any>("my_port")` |
| 27 | + |
| 28 | +```cpp |
| 29 | +// All of these create generic ports: |
| 30 | +InputPort<>("value") // defaults to AnyTypeAllowed |
| 31 | +InputPort<AnyTypeAllowed>("value") // explicit AnyTypeAllowed |
| 32 | +InputPort<BT::Any>("value") // BT::Any type |
| 33 | +``` |
| 34 | + |
| 35 | +The `isStronglyTyped()` method returns `false` for these ports: |
| 36 | + |
| 37 | +```cpp |
| 38 | +// From basic_types.h |
| 39 | +bool isStronglyTyped() const |
| 40 | +{ |
| 41 | + return type_info_ != typeid(AnyTypeAllowed) && type_info_ != typeid(BT::Any); |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +## Port Connection Rules |
| 46 | + |
| 47 | +### Rule 1: Same Type - Always Compatible |
| 48 | + |
| 49 | +Ports of the **exact same type** can always be connected: |
| 50 | + |
| 51 | +```cpp |
| 52 | +// Node A |
| 53 | +OutputPort<int>("value") // writes int |
| 54 | + |
| 55 | +// Node B |
| 56 | +InputPort<int>("value") // reads int |
| 57 | + |
| 58 | +// Connection: OK |
| 59 | +``` |
| 60 | + |
| 61 | +**Test reference:** `gtest_port_type_rules.cpp` - `SameType_IntToInt`, `SameType_StringToString`, `SameType_CustomTypeToCustomType` tests |
| 62 | + |
| 63 | +### Rule 2: Generic Port - Compatible with Any Type |
| 64 | + |
| 65 | +A **generic port** (`AnyTypeAllowed` or `BT::Any`) can connect to any other port: |
| 66 | + |
| 67 | +```cpp |
| 68 | +// Node A with generic output |
| 69 | +OutputPort<>("output") // generic, can write anything |
| 70 | + |
| 71 | +// Node B with typed input |
| 72 | +InputPort<int>("input_int") // expects int |
| 73 | + |
| 74 | +// Connection: OK - generic port accepts any type |
| 75 | +``` |
| 76 | + |
| 77 | +**Test reference:** `gtest_port_type_rules.cpp` - `GenericPort_AcceptsInt`, `GenericPort_AcceptsString`, `GenericOutput_ToTypedInput` tests |
| 78 | + |
| 79 | +### Rule 3: String is a "Universal Donor" (Generic Port) |
| 80 | + |
| 81 | +When a blackboard entry is created as `std::string`, it can be connected to ports of **any type** that has a `convertFromString<T>()` specialization. This is the "string as generic port" rule. |
| 82 | + |
| 83 | +**Source:** `xml_parsing.cpp` |
| 84 | +```cpp |
| 85 | +// special case related to convertFromString |
| 86 | +bool const string_input = (prev_info->type() == typeid(std::string)); |
| 87 | + |
| 88 | +if(port_type_mismatch && !string_input) |
| 89 | +{ |
| 90 | + // Error thrown only if NOT a string input |
| 91 | + throw RuntimeError("The creation of the tree failed..."); |
| 92 | +} |
| 93 | +``` |
| 94 | +
|
| 95 | +**Example:** |
| 96 | +```xml |
| 97 | +<Sequence> |
| 98 | + <!-- Creates blackboard entry "value" as string with value "42" --> |
| 99 | + <SetBlackboard value="42" output_key="value" /> |
| 100 | +
|
| 101 | + <!-- Reads "value" as int - OK because string can convert to int --> |
| 102 | + <NodeExpectingInt input="{value}" /> |
| 103 | +</Sequence> |
| 104 | +``` |
| 105 | + |
| 106 | +**Also applies to:** |
| 107 | +- Subtree port passing (string values passed to typed subtree ports) |
| 108 | +- Script node assignments |
| 109 | + |
| 110 | +**Test reference:** `gtest_port_type_rules.cpp` - `StringToInt_ViaConvertFromString`, `StringToCustomType_ViaConvertFromString`, `SubtreeStringInput_ToTypedPort` tests |
| 111 | + |
| 112 | +### Rule 4: String Creation in Blackboard |
| 113 | + |
| 114 | +When using `Blackboard::set<std::string>()`, the entry is created with `AnyTypeAllowed` type, not `std::string`: |
| 115 | + |
| 116 | +**Source:** `blackboard.h` |
| 117 | +```cpp |
| 118 | +// if a new generic port is created with a string, it's type should be AnyTypeAllowed |
| 119 | +if constexpr(std::is_same_v<std::string, T>) |
| 120 | +{ |
| 121 | + entry = createEntryImpl(key, PortInfo(PortDirection::INOUT)); // AnyTypeAllowed |
| 122 | +} |
| 123 | +``` |
| 124 | + |
| 125 | +This allows subsequent writes of different types to the same entry. |
| 126 | + |
| 127 | +**Test reference:** `gtest_port_type_rules.cpp` - `BlackboardSetString_CreatesGenericEntry`, `StringEntry_CanBecomeTyped` tests |
| 128 | + |
| 129 | +### Rule 5: Type Lock After First Strongly-Typed Write |
| 130 | + |
| 131 | +Once a blackboard entry receives a **strongly typed** value, its type is locked: |
| 132 | + |
| 133 | +**Source:** `blackboard.h` |
| 134 | +```cpp |
| 135 | +// special case: entry exists but it is not strongly typed... yet |
| 136 | +if(!entry.info.isStronglyTyped()) |
| 137 | +{ |
| 138 | + // Use the new type to create a strongly typed entry |
| 139 | + entry.info = TypeInfo::Create<T>(); |
| 140 | + // ... |
| 141 | + return; |
| 142 | +} |
| 143 | +``` |
| 144 | + |
| 145 | +After this, writing a different type will fail (with exceptions noted below). |
| 146 | + |
| 147 | +**Test reference:** `gtest_port_type_rules.cpp` - `TypeLock_CannotChangeAfterTypedWrite`, `TypeLock_XMLTreeCreation_TypeMismatch`, `TypeLock_RuntimeTypeChange_Fails` tests |
| 148 | + |
| 149 | +### Rule 6: BT::Any Bypasses Type Checking |
| 150 | + |
| 151 | +When a blackboard entry is **created with type `BT::Any`**, it can store different types over time. This requires the entry to be explicitly created as `BT::Any` type. |
| 152 | + |
| 153 | +**Important:** Wrapping a value with `BT::Any()` does **not** bypass type checking - the wrapper is unwrapped and the inner type is used: |
| 154 | + |
| 155 | +```cpp |
| 156 | +// This creates an entry of type int, NOT BT::Any |
| 157 | +bb->set("key", BT::Any(42)); |
| 158 | + |
| 159 | +// This will FAIL - entry is int, not BT::Any |
| 160 | +bb->set("key", BT::Any("hello")); // throws LogicError |
| 161 | +``` |
| 162 | +
|
| 163 | +To actually allow different types, create the entry as `BT::Any`: |
| 164 | +
|
| 165 | +```cpp |
| 166 | +// Create entry explicitly as BT::Any type |
| 167 | +bb->createEntry("key", TypeInfo::Create<BT::Any>()); |
| 168 | +
|
| 169 | +// Now different types are allowed |
| 170 | +bb->set("key", BT::Any(42)); // OK |
| 171 | +bb->set("key", BT::Any("hello")); // OK |
| 172 | +bb->set("key", BT::Any(3.14)); // OK |
| 173 | +``` |
| 174 | + |
| 175 | +**Test reference:** `gtest_port_type_rules.cpp` - `BTAny_WrapperDoesNotBypassTypeCheck`, `BTAny_EntryType_AllowsDifferentTypes`, `BTAny_Port_AcceptsDifferentTypes` tests |
| 176 | + |
| 177 | +### Rule 7: Type Mismatch Between Strongly Typed Ports - Error |
| 178 | + |
| 179 | +If two **strongly typed** ports with **different types** try to use the same blackboard entry, an error is thrown at tree creation: |
| 180 | + |
| 181 | +```xml |
| 182 | +<!-- This will FAIL at tree creation --> |
| 183 | +<Sequence> |
| 184 | + <NodeA output_int="{value}" /> <!-- Creates entry as int --> |
| 185 | + <NodeB input_string="{value}" /> <!-- Tries to read as string - ERROR --> |
| 186 | +</Sequence> |
| 187 | +``` |
| 188 | + |
| 189 | +**Test reference:** `gtest_port_type_rules.cpp` - `TypeLock_XMLTreeCreation_TypeMismatch`, `TypeLock_IntToDouble_Fails`, `TypeLock_CustomTypeChange_Fails` tests |
| 190 | + |
| 191 | +## Type Conversion via convertFromString |
| 192 | + |
| 193 | +### Built-in Conversions |
| 194 | + |
| 195 | +The library provides `convertFromString<T>()` for: |
| 196 | +- `int`, `long`, `long long`, and unsigned variants |
| 197 | +- `float`, `double` |
| 198 | +- `bool` (accepts "true"/"false", "1"/"0") |
| 199 | +- `std::string` |
| 200 | +- `std::vector<T>` (semicolon-separated values) |
| 201 | +- Enums (when registered) |
| 202 | + |
| 203 | +### Custom Type Conversion |
| 204 | + |
| 205 | +To make a custom type compatible with string ports, specialize `convertFromString`: |
| 206 | + |
| 207 | +```cpp |
| 208 | +namespace BT |
| 209 | +{ |
| 210 | +template <> |
| 211 | +inline Position2D convertFromString(StringView str) |
| 212 | +{ |
| 213 | + auto parts = splitString(str, ';'); |
| 214 | + if(parts.size() != 2) |
| 215 | + throw RuntimeError("invalid input"); |
| 216 | + |
| 217 | + Position2D output; |
| 218 | + output.x = convertFromString<double>(parts[0]); |
| 219 | + output.y = convertFromString<double>(parts[1]); |
| 220 | + return output; |
| 221 | +} |
| 222 | +} |
| 223 | +``` |
| 224 | + |
| 225 | +**Test reference:** `gtest_ports.cpp`, `t03_generic_ports.cpp` |
| 226 | + |
| 227 | +### JSON Format Support |
| 228 | + |
| 229 | +Custom types can also use JSON format with "json:" prefix: |
| 230 | + |
| 231 | +```cpp |
| 232 | +InputPort<Point2D>("pointE", R"(json:{"x":9,"y":10})", "description") |
| 233 | +``` |
| 234 | + |
| 235 | +**Test reference:** `gtest_ports.cpp` |
| 236 | + |
| 237 | +## Validation Timeline |
| 238 | + |
| 239 | +### At Tree Creation (XML Parsing) |
| 240 | + |
| 241 | +1. **Port name validation** - Checks port exists in node manifest |
| 242 | +2. **Literal value validation** - If port value is not a blackboard reference, validates conversion |
| 243 | +3. **Blackboard entry type check** - If entry exists, checks type compatibility |
| 244 | + |
| 245 | +**Source:** `xml_parsing.cpp` |
| 246 | +```cpp |
| 247 | +if(!is_blackboard && port_model.converter() && port_model.isStronglyTyped()) |
| 248 | +{ |
| 249 | + try |
| 250 | + { |
| 251 | + port_model.converter()(port_value); // Validate conversion |
| 252 | + } |
| 253 | + catch(std::exception& ex) |
| 254 | + { |
| 255 | + throw LogicError("The port... can not be converted to " + port_model.typeName()); |
| 256 | + } |
| 257 | +} |
| 258 | +``` |
| 259 | + |
| 260 | +### At Runtime (Blackboard::set) |
| 261 | + |
| 262 | +1. **Type match check** - Compares new type with entry's declared type |
| 263 | +2. **String conversion attempt** - If mismatch, tries `parseString()` |
| 264 | + |
| 265 | +## Summary Table |
| 266 | + |
| 267 | +| Scenario | Compatible? | Notes | |
| 268 | +|----------|-------------|-------| |
| 269 | +| Same types | Yes | Always works | |
| 270 | +| Generic port (either side) | Yes | `AnyTypeAllowed` or `BT::Any` | |
| 271 | +| String → Typed port | Yes | Via `convertFromString<T>()` | |
| 272 | +| Typed → String port | No | Type mismatch error | |
| 273 | +| int → double | No | Different strongly-typed | |
| 274 | +| Point2D → std::string | No | Unless entry was string first | |
| 275 | +| BT::Any entry to anything | Yes | Entry must be created as BT::Any type | |
| 276 | + |
| 277 | +## Reserved Port Names |
| 278 | + |
| 279 | +The following names **cannot** be used for ports: |
| 280 | +- `name` - Reserved for node instance name |
| 281 | +- `ID` - Reserved for node type ID |
| 282 | +- Names starting with `_` - Reserved for internal use |
| 283 | + |
| 284 | +**Test reference:** `gtest_port_type_rules.cpp` - `ReservedPortName_ThrowsOnRegistration` test |
| 285 | + |
| 286 | +## Common Patterns |
| 287 | + |
| 288 | +### Pattern 1: Type-Safe Port Chain |
| 289 | +```xml |
| 290 | +<Sequence> |
| 291 | + <CalculateGoal goal="{GoalPosition}" /> <!-- Output: Position2D --> |
| 292 | + <PrintTarget target="{GoalPosition}" /> <!-- Input: Position2D --> |
| 293 | +</Sequence> |
| 294 | +``` |
| 295 | + |
| 296 | +### Pattern 2: String Literal to Typed Port |
| 297 | +```xml |
| 298 | +<Sequence> |
| 299 | + <SetBlackboard value="1.5;2.5" output_key="pos" /> |
| 300 | + <MoveToPosition target="{pos}" /> <!-- Converts string to Position2D --> |
| 301 | +</Sequence> |
| 302 | +``` |
| 303 | + |
| 304 | +### Pattern 3: Generic Intermediate Storage |
| 305 | +```xml |
| 306 | +<Sequence> |
| 307 | + <TypedNode output_int="{value}" /> <!-- Writes int to generic entry --> |
| 308 | + <GenericNode input="{value}" /> <!-- Generic port reads it --> |
| 309 | + <AnotherTypedNode input_int="{value}"/> <!-- Int port reads it --> |
| 310 | +</Sequence> |
| 311 | +``` |
| 312 | + |
| 313 | +## Error Messages |
| 314 | + |
| 315 | +Common type-related errors: |
| 316 | + |
| 317 | +1. **"The creation of the tree failed because the port [X] was initially created with type [A] and, later type [B] was used somewhere else."** |
| 318 | + - Cause: Two nodes use same blackboard key with incompatible types |
| 319 | + - Solution: Ensure consistent types or use string/generic ports |
| 320 | + |
| 321 | +2. **"Blackboard::set(X): once declared, the type of a port shall not change."** |
| 322 | + - Cause: Runtime attempt to change entry type |
| 323 | + - Solution: Use consistent types or BT::Any |
| 324 | + |
| 325 | +3. **"The port with name X and value Y can not be converted to Z"** |
| 326 | + - Cause: Literal value cannot be parsed to port type |
| 327 | + - Solution: Fix value format or add `convertFromString` specialization |
| 328 | + |
| 329 | +## References |
| 330 | + |
| 331 | +- Source: `include/behaviortree_cpp/basic_types.h` - Type system definitions |
| 332 | +- Source: `include/behaviortree_cpp/blackboard.h` - Blackboard type checking |
| 333 | +- Source: `src/xml_parsing.cpp` - Tree creation validation |
| 334 | +- Tests: `tests/gtest_port_type_rules.cpp` - Comprehensive port type rule tests |
| 335 | +- Tests: `tests/gtest_ports.cpp` - Port connection tests |
| 336 | +- Tests: `tests/gtest_blackboard.cpp` - Blackboard tests |
| 337 | +- Tutorial: `examples/t03_generic_ports.cpp` - Custom type example |
0 commit comments