Tinverse logo Tinverse LLC

Tin DDS · C++ · evaluation release

Tin DDS for C++

DDS is more than publish and subscribe. It combines a data-centric programming model with state for multiple objects, policy-driven matching, bounded histories, discovery, repair, filtering, lifecycle, and status events.

DDS programming models

DDS core

DCPS defines how data is addressed, published, retained, and observed.

Data-centric publish/subscribe

Writers publish application data to topics; readers receive it without naming or connecting to a specific peer.

Participant → Topic<T> → DataWriter<T> / DataReader<T>

Multiple objects on one topic

A field such as a robot ID identifies each object, so DDS can maintain history, ownership, durability, and lifecycle separately for every robot.

write_instance · take_instance · dispose · unregister

State access and event-driven processing

Applications can inspect retained state with read/take operations or react to data and communication-status changes through listeners.

read · take · listener · communication status

Grouped and coherent changes

Publishers and subscribers establish a policy boundary; coherent sets keep readers from observing only part of a related update.

Publisher · Subscriber · begin/end coherent changes

Models built on DDS

Request/reply

A request and its reply are ordinary DDS samples connected by related-sample identity and conventionally paired topics.

request writer + reply reader + correlation identity

ROS 2 execution

Publishers, subscriptions, clients, services, events, and wait sets are mapped onto DDS through the ROS middleware boundary.

rmw_tindds_cpp
Tin DDS exposes related-sample primitives used to compose request/reply, but does not advertise a standalone DDS-RPC client/service facade. The catalog below states its actual public surface.

Tin DDS · C++

The Tin DDS implementation

Tin DDS exposes DDS entities for application types and serialized data, explicit publisher and subscriber groups, listener-driven status handling, and low-level composition points where an application needs direct control.

// Join a running ROS 2 graph if ROS_DOMAIN_ID is set; otherwise use 0.
char const* const domain_env = std::getenv("ROS_DOMAIN_ID");
tin::dds::domain_id const domain{
  domain_env != nullptr
    ? static_cast<std::uint32_t>(std::strtoul(domain_env, nullptr, 10))
    : 0U };

struct temperature {
  static constexpr std::string_view tin_dds_type_name{
    "sensors.Temperature" };
  float degrees_celsius{};
};

tin::dds::domain_participant participant{ domain };
auto topic = participant.create_topic<temperature>("sensors/temperature");
auto writer = participant.create_writer(topic);
auto reader = participant.create_reader(topic);
Entity creation returns std::expected, preserving enablement and QoS failure details. The public type supplies its stable DDS name; application code does not provide a type hash.

Tin DDS · data and state

C++ feature APIs and algorithms

DCPS entities and groups

participant.create_topic<T>(name) · participant.create_writer(topic) · participant.create_reader(topic) · create_publisher(participant, qos) · create_subscriber(participant, qos)

Indexed endpoint matching. Tin DDS indexes readers and writers by topic and type, then connects pairs whose QoS is compatible. Removing an endpoint or one of its parent groups also removes its matches.

A participant joins a DDS domain. A topic names the data being exchanged; writers publish samples and readers receive them. Publisher and subscriber groups let related endpoints share policy and coherent-update boundaries.

auto topic = participant.create_topic<Temperature>("temperature");
auto writer = participant.create_writer(topic);
auto reader = participant.create_reader(topic);

Type identity

T::tin_dds_type_name · tin::dds::describe_type<T>() · topic.type()

Type comparison during discovery. Tin DDS attaches the declared type identity to the topic and advertises it with each endpoint. A writer and reader match only when their advertised types are compatible.

The C++ class name is local to one program, so it cannot identify data across processes. The stable DDS type name gives both processes the same wire-level identity.

struct Temperature {
  static constexpr std::string_view tin_dds_type_name{
    "sensors.Temperature" };
  float degrees_celsius{};
};

auto type = tin::dds::describe_type<Temperature>();
auto topic = participant.create_topic<Temperature>(
  "sensors/temperature");
assert(type.name == "sensors.Temperature");
assert(topic.type().name == type.name);

Read, take, and batches

reader.read() · reader.take() · reader.read(batch) · reader.take(batch)

