Skip to main content
Basis ships a Python code generator (python/unit/generate_unit.py) that reads a .unit.yaml file and emits a strongly-typed C++ base class for your unit. You implement only the handler functions — all pub-sub wiring, synchronizer setup, publisher/subscriber creation, and topic-name template resolution are generated automatically.

How generation works

The generator performs these steps in order:
1

Validate the YAML

The unit YAML is loaded with PyYAML and validated against unit/schema.yaml using jsonschema. Invalid YAML is rejected before any files are written.
2

Derive C++ names

Topic names (e.g. /camera_left) are converted to valid C++ identifiers by replacing every non-alphanumeric character with an underscore and stripping leading underscores. /camera_left becomes camera_left.The serializer:CppType type field is split on the first :. The serializer prefix is used to select the right #include and the C++ type undergoes .:: substitution for protobuf packages.
3

Render Jinja2 templates

Each file is produced by a Jinja2 template in python/unit/templates/. The generator uses jinja2.StrictUndefined, so any template variable that is missing causes an error rather than silently expanding to an empty string.
4

Format with clang-format

Every generated file is formatted with clang-format using a minimal style override (KeepEmptyLinesAtTheStartOfBlocks: false, MaxEmptyLinesToKeep: 0).

Running the generator

python3 python/unit/generate_unit.py \
    <path/to/MyUnit.unit.yaml> \
    <output_dir> \
    <source_dir>
ArgumentDescription
unit_definition_filePath to the .unit.yaml file. The base name (without .unit.yaml) is used as the unit name and C++ class name.
output_dirDirectory for generated (do-not-edit) files. CMake uses ${CMAKE_CURRENT_BINARY_DIR}/generated.
source_dirDirectory for user-owned files. The generator writes to include/, src/, and template/ under this path.

Example

python3 python/unit/generate_unit.py \
    stereo_match/stereo_match.unit.yaml \
    stereo_match/build/generated \
    stereo_match

Generated files

The generator writes two categories of files.

Generated (do not edit)

These files live under <output_dir>/unit/<UnitName>/ and are overwritten every time the generator runs. Never edit them directly.
FileDescription
unit_base.hDefines unit::UnitName::Args, unit::UnitName::Base, and all HandlerPubSub subclasses.
unit_base.cppImplements Base::SetupSerializationHelpers(), Base::all_templated_topics, Args::argument_metadatas, and each handler’s PubSub::SetupPubSub().
create_unit.cppExports the CreateUnit C entry point used by the Basis unit loader.
handlers/<HandlerName>.hOne file per handler. Defines Input, Output, Synchronizer, and the PubSub struct.
Additionally, <source_dir>/template/<UnitName>.example.h and <source_dir>/template/<UnitName>.example.cpp are written on every run as annotated reference copies. They are safe to inspect but are regenerated on every run.

User-owned (edit these)

These files are written only once — if they already exist, the generator skips them. They are yours to edit.
FileDescription
<source_dir>/include/<UnitName>.hYour unit class header. Declares the class and lists the handler overrides.
<source_dir>/src/<UnitName>.cppYour unit class implementation. Implement each handler here.
If you add a new handler to the YAML after the initial generation, you must manually add the declaration to include/<UnitName>.h and the implementation stub to src/<UnitName>.cpp. The generator will not overwrite existing user-owned files.

Generated handler signature

For a handler named StereoMatch with inputs /camera_left and /camera_right and output /camera_stereo, the generator produces:
// handlers/StereoMatch.h  (generated — do not edit)
namespace unit::MyUnit::StereoMatch {

struct Input {
    basis::core::MonotonicTime time;
    // /camera_left
    std::shared_ptr<const sensor_msgs::Image> camera_left;
    // /camera_right
    std::shared_ptr<const sensor_msgs::Image> camera_right;
};

struct Output {
    // /camera_stereo
    std::shared_ptr<const example_msgs::StereoImage> camera_stereo;

