Hello World in C++
This tutorial covers building a pub/sub application using modern C++17 with HDDS.
Prerequisites:
- Rust toolchain (1.75+) -- required to build both the HDDS core library and hddsgen
- C++17 compiler (g++ or clang++), CMake 3.16+
- HDDS C++ SDK built:
make sdk-cxxin the hdds directory - hddsgen code generator (see below)
HDDS is implemented in Rust with C/C++ bindings. The Rust toolchain is needed to:
- Build the core library (
libhdds_c.a) --make sdk-cxxrunscargo buildinternally - Build hddsgen -- the IDL-to-C++ code generator
This is a one-time setup. Once built, hddsgen is a standalone binary and libhdds_c.a / libhdds_cxx.a are standard static libraries -- no Rust runtime dependency.
# Install Rust (if not already installed)
curl --proto '=https' --tlsv1.2 -sSf https://sh.rustup.rs | sh
source ~/.cargo/env
rustc --version # must be 1.75+
Always build hddsgen from the matching source tree. Using an older installed version will produce structurally different headers (missing initializers, different CDR2 layout) that are incompatible with the current SDK.
# Build and install hddsgen
cd /path/to/hdds_gen
cargo install --path .
# Verify version matches your HDDS checkout
hddsgen --version
The HDDS samples ship with pre-generated headers, so you can try the samples first without installing hddsgen.
The recommended C++ workflow uses the typed API: create_writer<T>() / create_reader<T>() with types generated by hddsgen. The typed methods handle CDR2 serialization automatically -- writer.write(msg) and reader.take() just work with hddsgen types.
A raw API (create_writer_raw(), write_raw(), take_raw()) is also available for untyped payloads or custom serialization.
Note on syntax: Typed methods return values (TypedDataWriter<T>), so you use dot syntax: writer.write(msg). Raw methods return std::unique_ptr<DataWriter>, so you use arrow syntax: writer->write_raw(buf, len).
Step 1: Create the IDL File
Create HelloWorld.idl:
struct HelloWorld {
long id;
string message;
};
IDL supports structs, enums, sequences, unions, nested types, and more. See IDL Syntax Reference for the full type system.
This tutorial uses a bare IDL struct for simplicity. The HDDS samples (sdk/samples/) use module hdds_samples { ... }; which wraps types in a C++ namespace -- you'll see using namespace hdds_samples; in sample code. Both approaches work; see the namespace note in Step 2 for details.
Step 2: Generate C++ Code
# -o writes to a file (recommended)
hddsgen gen cpp HelloWorld.idl -o HelloWorld.hpp
# Without -o, output goes to stdout (useful for piping):
# hddsgen gen cpp HelloWorld.idl > HelloWorld.hpp
# Wrap types in a namespace (recommended for larger projects):
# hddsgen gen cpp HelloWorld.idl --namespace-cpp MyProject -o HelloWorld.hpp
Without --namespace-cpp or an IDL module block, types land in the global namespace. This is fine for a tutorial, but will cause name collisions in real projects. Always use one of:
- IDL modules (recommended):
module MyProject { struct HelloWorld { ... }; };-- maps directly to C++ namespaces - CLI flag:
hddsgen gen cpp MyType.idl --namespace-cpp MyProject -o MyType.hpp
A common convention is to place generated headers in a generated/ subdirectory to separate them from hand-written code. The HDDS samples use this pattern:
my_project/
HelloWorld.idl
generated/HelloWorld.hpp # hddsgen output (-o generated/HelloWorld.hpp)
main.cpp # #include "generated/HelloWorld.hpp"
CMakeLists.txt
This generates HelloWorld.hpp containing:
HelloWorldstruct with public member access (.id,.message)encode_cdr2_le(buffer, size)instance method (CDR2 serialization, returns bytes written)decode_cdr2_le(data, size)instance method (CDR2 deserialization, returns bytes read)HelloWorldPubSubTypehelper class with staticserialize()/deserialize()wrapperscdr2namespace with alignment and bounds-checking helpers
If hddsgen is not in your PATH, build it from source:
cd /path/to/hdds_gen
cargo install --path .
Generated Code Structure
The generated header contains two main components:
1. The data struct with public members and CDR2 codec methods:
struct HelloWorld {
int32_t id = 0;
std::string message;
HelloWorld() = default;
HelloWorld(int32_t i, std::string msg);
// Encode to CDR2 little-endian buffer. Returns bytes written, or -1 on error.
[[nodiscard]] int encode_cdr2_le(std::uint8_t* dst, std::size_t len) const noexcept;
// Decode from CDR2 little-endian buffer. Returns bytes read, or -1 on error.
[[nodiscard]] int decode_cdr2_le(const std::uint8_t* src, std::size_t len) noexcept;
};
2. The PubSubType helper with DDS-compatible static interface:
class HelloWorldPubSubType {
public:
using type = HelloWorld;
static constexpr const char* type_name() noexcept;
static int serialize(const void* data, std::uint8_t* buffer, std::size_t buffer_size) noexcept;
static int deserialize(const std::uint8_t* buffer, std::size_t buffer_size, void* data) noexcept;
static std::size_t calculate_serialized_size(const void* data) noexcept;
static void* create_data() noexcept;
static void delete_data(void* data) noexcept;
};
For most use cases, you only need encode_cdr2_le() and decode_cdr2_le() on the struct directly. The PubSubType class is used internally by DDS infrastructure and for advanced type-erased patterns.
Step 3: Create the Publisher
The typed API (create_writer<T>, write()) handles CDR2 serialization automatically. A raw API (create_writer_raw, write_raw) is also available if you need to manage buffers yourself or work without IDL types.
Create publisher.cpp:
#include <hdds.hpp>
#include <iostream>
#include <thread>
#include <chrono>
#include "HelloWorld.hpp"
using namespace std::chrono_literals;
int main() {
try {
hdds::logging::init(hdds::LogLevel::Warn);
// Domain 0 by default. Use Participant("name", 42) for domain 42.
hdds::Participant participant("HelloPublisher");
// Typed writer: compile-time check that T has encode_cdr2_le
auto writer = participant.create_writer<HelloWorld>("HelloWorldTopic");
std::cout << "Publishing messages..." << std::endl;
for (int i = 0; i < 10; i++) {
// CDR2 serialization is handled automatically
writer.write(HelloWorld{i, "Hello from HDDS C++!"});
std::cout << " Published: id=" << i << std::endl;
std::this_thread::sleep_for(500ms);
}
} catch (const hdds::Error& e) {
std::cerr << "HDDS Error: " << e.what() << std::endl;
return 1;
}
return 0; // RAII cleanup
}
Raw API (untyped payloads / custom serialization)
auto writer = participant.create_writer_raw("HelloWorldTopic");
HelloWorld msg(i, "Hello from HDDS C++!");
std::uint8_t buffer[4096];
int bytes = msg.encode_cdr2_le(buffer, sizeof(buffer));
if (bytes > 0) {
writer->write_raw(buffer, static_cast<size_t>(bytes));
}
Step 4: Create the Subscriber
Same pattern on the read side: create_reader<T> + take() returns std::optional<T> with deserialization handled automatically. No need to re-specify the type -- the reader already knows it.
Create subscriber.cpp:
#include <hdds.hpp>
#include <iostream>
#include <chrono>
#include "HelloWorld.hpp"
using namespace std::chrono_literals;
int main() {
try {
hdds::logging::init(hdds::LogLevel::Warn);
hdds::Participant participant("HelloSubscriber");
// Typed reader: compile-time check that T has decode_cdr2_le
auto reader = participant.create_reader<HelloWorld>("HelloWorldTopic");
// WaitSet for event-driven reception
hdds::WaitSet waitset;
waitset.attach(reader.get_status_condition());
std::cout << "Waiting for messages..." << std::endl;
int received = 0;
while (received < 10) {
if (waitset.wait(5s)) {
// No need to re-specify <HelloWorld> -- reader already knows the type
while (auto msg = reader.take()) {
std::cout << " Received: id=" << msg->id
<< " msg='" << msg->message << "'" << std::endl;
received++;
}
} else {
std::cout << " (timeout)" << std::endl;
}
}
} catch (const hdds::Error& e) {
std::cerr << "HDDS Error: " << e.what() << std::endl;
return 1;
}
return 0;
}
Raw API (untyped payloads / custom serialization)
auto reader = participant.create_reader_raw("HelloWorldTopic");
while (auto data = reader->take_raw()) {
HelloWorld msg;
if (msg.decode_cdr2_le(data->data(), data->size()) > 0) {
std::cout << " Received: id=" << msg.id << std::endl;
}
}
Step 5: Create CMakeLists.txt
cmake_minimum_required(VERSION 3.16)
project(hdds-hello-world CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
# Configure with: cmake .. -DCMAKE_PREFIX_PATH=~/hdds/sdk/cmake
find_package(hdds REQUIRED)
add_executable(publisher publisher.cpp)
target_include_directories(publisher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(publisher PRIVATE hdds::hdds)
add_executable(subscriber subscriber.cpp)
target_include_directories(subscriber PRIVATE ${CMAKE_CURRENT_SOURCE_DIR})
target_link_libraries(subscriber PRIVATE hdds::hdds)
Alternative: manual paths (without find_package)
If you cannot use CMAKE_PREFIX_PATH, set paths manually:
cmake_minimum_required(VERSION 3.16)
project(hdds-hello-world CXX)
set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)
set(HDDS_ROOT "$ENV{HOME}/hdds" CACHE PATH "Path to HDDS source root (e.g. ~/hdds)")
set(HDDS_C_INCLUDE "${HDDS_ROOT}/sdk/c/include")
set(HDDS_CXX_INCLUDE "${HDDS_ROOT}/sdk/cxx/include")
set(HDDS_LIB_DIR "${HDDS_ROOT}/target/release")
set(HDDS_CXX_LIB_DIR "${HDDS_ROOT}/sdk/cxx/build")
add_executable(publisher publisher.cpp)
target_include_directories(publisher PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${HDDS_CXX_INCLUDE} ${HDDS_C_INCLUDE})
target_link_directories(publisher PRIVATE ${HDDS_CXX_LIB_DIR} ${HDDS_LIB_DIR})
target_link_libraries(publisher PRIVATE hdds_cxx hdds_c pthread dl m)
add_executable(subscriber subscriber.cpp)
target_include_directories(subscriber PRIVATE ${CMAKE_CURRENT_SOURCE_DIR} ${HDDS_CXX_INCLUDE} ${HDDS_C_INCLUDE})
target_link_directories(subscriber PRIVATE ${HDDS_CXX_LIB_DIR} ${HDDS_LIB_DIR})
target_link_libraries(subscriber PRIVATE hdds_cxx hdds_c pthread dl m)
cmake .. -DHDDS_ROOT=~/hdds
Step 6: Build and Run
mkdir build && cd build
# Point CMake to the HDDS cmake config (the /sdk/cmake dir contains hddsConfig.cmake)
cmake .. -DCMAKE_PREFIX_PATH=~/hdds/sdk/cmake
cmake --build .
With default QoS (BEST_EFFORT + VOLATILE), the writer does not keep a history -- messages published before the subscriber joins are lost forever. This is standard DDS behavior. Always start the subscriber before the publisher.
Note: TRANSIENT_LOCAL durability (which caches data for late joiners) is not yet fully implemented -- late-joining subscribers do not receive historical data. See Known Issues. For now, always ensure the subscriber is running before publishing.
# Terminal 1 -- start this first
./subscriber
# Terminal 2
./publisher
Without CMake (g++ direct)
You need two include paths (C++ wrapper + C FFI) and must link the static .a archives directly:
# Set HDDS_ROOT to wherever you cloned/extracted HDDS (e.g. ~/hdds)
HDDS_ROOT=~/hdds
g++ -std=c++17 \
-I${HDDS_ROOT}/sdk/cxx/include \
-I${HDDS_ROOT}/sdk/c/include \
publisher.cpp \
${HDDS_ROOT}/sdk/cxx/build/libhdds_cxx.a \
${HDDS_ROOT}/target/release/libhdds_c.a \
-lpthread -ldl -lm -o publisher
Always link the .a files directly as shown above. Do not use -lhdds_c with -L flags -- the linker may pick the .so shared library instead, causing cannot open shared object file errors at runtime.
After system-wide install (simpler paths)
After make install (or make install PREFIX=/opt/hdds), headers and libraries are in standard paths:
# CMake: point to install prefix (default /usr/local)
cmake .. -DCMAKE_PREFIX_PATH=/usr/local
# Or with a custom prefix:
cmake .. -DCMAKE_PREFIX_PATH=/opt/hdds
# g++ direct (if installed to /usr/local, no -I needed):
g++ -std=c++17 publisher.cpp \
/usr/local/lib/libhdds_cxx.a \
/usr/local/lib/libhdds_c.a \
-lpthread -ldl -lm -o publisher
Expected output:
# subscriber # publisher
Waiting for messages... Publishing messages...
Received: id=0 msg='Hello from HDDS C++!' Published: id=0
Received: id=1 msg='Hello from HDDS C++!' Published: id=1
Received: id=2 msg='Hello from HDDS C++!' Published: id=2
... ...
Want late joiners to receive past messages? (known limitation)
TRANSIENT_LOCAL durability is designed so the writer keeps samples for late-joining readers. However, late-joiner delivery is not yet fully implemented in HDDS -- subscribers that join after publication will not receive historical data. See Known Issues.
For now, TRANSIENT_LOCAL + RELIABLE is useful for ensuring no message loss when pub and sub are running simultaneously:
auto qos = hdds::QoS::reliable().transient_local();
auto writer = participant.create_writer<HelloWorld>("topic", qos);
auto reader = participant.create_reader<HelloWorld>("topic", qos);
writer.write(HelloWorld{1, "typed + QoS"}); // .write(), not ->write()
QoS Configuration
// Fluent QoS builder
auto qos = hdds::QoS::reliable()
.transient_local()
.history_depth(10)
.deadline(std::chrono::milliseconds(100));
// Create typed writer/reader with custom QoS
auto writer = participant.create_writer<MyType>("topic", qos);
auto reader = participant.create_reader<MyType>("topic", qos);
// Or raw writer/reader (untyped)
auto raw_writer = participant.create_writer_raw("topic", qos);
auto raw_reader = participant.create_reader_raw("topic", qos);
The C++ SDK supports IntraProcess and UdpMulticast transports. For TCP, QUIC, or LowBandwidth transports, use the HDDS_TRANSPORT environment variable (e.g., HDDS_TRANSPORT=tcp) or the C API directly. See the C++ API Reference for details.
Typed API
The template methods create_writer<T>() and create_reader<T>() are the recommended way to publish and subscribe with hddsgen types. They return TypedDataWriter<T> / TypedDataReader<T> wrappers that handle CDR2 serialization automatically -- no need to re-specify the type on each call:
// Typed write (T must have encode_cdr2_le, generated by hddsgen)
auto writer = participant.create_writer<HelloWorld>("HelloWorldTopic");
writer.write(HelloWorld{1, "Hello typed!"});
// Typed read -- no need to re-specify <HelloWorld>, the reader knows the type
auto reader = participant.create_reader<HelloWorld>("HelloWorldTopic");
if (auto msg = reader.take()) {
std::cout << "Received: " << msg->message << std::endl;
}
Using a type without encode_cdr2_le/decode_cdr2_le in create_writer<T>/create_reader<T> produces a clear compile-time error with instructions to generate the type via hddsgen.
RAII Resource Management
All HDDS C++ objects use RAII. No manual cleanup needed:
{
hdds::Participant participant("my_app");
auto writer = participant.create_writer_raw("topic");
writer->write_raw(data);
} // Everything cleaned up automatically
Logging
Two options for log configuration:
// Option 1: C++ API (recommended)
hdds::logging::init(hdds::LogLevel::Warn);
// Option 2: Environment variable
// export RUST_LOG=hdds=info
hdds::logging::init_env(); // reads RUST_LOG
What's Next?
- C++ API Reference -- Complete C++ documentation
- QoS Configuration -- Reliable delivery, transient local, deadlines
- Samples: Multi-Topic -- Publish to multiple topics from one participant
- Listeners -- Callback-based event monitoring with
#include <hdds_listener.hpp>(see also discovery samples) - Hello World Python -- Python version of this tutorial