Bounded reader-history selection. Read preserves a history entry; take removes it. Rings and descriptor ownership keep access finite.

Use read when later calls should still see the sample. Use take when the sample has been consumed. Batch calls apply the same choice to several caller-owned slots at once.

auto observed = reader->read(); // remains in history
auto consumed = reader->take(); // removed from history

std::array<tin::dds::sample_slot<Temperature>, 8> batch{};
auto const count = reader->take(batch);

Topic instances and lifecycle

writer.write(value, key) · reader.take_instance(key) · writer.dispose_instance(key) · writer.unregister_instance(key) · reader.instance_status(key)

Key-indexed object lifecycle. Bounded per-key history scans and a lifecycle table preserve state independently for each object.

One topic can carry many robots or sensors, identified by a key such as a robot ID. DDS keeps separate history and lifecycle state for each object. A reader can select one object or observe when it is disposed or unregistered.

writer->write(state, "robot-17");
auto sample = reader->take_instance("robot-17");
writer->dispose_instance("robot-17");

QoS matching and partitions

offered.compatibility_with(requested) · participant.create_writer(topic, qos) · participant.create_reader(topic, qos)

DDS requested/offered compatibility. Admission runs before graph mutation; partition matching uses bounded exact and */? wildcard comparisons.

A writer offers delivery behavior and a reader requests what it needs. They match only when those policies are compatible. Partitions divide one domain into named groups without changing topic names.

tin::dds::qos_profile offered{};
offered.reliability = tin::dds::reliability_kind::reliable;
offered.partitions = { "control/*" };
auto result = offered.compatibility_with(requested);

History and resource limits

participant.create_writer(topic, qos) · participant.create_reader(topic, qos)

Bounded KEEP_LAST/KEEP_ALL admission. KEEP_LAST evicts the oldest entry; KEEP_ALL rejects admission at its configured limit.

KEEP_LAST drops the oldest sample when its depth is full. KEEP_ALL retains samples until an explicit resource limit is reached, then rejects new admission. These limits make memory use and backlog a deployment choice.

qos.history = tin::dds::history_kind::keep_last;
qos.depth = 8U;
qos.resource_limits.max_samples = 64;
qos.resource_limits.max_samples_per_instance = 8;

Content-filtered data

reader.set_filter(predicate) · reader.clear_filter() · subscriber.create_serialized_datareader(name, type, qos, validator)

Pre-admission content filtering. The predicate runs before reader-history admission on local, shared-memory, and RTPS ingress paths.

The reader's predicate runs before a sample enters its history. Samples that do not match use no reader-history space. The application can replace or clear the filter without recreating the reader.

reader->set_filter([](Temperature const& value) {
  return value.degrees_celsius > 80.0F;
});
reader->clear_filter();

Durability and late joiners

participant.create_writer(topic, qos) · durability.start() · durability.checkpoint() · durability.close()

Bounded durability replay with atomic checkpointing. Writer retention preserves original sequence metadata.

Durability lets a reader receive state written before it joined. Transient-local history lasts while its writer is running; the persistent service can recover retained state after a restart. Follow the late-joiner example.

qos.durability = tin::dds::durability_kind::persistent;
qos.durability_service.history_depth = 8;
durability.start();
durability.checkpoint();

Time, liveliness, ownership, and order

participant.create_writer(topic, qos) · writer.refresh_deadline_status() · writer.refresh_liveliness_status() · reader.take()

Lease expiry, strongest-writer arbitration, and source-order selection. Each policy uses bounded status or history scans.

Deadline states how often data is expected, and liveliness detects an inactive writer. Exclusive ownership chooses the strongest eligible writer. Destination or source order determines how readers arrange accepted samples.

qos.deadline = {
  std::chrono::milliseconds{ 10 }
};
qos.liveliness = tin::dds::liveliness_kind::manual_by_topic;
qos.ownership = tin::dds::ownership_kind::exclusive;
qos.ownership_strength = 100;
DDS + RTPS

Presentation and coherent changes

publisher.begin_coherent_changes() · publisher.end_coherent_changes() · publisher.cancel_coherent_changes()