    basis::HandlerPubSub::TopicMap ToTopicMap();
};
Your implementation in src/MyUnit.cpp receives a const Input& and returns an Output:
// src/MyUnit.cpp  (user-owned)
StereoMatch::Output MyUnit::StereoMatch(const StereoMatch::Input& input) {
    auto result = std::make_shared<example_msgs::StereoImage>();
    // use input.camera_left and input.camera_right
    return StereoMatch::Output{ .camera_stereo = result };
}
input.time is the monotonic clock time at which the synchronizer fired — useful for logging and for deterministic replay. For handlers with accumulate, the input field type is a vector:
// /event_data with accumulate: 10
std::vector<std::shared_ptr<const protobuf::Event>> event_data;

The generated unit class

include/<UnitName>.h starts with:
// include/MyUnit.h  (user-owned — generated once)
#include <unit/MyUnit/unit_base.h>

class MyUnit : public unit::MyUnit::Base {
public:
  MyUnit(const Args& args,
         const std::optional<std::string_view>& name_override = {})
      : unit::MyUnit::Base(args, name_override) {}

  virtual unit::MyUnit::StereoMatch::Output
  StereoMatch(const unit::MyUnit::StereoMatch::Input& input) override;
};
unit::MyUnit::Base inherits from basis::SingleThreadedUnit. Its Initialize() is final — you never override it. All setup is done by Base itself through the generated CreatePublishersSubscribers().

CMake integration

Add the following to your unit’s CMakeLists.txt:
include(${BASIS_SOURCE_ROOT}/cmake/Unit.cmake)

generate_unit(MyUnit
    DEPENDS
        basis::protobuf_serialization  # or other serialization targets
        my_proto_messages
)
generate_unit is a CMake function defined in cmake/Unit.cmake. It:
  1. Registers a custom command that runs generate_unit.py whenever the .unit.yaml or any template changes.
  2. Creates a shared library target unit_MyUnit (output name MyUnit.unit.so) from the generated sources and your src/MyUnit.cpp.
  3. Creates a standalone executable target unit_MyUnit_bin (output name MyUnit) that links the unit library against basis::unit::main.
  4. Defines an alias unit::MyUnit pointing to the shared library.
  5. Installs the .so to unit/, the YAML to unit/, and the binary to bin/.
cmake/Unit.cmake (excerpt)
add_custom_command(
    COMMAND
        ${BASIS_SOURCE_ROOT}/python/unit/generate_unit.py
            ${UNIT_FILE_NAME}
            ${CMAKE_CURRENT_BINARY_DIR}/generated
            ${CMAKE_CURRENT_SOURCE_DIR}
    DEPENDS
        ${BASIS_SOURCE_ROOT}/python/unit/generate_unit.py
        ${CMAKE_CURRENT_SOURCE_DIR}/${UNIT_NAME}.unit.yaml
        # ...all Jinja2 templates...
    OUTPUT
        ${GENERATED_DIR}/unit/${UNIT_NAME}/unit_base.h
        ${GENERATED_DIR}/unit/${UNIT_NAME}/unit_base.cpp
        ${GENERATED_DIR}/unit/${UNIT_NAME}/create_unit.cpp
        # ...
)

add_library(${TARGET_NAME} SHARED
    src/${UNIT_NAME}.cpp
    ${GENERATED_DIR}/unit/${UNIT_NAME}/unit_base.cpp
    ${GENERATED_DIR}/unit/${UNIT_NAME}/create_unit.cpp
)
target_link_libraries(${TARGET_NAME} basis::unit basis::synchronizers ${PARSED_ARGS_DEPENDS})

Rebuilding after YAML changes

Because CMake tracks the .unit.yaml file as a dependency, rebuilding is as simple as:
cmake --build build/
CMake detects the changed YAML, re-runs the generator, and recompiles the affected sources.
If you change only the handler body in src/MyUnit.cpp, only that translation unit is recompiled — the generator is not re-invoked.

File layout summary

my_unit/
├── my_unit.unit.yaml           # source of truth — you edit this
├── include/
│   └── my_unit.h               # user-owned class declaration
├── src/
│   └── my_unit.cpp             # user-owned handler implementations
├── template/
│   ├── my_unit.example.h       # regenerated reference copy
│   └── my_unit.example.cpp     # regenerated reference copy
└── build/generated/unit/my_unit/
    ├── unit_base.h             # generated — do not edit
    ├── unit_base.cpp           # generated — do not edit
    ├── create_unit.cpp         # generated — do not edit
    └── handlers/
        ├── StereoMatch.h       # generated — do not edit
        └── ...

Next steps

Unit YAML reference

All fields you can declare in a .unit.yaml file

Serialization

How the serializer prefix resolves to a plugin

Units

The generated Base class and HandlerPubSub in depth

Launch files

How to run your compiled unit in a launch file

Build docs developers (and LLMs) love