Skip to main content

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-cxx in the hdds directory
  • hddsgen code generator (see below)
Rust is required (even for C++ only)

HDDS is implemented in Rust with C/C++ bindings. The Rust toolchain is needed to:

  1. Build the core library (libhdds_c.a) -- make sdk-cxx runs cargo build internally
  2. 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+
Build hddsgen from the hdds_gen repository

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.

C++ API

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.

Tutorial vs samples

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
Use namespaces in real projects

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
Project layout

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:

  • HelloWorld struct 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)
  • HelloWorldPubSubType helper class with static serialize()/deserialize() wrappers
  • cdr2 namespace with alignment and bounds-checking helpers
Installing hdds-gen

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 .
Start the subscriber first

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
Static archives only

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);
Available transports

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;
}
tip

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?