Staged coherent-set commit. Samples are released by publisher scope only when the complete indexed set is available; incomplete remote sets remain hidden.

begin and end mark several related writes as one update. A reader requesting coherent access sees the complete set, not an intermediate state, whether the samples arrive locally or over RTPS.

publisher.begin_coherent_changes();
trajectory_writer.write(trajectory);
limits_writer.write(limits);
publisher.end_coherent_changes();

Listeners and communication status

reader.set_listener(listener) · writer.set_listener(listener) · reader.refresh_matched_status() · reader.refresh_deadline_status()

Delta-based status propagation. Trackers compare cumulative counters with the last observed baseline and dispatch changes through the entity hierarchy.

A listener lets DDS notify the application instead of requiring it to poll continuously. Separate statuses report new data, endpoint matches, loss, rejection, incompatible QoS, missed deadlines, and liveliness changes.

tin::dds::data_reader_listener listener{};
listener.data_available = [](tin::dds::sample_info const&) {
  schedule_reader_work();
};
reader->set_listener(std::move(listener));
Tin DDS request/reply

Request/reply correlation

reply_writer.write_with_related_sample(payload, sequence, request_guid, request_sequence) · reply_reader.take_related_sample(request_guid, request_sequence)

Related-sample identity correlation. A reply carries the requesting writer GUID and sequence number for bounded indexed lookup.

A reply records the writer and sequence number of its request. The requester can therefore find the matching reply even when many requests are in flight. The application still owns the request and reply topics and service behavior.

reply_writer.write_with_related_sample(
  reply_payload, reply_sequence,
  request_writer_guid, request_sequence);

Tin DDS · C++ durability

Configure retention and persistent recovery

A writer keeps a bounded history for readers that arrive later. Persistent durability saves that history to a checkpoint and restores it when the participant restarts.

tin::dds::qos_profile qos{};
qos.durability = tin::dds::durability_kind::persistent;
qos.history = tin::dds::history_kind::keep_last;
qos.depth = 8U;
qos.max_samples = 64U;
qos.durability_service.history = tin::dds::history_kind::keep_last;
qos.durability_service.history_depth = 8;
qos.durability_service.max_samples = 64;
qos.durability_service.max_instances = 16;
qos.durability_service.max_samples_per_instance = 8;

auto topic = participant.create_topic<RobotState>("robot/state", qos);
auto writer = participant.create_writer(topic, qos);

tin::dds::persistent_durability_service durability{
  participant,
  tin::dds::persistent_durability_service_options{
    .checkpoint_path = "state/domain.durability",
    .recover_on_start = true,
    .checkpoint_on_close = true,
  },
};

if (!durability.start()) {
  return EXIT_FAILURE;
}

if (!writer || !writer->write(state)) {
  return EXIT_FAILURE;
}

if (!durability.checkpoint()) {
  return EXIT_FAILURE;
}

Tin DDS · network and runtime

C++ delivery mechanisms

RTPS

Discovery

participant.enable() · service.start() · service.wait_for_reader(timeout) · service.wait_for_writer(timeout)

SPDP lease discovery with SEDP requested/offered endpoint matching. Participant leases and endpoint announcements feed the indexed match graph.

Participants announce themselves, then announce the topics and QoS of their readers and writers. Compatible endpoints match automatically. When a participant stops renewing its lease, the runtime removes it and its endpoints.

auto writer = participant.create_writer(topic);
auto reader = participant.create_reader(topic);
// SPDP/SEDP matches compatible remote endpoints.
RTPS

Reliability and fragmentation

participant.create_writer(topic, reliable_qos) · writer.write(sample) · reader.take()

RTPS stateful reliable writer/reader repair. HEARTBEAT/ACKNACK repairs sequence ranges; DATA_FRAG and NACK_FRAG drive selective fragment repair.

Best-effort delivery sends a sample without repairing loss. Reliable delivery retains bounded history and retransmits missing samples or fragments requested by readers. Duplicate and reordered packets do not become duplicate application samples.

qos.reliability = tin::dds::reliability_kind::reliable;
qos.history = tin::dds::history_kind::keep_last;
qos.depth = 32U;
writer->write(sample);
Transport selection

