diff --git a/.config/clang-format b/.config/clang-format new file mode 100644 index 0000000..3699417 --- /dev/null +++ b/.config/clang-format @@ -0,0 +1,69 @@ +--- +# C++ Formatting rules for Polymath Code Standard + +# See https://releases.llvm.org/14.0.0/tools/clang/docs/ClangFormatStyleOptions.html for documentation of these options +BasedOnStyle: Google +IndentWidth: 2 +ColumnLimit: 120 + +AccessModifierOffset: -2 +AlignAfterOpenBracket: AlwaysBreak +AlignConsecutiveAssignments: None +AlignConsecutiveDeclarations: None +AlignEscapedNewlines: Left +AlignTrailingComments: false +AllowAllArgumentsOnNextLine: true +AllowAllParametersOfDeclarationOnNextLine: true +AllowShortBlocksOnASingleLine: Empty +AllowShortFunctionsOnASingleLine: false +BinPackArguments: false +BinPackParameters: false +BraceWrapping: + AfterClass: true + AfterControlStatement: MultiLine + AfterEnum: true + AfterFunction: true + AfterNamespace: true + AfterStruct: true + AfterUnion: true + AfterExternBlock: true + BeforeCatch: false + BeforeElse: false + BeforeLambdaBody: false + BeforeWhile: false + IndentBraces: false + SplitEmptyFunction: false + SplitEmptyRecord: false + SplitEmptyNamespace: false +BreakBeforeBraces: Custom +BreakConstructorInitializers: BeforeComma +CompactNamespaces: false +ContinuationIndentWidth: 2 +ConstructorInitializerIndentWidth: 0 +DerivePointerAlignment: false +EmptyLineAfterAccessModifier: Never +EmptyLineBeforeAccessModifier: LogicalBlock +FixNamespaceComments: true +IncludeBlocks: Regroup +IncludeCategories: + # Headers in <> with .h extension (best guess at C system headers) + - Regex: '<([A-Za-z0-9\Q/-_\E])+\.h>' + Priority: 1 + # Headers in <> without extension (C++ system headers) + - Regex: '<([A-Za-z0-9\Q/-_\E])+>' + Priority: 2 + # Headers in <> with other extensions. + - Regex: '<([A-Za-z0-9.\Q/-_\E])+>' + Priority: 3 + # Headers in "" + - Regex: '"([A-Za-z0-9.\Q/-_\E])+"' + Priority: 4 +IndentAccessModifiers: false +IndentPPDirectives: BeforeHash +PackConstructorInitializers: Never +PointerAlignment: Middle +ReferenceAlignment: Middle +ReflowComments: false +SeparateDefinitionBlocks: Always +SortIncludes: CaseInsensitive +SpacesBeforeTrailingComments: 2 diff --git a/.config/copyright.txt b/.config/copyright.txt new file mode 100644 index 0000000..82773ca --- /dev/null +++ b/.config/copyright.txt @@ -0,0 +1,13 @@ +Copyright (c) 2025-present Polymath Robotics, Inc. + +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. diff --git a/.cpplint.cfg b/.cpplint.cfg new file mode 100644 index 0000000..8f355ec --- /dev/null +++ b/.cpplint.cfg @@ -0,0 +1,19 @@ +# Because of cpplint's config file assumptions, this can't be contained in .config/ directory +linelength=256 + +# TODO(emerson) we need to apply a copyright check, maybe not via this tool though +filter=-legal/copyright +# TODO(emerson) we want these, but the style as enforced here is probably not quite right for us +filter=-build/header_guard +filter=-build/c++17 + +# Per our style guide, we want to allow the use of non-const reference passing for output parameters +filter=-runtime/references + +# Disable all formatting checks, this is handled by clang-format +filter=-readability/braces +filter=-whitespace/braces +filter=-whitespace/indent +filter=-whitespace/newline +filter=-whitespace/parens +filter=-whitespace/semicolon diff --git a/.github/workflows/build.yml b/.github/workflows/build.yml new file mode 100644 index 0000000..6a3a78f --- /dev/null +++ b/.github/workflows/build.yml @@ -0,0 +1,39 @@ +--- +name: Build and test +"on": + pull_request: + push: + branches: + - main + +jobs: + build_and_test: + runs-on: ubuntu-latest + strategy: + fail-fast: false + matrix: + ros: [humble, jazzy, kilted, rolling] + package: [behaviortree_cpp_pluginlib, behaviortree_cpp_pluginlib_tests] + include: + - ros: humble + ubuntu: jammy + - ros: jazzy + ubuntu: noble + - ros: kilted + ubuntu: noble-testing + - ros: rolling + ubuntu: noble + name: ${{ matrix.ros }} - ${{ matrix.package }} + container: + image: ghcr.io/ros-tooling/setup-ros-docker/setup-ros-docker-ubuntu-${{ matrix.ubuntu }}:latest + steps: + - uses: actions/checkout@v4 + - uses: ros-tooling/action-ros-ci@v0.4 + with: + target-ros2-distro: ${{ matrix.ros }} + package-name: ${{ matrix.package }} + import-token: ${{ secrets.GITHUB_TOKEN }} + - uses: actions/upload-artifact@v4 + with: + name: colcon-logs__${{ matrix.package }}__${{ matrix.ros }} + path: ros_ws/log diff --git a/.github/workflows/pre-commit.yml b/.github/workflows/pre-commit.yml new file mode 100644 index 0000000..45969be --- /dev/null +++ b/.github/workflows/pre-commit.yml @@ -0,0 +1,16 @@ +--- +name: pre-commit +"on": + pull_request: + push: + branches: + - main + +jobs: + pre-commit: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-python@v5 + - run: sudo apt-get update && sudo apt-get install libxml2-utils + - uses: pre-commit/action@v3.0.1 diff --git a/.pre-commit-config.yaml b/.pre-commit-config.yaml new file mode 100644 index 0000000..97e6be8 --- /dev/null +++ b/.pre-commit-config.yaml @@ -0,0 +1,68 @@ +--- +# See https://pre-commit.com for more information on these settings +repos: + # Generally useful checks provided by pre-commit + - repo: https://github.com/pre-commit/pre-commit-hooks + rev: v5.0.0 + hooks: + - id: check-added-large-files + - id: check-ast + - id: check-case-conflict + - id: check-merge-conflict + - id: check-shebang-scripts-are-executable + - id: check-symlinks + - id: check-xml + - id: end-of-file-fixer + - id: forbid-submodules + - id: mixed-line-ending + - id: trailing-whitespace + # C++ formatting + - repo: https://github.com/pre-commit/mirrors-clang-format + rev: v19.1.7 + hooks: + - id: clang-format + args: ["--style=file:.config/clang-format"] + # C++ linting + - repo: https://github.com/cpplint/cpplint + rev: 2.0.0 + hooks: + - id: cpplint + args: ["--config=.cpplint.cfg", --quiet, --output=sed] + # Markdown + - repo: https://github.com/jackdewinter/pymarkdown + rev: v0.9.28 + hooks: + - id: pymarkdown + args: [-d, MD013, fix] + # XML + - repo: https://github.com/emersonknapp/ament_xmllint + rev: v0.1 + hooks: + - id: ament_xmllint + # CMake + - repo: https://github.com/cmake-lint/cmake-lint + rev: 1.4.3 + hooks: + - id: cmakelint + args: [--linelength=140] + # License headers + - repo: https://github.com/Lucas-C/pre-commit-hooks + rev: v1.5.5 + hooks: + - id: insert-license + types_or: [cmake] + name: Copyright headers for Python/CMake + args: [ + --license-filepath, .config/copyright.txt, + --comment-style, '#', + --allow-past-years, + --no-extra-eol, + ] + - id: insert-license + types_or: [c++, c] + name: Copyright headers for C/C++ + args: [ + --license-filepath, .config/copyright.txt, + --comment-style, '//', + --allow-past-years, + ] diff --git a/.ruff.toml b/.ruff.toml new file mode 100644 index 0000000..e0801ad --- /dev/null +++ b/.ruff.toml @@ -0,0 +1,20 @@ +# Python rules for Polymath Code Standard + +line-length = 120 +indent-width = 4 + +[format] +preview = true +quote-style = "single" +indent-style = "space" +skip-magic-trailing-comma = false +line-ending = "lf" + +[lint] +select = ["E4", "E7", "E9", "F", "I", "PTH"] +# Rules intended for future application +# select = ["N", "D", "C90", "UP", "PERF", "RUF"] +ignore = [] +fixable = ["ALL"] +unfixable = [] +dummy-variable-rgx = "^(_+|(_+[a-zA-Z0-9_]*[a-zA-Z0-9]+?))$" diff --git a/README.md b/README.md index f18c78e..1fb1e07 100644 --- a/README.md +++ b/README.md @@ -1,2 +1,11 @@ # behaviortree_cpp_pluginlib -Pluginlib integration for registering and loading BehaviorTree.CPP extensions automatically + +Pluginlib integration for registering and loading BehaviorTree.CPP extensions automatically. + +This allows a workflow where any number of BT node plugins can be exposed in a distributed manner, with a single CMake call, so that no centralized plugin-exposure mechanism is needed to be kept updated. + +The use of pluginlib enables discovering all these nodes automatic automatically at runtime, without having to provide a list ahead of time to the factory. + +The packages in this repository are: +* [behaviortree_cpp_pluginlib](./behaviortree_cpp_pluginlib/) - main implementation, see here for all information +* [behaviortree_cpp_pluginlib_tests](./behaviortree_cpp_pluginlib_tests/) - separated test package to make sure all exposed build-system tools are working as intended for a user diff --git a/behaviortree_cpp_pluginlib/CMakeLists.txt b/behaviortree_cpp_pluginlib/CMakeLists.txt new file mode 100644 index 0000000..c186f6d --- /dev/null +++ b/behaviortree_cpp_pluginlib/CMakeLists.txt @@ -0,0 +1,95 @@ +# Copyright (c) 2025-present Polymath Robotics, Inc. +# +# 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. + +cmake_minimum_required(VERSION 3.22) +project(behaviortree_cpp_pluginlib) + +if(NOT CMAKE_C_STANDARD) + set(CMAKE_C_STANDARD 99) +endif() +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) + add_link_options(-Wl,-no-undefined) +endif() + +find_package(ament_cmake_auto REQUIRED) +ament_auto_find_build_dependencies() + +# Create library +add_library(${PROJECT_NAME} SHARED + src/factory.cpp +) +target_include_directories(${PROJECT_NAME} PUBLIC + $ + $ +) +target_link_libraries(${PROJECT_NAME} + PUBLIC + behaviortree_cpp::behaviortree_cpp + pluginlib::pluginlib + PRIVATE + rcutils::rcutils +) + +# Install and export resources +install( + TARGETS ${PROJECT_NAME} + EXPORT ${PROJECT_NAME}_TARGETS + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin +) +install( + EXPORT ${PROJECT_NAME}_TARGETS + NAMESPACE ${PROJECT_NAME}:: + DESTINATION share/${PROJECT_NAME}/cmake +) +install( + FILES src/autoregistrar.cpp.in src/plugin_description.xml.in + DESTINATION share/${PROJECT_NAME} +) +install( + DIRECTORY include/ + DESTINATION include +) +install( + DIRECTORY cmake + DESTINATION share/${PROJECT_NAME} +) + +if(BUILD_TESTING) + ament_auto_find_test_dependencies() + + set(arg_TARGET test_register) + configure_file( + src/autoregistrar.cpp.in + ${PROJECT_BINARY_DIR}/test/autoregistrar.cpp + @ONLY + ) + + ament_add_gtest(test_register + test/test_register.cpp + ${PROJECT_BINARY_DIR}/test/autoregistrar.cpp + ) + target_link_libraries(test_register ${PROJECT_NAME}) +endif() + +ament_export_targets(${PROJECT_NAME}_TARGETS HAS_LIBRARY_TARGET) +ament_export_dependencies(behaviortree_cpp pluginlib) +ament_package( + CONFIG_EXTRAS "${PROJECT_NAME}-extras.cmake" +) diff --git a/behaviortree_cpp_pluginlib/DEVELOPING.md b/behaviortree_cpp_pluginlib/DEVELOPING.md new file mode 100644 index 0000000..db46500 --- /dev/null +++ b/behaviortree_cpp_pluginlib/DEVELOPING.md @@ -0,0 +1,21 @@ +# Implementation Details + +## BT Plugin Base Class + +[plugin.hpp](./include/behaviortree_cpp_pluginlib/plugin.hpp) defines the abstract base class `BT::BehaviorTreePlugin` which has a no-argument constructor and so can be exposed to `pluginlib`. + +Prerequisites for `pluginlib`: +1. Plugins must derive from a known base class and be able to be used polymorphically +1. Plugins must provide a no-argument constructor + +## C++ Registration Macros + +In [register_macro.hpp](./include/behaviortree_cpp_pluginlib/register_macro.hpp) we provide the macros needed to register an arbitrary number of plugin entrypoints from a single library. + +See the doc comments in that file for the breakdown of the implementation. + +## CMake Macro + +Follow the logic in [register_behaviortree_cpp_plugin.cmake](./cmake/register_behaviortree_cpp_plugin.cmake) for the implementation details. High level: +1. Add `autoregistrar.cpp` to the library target +1. Create `plugin_description.xml` file and register it with the resource index for `pluginlib` to find diff --git a/behaviortree_cpp_pluginlib/README.md b/behaviortree_cpp_pluginlib/README.md new file mode 100644 index 0000000..bae61d9 --- /dev/null +++ b/behaviortree_cpp_pluginlib/README.md @@ -0,0 +1,95 @@ +# BehaviorTree.CPP Plugins + +This package provides a set of tools that allows users to easily export plugins for `behaviortree_cpp`, such as new types of BT Nodes. + +Under the hood, it uses ROS' `pluginlib` to allow for discovery of plugins without hardcoding of library names and classes - which the patterns provided by `behaviortree_cpp` and `navigation2` currently require. + +# Usage + +There are two usage patterns - the plugin provider and the plugin consumer. + +## Registering Plugins + +Simple as 1, 2, 3: depend on this package, register your library as a plugin provider, call registration macro on plugin classes. + +1. `package.xml` - you need only a build dependency on this package + + ```xml + ... + behaviortree_cpp_pluginlib + ... + ``` + +2. `CMakeLists.txt` - find the package, link against its library target, and call custom macro to register your target as containing plugins + + ```cmake + find_package(behaviortree_cpp_pluginlib REQUIRED) + + ... + add_library(my_plugin_library + src/source1.cpp + ... + ) + target_link_libraries(my_plugin_library + behaviortree_cpp_pluginlib::plugin + ... + ) + register_behaviortree_cpp_plugin(my_plugin_library) + ... + ``` + +3. C++ sources (note: registration needs to happen in compilation unit, not in exposed header!) + + ```c++ + #include "behaviortree_cpp_pluginlib/register_macro.hpp + + ... + + namespace my_namespace + { + class MyActionNodeClass : public BT::SyncActionNode + { + ... + }; + } // namespace my_namespace + + ... + + BT_REGISTER_PLUGIN_NODE(MyActionNodeName, my_namespace::MyActionNodeClass) + ``` + +4. Also note a more generic exposure mode. `BT_REGISTER_PLUGIN_NODE` is provided for simple use in the average case, but if you have other exposures to do, you can use: + + ``` + BT_PLUGIN_REGISTER(factory) + { + // factory will be a BT::BehaviorTreeFactory & + factory.ArbitraryMethodCalls(); + } + ``` + +## Loading Plugins + +To load all registered plugins, link against the exported library target and use the `BT::PluginAwareFactory` + +1. `CMakeLists.txt` - find and link against the loader + + ```cmake + target_link_libraries(my_loader_lib + behaviortree_cpp_pluginlib::behaviortree_cpp_pluginlib + ... + ) + ``` + +2. Create your factory and load all plugins with the provided function: + + ```c++ + #include "behaviortree_cpp_pluginlib/factory.hpp" + ... + BT::PluginAwareFactory factory; + ... + ``` + +# Implementation Details + +For more information about what's happening under the hood to enable these usage patterns, see [DEVELOPING.md](./DEVELOPING.md) diff --git a/behaviortree_cpp_pluginlib/behaviortree_cpp_pluginlib-extras.cmake b/behaviortree_cpp_pluginlib/behaviortree_cpp_pluginlib-extras.cmake new file mode 100644 index 0000000..3eff6b9 --- /dev/null +++ b/behaviortree_cpp_pluginlib/behaviortree_cpp_pluginlib-extras.cmake @@ -0,0 +1,15 @@ +# Copyright (c) 2025-present Polymath Robotics, Inc. +# +# 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. + +include("${behaviortree_cpp_pluginlib_DIR}/register_behaviortree_cpp_plugin.cmake") diff --git a/behaviortree_cpp_pluginlib/cmake/register_behaviortree_cpp_plugin.cmake b/behaviortree_cpp_pluginlib/cmake/register_behaviortree_cpp_plugin.cmake new file mode 100644 index 0000000..24865b4 --- /dev/null +++ b/behaviortree_cpp_pluginlib/cmake/register_behaviortree_cpp_plugin.cmake @@ -0,0 +1,60 @@ +# Copyright (c) 2025-present Polymath Robotics, Inc. +# +# 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. + +# +# Register a library target as providing BehaviorTree.CPP plugins +# +# Example usage: +# register_behaviortree_cpp_plugin(my_library) +# +# :param TARGET: name of a valid CMake shared library target to export plugins from +# :type TARGET: string +# +# @public +# +function(register_behaviortree_cpp_plugin arg_TARGET) + if(NOT arg_TARGET) + message(FATAL_ERROR "register_behaviortree_cpp_plugin() called without TARGET argument") + endif() + + get_filename_component(template_src "${behaviortree_cpp_pluginlib_DIR}" DIRECTORY) + set(template_dest ${PROJECT_BINARY_DIR}/behaviortree_cpp_pluginlib/${arg_TARGET}) + + # Define the AutoRegistrar plugin implementation class that registers the declared BT extensions + configure_file( + ${template_src}/autoregistrar.cpp.in + ${template_dest}__autoregistrar.cpp + @ONLY + ) + target_sources("${arg_TARGET}" PRIVATE + ${template_dest}__autoregistrar.cpp + ) + + # Create the pluginlib XML file to expose the above AutoRegistrar to pluginlib's ClassLoader + configure_file( + ${template_src}/plugin_description.xml.in + ${template_dest}__btcpp_plugin_description.xml + @ONLY + ) + install(FILES ${template_dest}__btcpp_plugin_description.xml DESTINATION share/${PROJECT_NAME}) + + # Mimics `pluginlib_export_plugin_description_file`, which assumes a file in CMAKE_CURRENT_SOURCE_DIR + # Can't override by setting CMAKE_CURRENT_SOURCE_DIR because `install()` in that context needs a relative filename + set(__PLUGINLIB_CATEGORY_CONTENT__behaviortree_cpp + "${__PLUGINLIB_CATEGORY_CONTENT__behaviortree_cpp}share/${PROJECT_NAME}/${arg_TARGET}__btcpp_plugin_description.xml\n" + PARENT_SCOPE + ) + list(APPEND __PLUGINLIB_PLUGIN_CATEGORIES "behaviortree_cpp") + set(__PLUGINLIB_PLUGIN_CATEGORIES "${__PLUGINLIB_PLUGIN_CATEGORIES}" PARENT_SCOPE) +endfunction() diff --git a/behaviortree_cpp_pluginlib/include/behaviortree_cpp_pluginlib/factory.hpp b/behaviortree_cpp_pluginlib/include/behaviortree_cpp_pluginlib/factory.hpp new file mode 100644 index 0000000..ffc411b --- /dev/null +++ b/behaviortree_cpp_pluginlib/include/behaviortree_cpp_pluginlib/factory.hpp @@ -0,0 +1,42 @@ +// Copyright (c) 2025-present Polymath Robotics, Inc. +// +// 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. +#pragma once + +#include +#include + +#include "behaviortree_cpp/bt_factory.h" +#include "behaviortree_cpp_pluginlib/plugin.hpp" +#include "pluginlib/class_loader.hpp" + +namespace BT +{ + +/// +/// @class BT::PluginAwareFactory +/// @brief Factory specialization that automatically loads all available plugins on construction. +/// +class PluginAwareFactory : public BT::BehaviorTreeFactory +{ +public: + /// @brief Construct a BehaviorTreeFactory with all available plugins loaded + /// @param plugin_xmls Optional list of plugin XML files to load. If empty, will use pluginlib to discover plugins. + explicit PluginAwareFactory(const std::vector & plugin_xmls = {}); + virtual ~PluginAwareFactory(); + +private: + pluginlib::ClassLoader loader_; +}; + +} // namespace BT diff --git a/behaviortree_cpp_pluginlib/include/behaviortree_cpp_pluginlib/plugin.hpp b/behaviortree_cpp_pluginlib/include/behaviortree_cpp_pluginlib/plugin.hpp new file mode 100644 index 0000000..9fb4526 --- /dev/null +++ b/behaviortree_cpp_pluginlib/include/behaviortree_cpp_pluginlib/plugin.hpp @@ -0,0 +1,32 @@ +// Copyright (c) 2025-present Polymath Robotics, Inc. +// +// 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. +#pragma once + +#include "behaviortree_cpp/bt_factory.h" + +namespace BT +{ +/// @brief Interface class that lets us expose a pluginlib plugin with arbitrary BehaviorTree extensions. +/// Normally do not subclass directly, instead call BT_REGISTER_PLUGIN with a function body for register +class BehaviorTreePlugin +{ +public: + /// @brief Empty no-argument constructor for pluginlib loading. + BehaviorTreePlugin() = default; + + /// @brief Abitrarily modify a factory, with the intention of adding new types as plugins. + /// @param factory Factory to modify. + virtual void registerTypes(BT::BehaviorTreeFactory & factory) = 0; +}; +} // namespace BT diff --git a/behaviortree_cpp_pluginlib/include/behaviortree_cpp_pluginlib/register_macro.hpp b/behaviortree_cpp_pluginlib/include/behaviortree_cpp_pluginlib/register_macro.hpp new file mode 100644 index 0000000..55c8431 --- /dev/null +++ b/behaviortree_cpp_pluginlib/include/behaviortree_cpp_pluginlib/register_macro.hpp @@ -0,0 +1,71 @@ +// Copyright (c) 2025-present Polymath Robotics, Inc. +// +// 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. +#pragma once + +#include + +#include "behaviortree_cpp/bt_factory.h" + +namespace BT +{ +using PluginRegisterFunction = void (*)(BT::BehaviorTreeFactory &); + +/// @brief Forward declare this function to get a global registration function list, +/// which must be implemented in plugin loader libraries. +/// This is autogenerated by the AutoPluginRegistrar created by the CMake macros. +std::vector & get_plugin_register_functions(); +} // namespace BT + +/// @brief Implementation detail for plugin registration. +/// @details +/// Generates a static registration function, +/// a uniquely named struct in an anonymous namespace, +/// and creates an instance of that struct in place. +/// The constructor of the struct is called by the static instance, which is created on loading the library. +/// That constructor adds the plugin registration function to a static global vector that is declared here, +/// but defined by autogenerated code in plugin provider libraries. +/// Automatically filling that vector of plugin registration functions is the end result of these macros. +#define BT_PLUGIN_REGISTER_IMPL(factory_param, UniqueID) \ + static void bt_plugin_register_fn_##UniqueID(BT::BehaviorTreeFactory & factory_param); \ + namespace \ + { \ + struct bt_plugin_register_struct_##UniqueID \ + { \ + bt_plugin_register_struct_##UniqueID() \ + { \ + BT::get_plugin_register_functions().push_back(&bt_plugin_register_fn_##UniqueID); \ + } \ + } bt_plugin_register_instance_##UniqueID; \ + } \ + static void bt_plugin_register_fn_##UniqueID(BT::BehaviorTreeFactory & factory_param) + +#define BT_PLUGIN_REGISTER_HOP1(factory_param, UniqueID) BT_PLUGIN_REGISTER_IMPL(factory_param, UniqueID) + +/// @brief Arbitrarily modify the BehaviorTreeFactory at will by providing a function body +/// @param factory_param The name that the factory will be given to you as +/// @example +/// BT_PLUGIN_REGISTER(factory) +/// { +/// factory.registerNodeType("MyNode"); +/// } +#define BT_PLUGIN_REGISTER(factory_param) BT_PLUGIN_REGISTER_HOP1(factory_param, __COUNTER__) + +/// @brief Call this macro to register a single BT Node with as little boilerplate as possible +/// @param node_name The name that the exposed BT node will be used by, e.g. +/// @param node_class The fully qualified class name that implements the node, e.g. my_namespace::MyNodeClass +#define BT_PLUGIN_REGISTER_NODE(node_name, node_class) \ + BT_PLUGIN_REGISTER(factory) \ + { \ + factory.registerNodeType(#node_name); \ + } diff --git a/behaviortree_cpp_pluginlib/package.xml b/behaviortree_cpp_pluginlib/package.xml new file mode 100644 index 0000000..f7d796e --- /dev/null +++ b/behaviortree_cpp_pluginlib/package.xml @@ -0,0 +1,23 @@ + + + + behaviortree_cpp_pluginlib + 0.0.0 + Tools enabling users to define BehaviorTree.CPP extensions using pluginlib. + Polymath Robotics Engineering + Apache-2.0 + Emerson Knapp + + ament_cmake + ament_cmake_auto + + behaviortree_cpp + pluginlib + rcutils + + ament_cmake_gtest + + + ament_cmake + + diff --git a/behaviortree_cpp_pluginlib/src/autoregistrar.cpp.in b/behaviortree_cpp_pluginlib/src/autoregistrar.cpp.in new file mode 100644 index 0000000..20199f3 --- /dev/null +++ b/behaviortree_cpp_pluginlib/src/autoregistrar.cpp.in @@ -0,0 +1,31 @@ +// Copyright (c) 2025-present Polymath Robotics, Inc. All rights reserved +// Proprietary. Any unauthorized copying, distribution, or modification of this software is strictly prohibited. + +#include "behaviortree_cpp/bt_factory.h" +#include "behaviortree_cpp_pluginlib/plugin.hpp" +#include "pluginlib/class_list_macros.hpp" + +namespace BT { +/// @brief Implements forward-declared get_plugin_register_functions +/// Owns a global static vector of functions, and returns mutable reference to it. +std::vector& get_plugin_register_functions() { + static std::vector registry; + return registry; +} +} // namespace BT + +namespace @arg_TARGET@::_BT_PLUGINS_IMPL +{ +/// @brief Implements the single plugin class for a given library to expose to pluginlib +class AutoPluginRegistrar : public BT::BehaviorTreePlugin { +public: + /// @brief Simply iterates over the static function list, which was populated at library load + void registerTypes(BT::BehaviorTreeFactory& factory) override { + for (auto& fn : BT::get_plugin_register_functions()) { + fn(factory); + } + } +}; +} // namespace @arg_TARGET@::_BT_PLUGINS_IMPL + +PLUGINLIB_EXPORT_CLASS(@arg_TARGET@::_BT_PLUGINS_IMPL::AutoPluginRegistrar, BT::BehaviorTreePlugin) diff --git a/behaviortree_cpp_pluginlib/src/factory.cpp b/behaviortree_cpp_pluginlib/src/factory.cpp new file mode 100644 index 0000000..bac0bc7 --- /dev/null +++ b/behaviortree_cpp_pluginlib/src/factory.cpp @@ -0,0 +1,63 @@ +// Copyright (c) 2025-present Polymath Robotics, Inc. +// +// 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. + +#include "behaviortree_cpp_pluginlib/factory.hpp" + +#include +#include +#include + +#include "behaviortree_cpp/bt_factory.h" +#include "behaviortree_cpp_pluginlib/plugin.hpp" +#include "rcutils/logging_macros.h" + +namespace BT +{ + +PluginAwareFactory::PluginAwareFactory(const std::vector & plugin_xmls) +: BT::BehaviorTreeFactory() +, loader_("behaviortree_cpp", "BT::BehaviorTreePlugin", "plugin", plugin_xmls) +{ + RCUTILS_LOG_INFO_NAMED("behaviortree_cpp_pluginlib", "Created classloader for BT::BehaviorTreePlugin"); + for (const std::string & class_name : loader_.getDeclaredClasses()) { + std::shared_ptr plugin = loader_.createSharedInstance(class_name); + std::string class_library_path = loader_.getClassLibraryPath(class_name); + RCUTILS_LOG_INFO_NAMED( + "behaviortree_cpp_pluginlib", + "Loaded plugin class %s from library %s", + class_name.c_str(), + class_library_path.c_str()); + plugin->registerTypes(*this); + } +} + +PluginAwareFactory::~PluginAwareFactory() +{ + // First grab all the IDs, since unregistering them modifies the map and invalidates iterators + std::vector ids_to_unregister; + for (const auto & [id, _] : builders()) { + ids_to_unregister.push_back(id); + } + // Now unregister all builders from the underlying factory, to make sure objects from classloader + // loaded libraries are destroyed before the classloader itself is destroyed. + for (const auto & id : ids_to_unregister) { + try { + unregisterBuilder(id); + } catch (const BT::LogicError & e) { + // Thrown when trying to unregister builtin IDs, but that's fine, just skip it. + } + } +} + +} // namespace BT diff --git a/behaviortree_cpp_pluginlib/src/plugin_description.xml.in b/behaviortree_cpp_pluginlib/src/plugin_description.xml.in new file mode 100644 index 0000000..5c6d92c --- /dev/null +++ b/behaviortree_cpp_pluginlib/src/plugin_description.xml.in @@ -0,0 +1,8 @@ + + + + Automatically generated BehaviorTree.CPP plugin registration class for "@PROJECT_NAME@" project's target "@arg_TARGET@" + + diff --git a/behaviortree_cpp_pluginlib/test/test_register.cpp b/behaviortree_cpp_pluginlib/test/test_register.cpp new file mode 100644 index 0000000..0230180 --- /dev/null +++ b/behaviortree_cpp_pluginlib/test/test_register.cpp @@ -0,0 +1,68 @@ +// Copyright (c) 2025-present Polymath Robotics, Inc. +// +// 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. + +#include + +#include "behaviortree_cpp_pluginlib/register_macro.hpp" +#include "gtest/gtest.h" + +namespace btplugin::testing +{ +class CustomNode1 : public BT::SyncActionNode +{ +public: + CustomNode1(const std::string & bt_node_name, const BT::NodeConfig & conf) + : BT::SyncActionNode(bt_node_name, conf) + {} + + static BT::PortsList providedPorts() + { + return BT::PortsList{}; + } + + BT::NodeStatus tick() override + { + return BT::NodeStatus::SUCCESS; + } +}; + +using CustomNode2 = CustomNode1; +using CustomNode3 = CustomNode2; + +} // namespace btplugin::testing + +BT_PLUGIN_REGISTER_NODE(CustomNode1, btplugin::testing::CustomNode1) + +BT_PLUGIN_REGISTER(factory) +{ + factory.registerNodeType("CustomNode2"); + factory.registerNodeType("CustomNode3"); +} + +TEST(Registration, MacrosBasicUse) +{ + BT::BehaviorTreeFactory factory; + + auto plugin_fns = BT::get_plugin_register_functions(); + ASSERT_EQ(plugin_fns.size(), 2); + for (const auto & plugin_fn : plugin_fns) { + plugin_fn(factory); + } + + auto builders = factory.builders(); + ASSERT_NO_THROW(builders.at("CustomNode1")); + ASSERT_NO_THROW(builders.at("CustomNode2")); + ASSERT_NO_THROW(builders.at("CustomNode3")); + ASSERT_THROW(builders.at("NonexistentNode"), std::out_of_range); +} diff --git a/behaviortree_cpp_pluginlib_tests/CMakeLists.txt b/behaviortree_cpp_pluginlib_tests/CMakeLists.txt new file mode 100644 index 0000000..b281899 --- /dev/null +++ b/behaviortree_cpp_pluginlib_tests/CMakeLists.txt @@ -0,0 +1,59 @@ +# Copyright (c) 2025-present Polymath Robotics, Inc. +# +# 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. + +cmake_minimum_required(VERSION 3.22) +project(behaviortree_cpp_pluginlib_tests) + +if(NOT CMAKE_C_STANDARD) + set(CMAKE_C_STANDARD 99) +endif() +if(NOT CMAKE_CXX_STANDARD) + set(CMAKE_CXX_STANDARD 17) +endif() +if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang") + add_compile_options(-Wall -Wextra -Wpedantic) + add_link_options(-Wl,-no-undefined) +endif() + +find_package(ament_cmake_auto REQUIRED) +ament_auto_find_build_dependencies() + +if(BUILD_TESTING) + ament_auto_find_test_dependencies() + + # Create test libraries + add_library(test_plugin_a SHARED test/plugin_a.cpp) + target_link_libraries(test_plugin_a PUBLIC behaviortree_cpp_pluginlib::behaviortree_cpp_pluginlib) + register_behaviortree_cpp_plugin(test_plugin_a) + + add_library(test_plugin_b SHARED test/plugin_b.cpp) + target_link_libraries(test_plugin_b PRIVATE behaviortree_cpp_pluginlib::behaviortree_cpp_pluginlib) + register_behaviortree_cpp_plugin(test_plugin_b) + + # In the special case of a test-only package that isn't normally installed on target systems, + # we may install the test targets to run install-space testing on them (pluginlib loading) + install( + TARGETS test_plugin_a test_plugin_b + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + RUNTIME DESTINATION bin + ) + + ament_add_gtest(test_factory test/test_factory.cpp) + target_link_libraries(test_factory + behaviortree_cpp_pluginlib::behaviortree_cpp_pluginlib + ) +endif() + +ament_package() diff --git a/behaviortree_cpp_pluginlib_tests/package.xml b/behaviortree_cpp_pluginlib_tests/package.xml new file mode 100644 index 0000000..95ab44a --- /dev/null +++ b/behaviortree_cpp_pluginlib_tests/package.xml @@ -0,0 +1,20 @@ + + + + behaviortree_cpp_pluginlib_tests + 0.0.0 + Testing install-space artifacts from behaviortree_cpp_pluginlib + Polymath Robotics Engineering + Apache-2.0 + Emerson Knapp + + ament_cmake + ament_cmake_auto + + ament_cmake_gtest + behaviortree_cpp_pluginlib + + + ament_cmake + + diff --git a/behaviortree_cpp_pluginlib_tests/test/plugin_a.cpp b/behaviortree_cpp_pluginlib_tests/test/plugin_a.cpp new file mode 100644 index 0000000..fca7c73 --- /dev/null +++ b/behaviortree_cpp_pluginlib_tests/test/plugin_a.cpp @@ -0,0 +1,46 @@ + +// Copyright (c) 2025-present Polymath Robotics, Inc. +// +// 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. + +#include + +#include "behaviortree_cpp_pluginlib/register_macro.hpp" + +namespace testing::plugin_a +{ + +class CustomNodeA1 : public BT::SyncActionNode +{ +public: + CustomNodeA1(const std::string & bt_node_name, const BT::NodeConfig & conf) + : BT::SyncActionNode(bt_node_name, conf) + {} + + static BT::PortsList providedPorts() + { + return BT::PortsList{}; + } + + BT::NodeStatus tick() override + { + return BT::NodeStatus::SUCCESS; + } +}; + +using CustomNodeA2 = CustomNodeA1; + +} // namespace testing::plugin_a + +BT_PLUGIN_REGISTER_NODE(CustomNodeA1, testing::plugin_a::CustomNodeA1) +BT_PLUGIN_REGISTER_NODE(CustomNodeA2, testing::plugin_a::CustomNodeA2) diff --git a/behaviortree_cpp_pluginlib_tests/test/plugin_b.cpp b/behaviortree_cpp_pluginlib_tests/test/plugin_b.cpp new file mode 100644 index 0000000..5d29e9e --- /dev/null +++ b/behaviortree_cpp_pluginlib_tests/test/plugin_b.cpp @@ -0,0 +1,48 @@ +// Copyright (c) 2025-present Polymath Robotics, Inc. +// +// 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. + +#include + +#include "behaviortree_cpp_pluginlib/register_macro.hpp" + +namespace testing::plugin_b +{ + +class CustomNodeB1 : public BT::SyncActionNode +{ +public: + CustomNodeB1(const std::string & bt_node_name, const BT::NodeConfig & conf) + : BT::SyncActionNode(bt_node_name, conf) + {} + + static BT::PortsList providedPorts() + { + return BT::PortsList{}; + } + + BT::NodeStatus tick() override + { + return BT::NodeStatus::SUCCESS; + } +}; + +using CustomNodeB2 = CustomNodeB1; + +} // namespace testing::plugin_b + +BT_PLUGIN_REGISTER(factory) +{ + factory.registerNodeType("CustomNodeB1"); + factory.registerNodeType("CustomNodeB2"); +} diff --git a/behaviortree_cpp_pluginlib_tests/test/test_factory.cpp b/behaviortree_cpp_pluginlib_tests/test/test_factory.cpp new file mode 100644 index 0000000..9164541 --- /dev/null +++ b/behaviortree_cpp_pluginlib_tests/test/test_factory.cpp @@ -0,0 +1,31 @@ +// Copyright (c) 2025-present Polymath Robotics, Inc. +// +// 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. + +#include +#include +#include + +#include "behaviortree_cpp_pluginlib/factory.hpp" +#include "gtest/gtest.h" + +TEST(Factory, AutofactoryEndToEnd) +{ + BT::PluginAwareFactory factory; + const auto & builders = factory.builders(); + ASSERT_NO_THROW(builders.at("CustomNodeA1")); + ASSERT_NO_THROW(builders.at("CustomNodeA2")); + ASSERT_NO_THROW(builders.at("CustomNodeB1")); + ASSERT_NO_THROW(builders.at("CustomNodeB2")); + ASSERT_THROW(builders.at("NonexistentNode"), std::out_of_range); +}