UDP and shared memory

writer.write(value) · runtime.poll_once()

Per-reader transport selection. Each match uses an eligible same-host descriptor path or RTPS/UDP; mixed fanout chooses independently.

The writer uses the same call for every reader. Tin DDS uses shared memory for an eligible same-host reader and RTPS/UDP for a remote reader. One write can use both paths when its readers have different locations.

writer->write(sample);
// Same-host reader: shared-memory descriptor
// Remote reader: RTPS DATA or DATA_FRAG
ROS 2 + shared memory

Loaned samples

evaluate_loan_support(request) · try_loan_sample(arena, request) · loan.construct() · loan.publish() · loan.cancel()

Bounded slot reservation with RAII cancellation. An eligible publisher reserves one shared-memory slot; an unfinished loan is returned automatically.

The application asks the transport for a sample slot and constructs the message there. Publishing transfers that slot; cancellation returns it. This avoids a copy when the type and transport are eligible for loaning.

auto loan = tin::dds::ros2::try_loan_sample(arena, request);
if (loan) {
  auto& sample = loan->construct();
  sample.temperature = measured;
  loan->publish();
}
Teleoperation

Authority and command admission

factory.create_command_writer<T>(topic) · factory.create_command_reader<T>(topic, admission) · authority.grant(session, now) · reader.adopt_authority(lease) · reader.take(now, clock_quality)

Exclusive authority with sequence-and-freshness admission. A monotonic lease selects one controller and its epoch; the reader then rejects commands for the wrong robot, session, or epoch as well as stale, duplicate, and out-of-order commands.

A lease gives one operator session control of one robot for a bounded time and epoch. The reader accepts only commands from that session whose sequence increases and whose timestamp is fresh. Rejected commands never reach the application as valid control input.

control_authority authority{
  control_authority_config{
    .robot = *robot,
    .lease_duration_ticks = 1'000U,
  }
};
auto grant = authority.grant(*session,
  monotonic_timestamp::from_ticks(10U));
reader->adopt_authority(grant.lease);

auto command = reader->take(
  synchronized_timestamp::from_tai_nanoseconds(now_ns),
  clock_quality::synchronized);
Partial

DDS Security

participant_authentication_runtime::create(...) · security.progress(...) · security.authenticated(peer)

PKI-DH authentication state machine with ordered permissions evaluation. Bounded peer sessions and signed policies fail closed.

The identity certificate proves who the peer is. Governance says which traffic must be protected, and permissions say which topics that identity may publish or subscribe to. These building blocks exist; complete secured RTPS composition and vendor interoperability are not yet claimed.

auto config = participant_authentication_runtime_configuration{}
  .with_graph_admission(*graph_admission)
  .with_remote_permissions_validator(remote_permissions);

auto security = participant_authentication_runtime::create(
  participant, *runtime, local_identity,
  trust_store, session_limits, config);

security->progress(tick, now,
  discovery_buffer, user_buffer);
if (!security->authenticated(peer_guid)) {
  reject_peer();
}

Tin DDS · C++ guides

Building DDS applications

Start with a topic, add state for multiple objects and reliable delivery, then use the same writer call across UDP and shared memory. The final guides authenticate a peer and admit teleoperation commands by authority, sequence, and freshness.

Tutorial 1 · about 20 minutes

Connect two processes through one topic

A participant is one application's membership in a DDS domain. It owns that application's topics, readers, writers, discovery state, histories, and status. Its runtime owns the sockets and protocol timers that connect those local objects to participants in other processes.

DDS feature

Participants, topics, and endpoint matching

Each process creates its own participant and topic object. DDS connects their endpoints when the domain, topic name, type identity, and QoS are compatible.

What exists in each process

Process A memory                       Process B memory
participant 1                         participant 2
├─ topic metadata                     ├─ topic metadata
├─ writer and writer history          ├─ reader and reader history
├─ discovered reader + match          ├─ discovered writer + match
└─ RTPS runtime + UDP sockets         └─ RTPS runtime + UDP sockets
          │                                      ▲
          └──────── CDR sample in RTPS DATA ─────┘

The two topic objects do not share an address or C++ lifetime. “The same topic” means that both processes independently declare the same distributed identity. SPDP discovers the participants; SEDP exchanges their writer and reader descriptions; RTPS carries the serialized sample after they match.

Share the data contract

Both executables include the same type definition and CDR mapping.

struct Temperature {
  static constexpr std::string_view tin_dds_type_name{
    "sensors.Temperature" };
  float degrees_celsius{};
};

template<typename Archive, typename Self>
  requires std::same_as<std::remove_cvref_t<Self>, Temperature>
bool tin_dds_serde(Archive& archive, Self& value) {
  return archive(value.degrees_celsius);
}

Process A: publish

auto config = tin::dds::static_peer_configuration::loopback(
  7U, 1U, 2U); // domain 7, this participant 1, peer 2
auto dds = tin::dds::participant_service::connect(*config);

auto topic = dds->create_topic<Temperature>(
  "sensors/temperature");
auto writer = dds->create_writer(topic);

if (!writer ||
    !writer->wait_for_reader(std::chrono::seconds{ 2 })) {
  return EXIT_FAILURE;
}

if (!writer->write_for(
      Temperature{ .degrees_celsius = 23.5F },
      std::chrono::milliseconds{ 100 })) {
  return EXIT_FAILURE;
}

Process B: receive

auto config = tin::dds::static_peer_configuration::loopback(
  7U, 2U, 1U); // domain 7, this participant 2, peer 1
auto dds = tin::dds::participant_service::connect(*config);

auto topic = dds->create_topic<Temperature>(
  "sensors/temperature");
auto reader = dds->create_reader(topic);

if (!reader ||
    !reader->wait_for_writer(std::chrono::seconds{ 2 })) {
  return EXIT_FAILURE;
}

auto sample = reader->take_for(
  std::chrono::milliseconds{ 100 });

What to verify

  • The participant IDs differ, while the domain, topic name, type identity, and QoS agree.
  • The writer and reader match without sharing a C++ object or memory address.
  • write_for() serializes and sends the sample; take_for() returns it from Process B's reader history.
  • Changing the domain, topic name, type identity, or QoS compatibility prevents the match.

Tutorial 2 · about 15 minutes

Represent many robots as instances of one topic

Use each robot's ID as its key on one topic. Configure a bounded transient-local history so a late reader receives recent state.

DDS feature

Topic instances and late-reader replay

History, ownership, durability, and lifecycle are maintained independently for each key.

tin::dds::qos_profile qos{};
qos.reliability = tin::dds::reliability_kind::reliable;
qos.durability = tin::dds::durability_kind::transient_local;
qos.history = tin::dds::history_kind::keep_last;
qos.depth = 8U;
qos.max_samples = 64U;

auto topic = participant.create_topic<RobotPose>("fleet/pose", qos);
auto writer = participant.create_writer(topic, qos);

writer->write(pose, "robot-17");

// This reader may be created after the write. Transient-local durability
// replays retained state from the still-running writer.
auto reader = participant.create_reader(topic, qos);
auto sample = reader->take_instance("robot-17");

What to verify

  • Another robot ID creates another independently managed object, not another topic.
  • take_instance() selects only the requested robot.
  • A late reader receives the retained sample with its original sequence metadata.

Tutorial 3 · about 20 minutes

Request reliable delivery

Reliable QoS makes delivery a writer-reader contract. The writer keeps bounded repair history while RTPS heartbeats and acknowledgements recover missing samples and fragments.

DDS feature

Reliable RTPS

HEARTBEAT and ACKNACK repair missing sequence ranges; NACK_FRAG requests only missing fragments.

tin::dds::qos_profile reliable{};
reliable.reliability = tin::dds::reliability_kind::reliable;
reliable.history = tin::dds::history_kind::keep_last;
reliable.depth = 32U;
reliable.max_samples = 32U;

auto topic = participant.create_topic<Temperature>(
  "sensors/temperature", reliable);
auto writer = participant.create_writer(topic, reliable);
auto reader = participant.create_reader(topic, reliable);

writer->write(Temperature{ .degrees_celsius = 23.5F });
auto sample = reader->take();

What to verify

  • A reliable writer satisfies a reader requesting reliable delivery.
  • A best-effort writer does not match that reader.
  • Repair remains bounded by the configured writer history.

Tutorial 4 · about 10 minutes

Use shared memory without changing publish code

The application writes to a DDS topic. Tin DDS selects an eligible same-host shared-memory path or RTPS/UDP for each matched reader.

DDS feature

Location-transparent publish/subscribe

Transport selection belongs to the runtime, not to each application write.

if (!writer->write(sample)) {
  return EXIT_FAILURE;
}

// Same-host match  → shared-memory descriptor
// Remote match     → RTPS DATA or DATA_FRAG over UDP
// Mixed fanout     → one write, transport selected per reader

What to verify

  • The C++ call is identical for shared-memory and UDP readers.
  • An unavailable shared-memory path falls back without changing topic code.
  • Type matching and reliability remain DDS concerns on either transport.

Tutorial 5 · about 20 minutes

Authenticate and authorize a DDS peer

Bind signed governance and permissions policy to the participant, run bounded PKI-DH handshake progress, and admit the remote identity before activating protected communication.

DDS feature

DDS Security participant authentication

Authentication proves peer identity; governance and permissions determine whether that identity may join the domain and use its endpoints.

using namespace tin::dds::security;

auto graph_admission = permissions_graph_admission::create(
  domain, signed_governance, signed_permissions);
graph_admission->set_authorization_time(authorization_time);

auto config = participant_authentication_runtime_configuration{}
  .with_graph_admission(*graph_admission)
  .with_remote_permissions_validator(remote_permissions);

auto security = participant_authentication_runtime::create(
  participant,
  *runtime,
  local_identity,
  trust_store,
  session_limits,
  config);

std::array<std::byte, 4096> discovery_buffer{};
std::array<std::byte, 4096> user_buffer{};

for (std::uint64_t tick{};
     !security->authenticated(peer_guid);
     ++tick) {
  security->progress(
    tick,
    participant_authentication_runtime::clock::now(),
    discovery_buffer,
    user_buffer);
}

What to verify

  • An untrusted certificate or invalid handshake never produces an authenticated peer.
  • Expired or unauthorized permissions fail before endpoints enter the match graph.
  • Handshake sessions, retries, and message buffers remain within configured limits.

Tutorial 6 · about 20 minutes

Admit a teleoperation command

Give one operator session control authority, attach the resulting lease to the command reader, and accept only commands from that session and epoch that are fresh and strictly ordered.

DDS feature

Exclusive authority and command admission

Typed DDS command envelopes combine an authority lease with robot, session, epoch, sequence, and synchronized-time checks.

using namespace tin::dds::teleop;

auto robot = robot_id::from_value(17U);
auto session = operator_session_id::from_value(42U);
auto sequence = command_sequence::from_value(1U);

endpoint_factory endpoints{ participant };
auto writer = endpoints.create_command_writer<MotionSetpoint>(
  "robot/motion");
auto reader = endpoints.create_command_reader<MotionSetpoint>(
  "robot/motion",
  command_admission_config{
    .robot = *robot,
    .freshness_limit_nanoseconds = 5'000'000U,
  });

control_authority authority{ control_authority_config{
  .robot = *robot,
  .lease_duration_ticks = 1'000U,
} };
auto grant = authority.grant(
  *session, monotonic_timestamp::from_ticks(10U));
reader->adopt_authority(grant.lease);

motion_command<MotionSetpoint> command{
  .robot = *robot,
  .session = *session,
  .epoch = grant.lease.epoch,
  .sequence = *sequence,
  .generated_at = synchronized_timestamp::from_tai_nanoseconds(1'000'000U),
  .requested_mode = operation_mode::enabled,
  .payload = setpoint,
};

writer->publish(command);
auto admitted = reader->take(
  synchronized_timestamp::from_tai_nanoseconds(2'000'000U),
  clock_quality::synchronized);
if (!admitted) {
  enter_safe_hold(admitted.error());
}

What to verify

  • A command from a session without the active lease is rejected.
  • Repeating the same sequence number is reported as duplicate or out of order.
  • A command outside the freshness limit is rejected before the application receives it.