diff --git a/rmw/include/rmw/impl/cpp/buffer_backend_metadata.hpp b/rmw/include/rmw/impl/cpp/buffer_backend_metadata.hpp new file mode 100644 index 00000000..efced698 --- /dev/null +++ b/rmw/include/rmw/impl/cpp/buffer_backend_metadata.hpp @@ -0,0 +1,181 @@ +// Copyright 2026 Open Source Robotics Foundation, 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. + +#ifndef RMW__IMPL__CPP__BUFFER_BACKEND_METADATA_HPP_ +#define RMW__IMPL__CPP__BUFFER_BACKEND_METADATA_HPP_ + +#include +#include +#include +#include +#include +#include +#include + +namespace rmw +{ +namespace impl +{ +namespace cpp +{ + +inline constexpr char kBufferBackendMetadataPrefix[] = "backends:"; + +inline std::string +escape_buffer_backend_metadata_field(const std::string & input) +{ + std::string out; + out.reserve(input.size()); + for (char c : input) { + switch (c) { + case '%': + out += "%25"; + break; + case ';': + out += "%3B"; + break; + case ':': + out += "%3A"; + break; + case '/': + out += "%2F"; + break; + default: + out += c; + break; + } + } + return out; +} + +inline int +buffer_backend_metadata_hex_value(char c) +{ + if (c >= '0' && c <= '9') { + return c - '0'; + } + if (c >= 'A' && c <= 'F') { + return 10 + (c - 'A'); + } + if (c >= 'a' && c <= 'f') { + return 10 + (c - 'a'); + } + return -1; +} + +inline std::string +unescape_buffer_backend_metadata_field(const std::string_view input) +{ + std::string out; + out.reserve(input.size()); + for (size_t i = 0; i < input.size(); ++i) { + if (input[i] == '%' && i + 2 < input.size()) { + int hi = buffer_backend_metadata_hex_value(input[i + 1]); + int lo = buffer_backend_metadata_hex_value(input[i + 2]); + if (hi >= 0 && lo >= 0) { + out += static_cast((hi << 4) | lo); + i += 2; + continue; + } + } + out += input[i]; + } + return out; +} + +template +std::string +serialize_buffer_backend_metadata(const MapT & metadata) +{ + if (metadata.empty()) { + return {}; + } + + std::vector> entries; + entries.reserve(metadata.size()); + for (const auto & pair : metadata) { + if (!pair.first.empty()) { + entries.emplace_back(pair.first, pair.second); + } + } + if (entries.empty()) { + return {}; + } + + std::sort( + entries.begin(), entries.end(), + [](const auto & lhs, const auto & rhs) { + return lhs.first < rhs.first; + }); + + std::string output{kBufferBackendMetadataPrefix}; + for (size_t i = 0; i < entries.size(); ++i) { + output += escape_buffer_backend_metadata_field(entries[i].first); + output += ":"; + output += escape_buffer_backend_metadata_field(entries[i].second); + if (i + 1 < entries.size()) { + output += ";"; + } + } + return output; +} + +inline std::map +parse_buffer_backend_metadata(const char * serialized_metadata) +{ + std::map result; + if (!serialized_metadata || serialized_metadata[0] == '\0') { + return result; + } + + std::string_view input{serialized_metadata}; + constexpr std::string_view prefix{kBufferBackendMetadataPrefix}; + if (input.rfind(prefix, 0) != 0) { + return result; + } + + std::string_view body = input.substr(prefix.size()); + if (body.empty()) { + return result; + } + + size_t start = 0; + while (start <= body.size()) { + size_t sep = body.find(';', start); + std::string_view entry = body.substr( + start, sep == std::string_view::npos ? std::string_view::npos : sep - start); + if (!entry.empty()) { + size_t colon = entry.find(':'); + std::string name = unescape_buffer_backend_metadata_field(entry.substr(0, colon)); + std::string metadata; + if (colon != std::string_view::npos) { + metadata = unescape_buffer_backend_metadata_field(entry.substr(colon + 1)); + } + if (!name.empty()) { + result[std::move(name)] = std::move(metadata); + } + } + if (sep == std::string_view::npos) { + break; + } + start = sep + 1; + } + return result; +} + +} // namespace cpp +} // namespace impl +} // namespace rmw + +#endif // RMW__IMPL__CPP__BUFFER_BACKEND_METADATA_HPP_ diff --git a/rmw/include/rmw/topic_endpoint_info.h b/rmw/include/rmw/topic_endpoint_info.h index c05d397a..6dec1d07 100644 --- a/rmw/include/rmw/topic_endpoint_info.h +++ b/rmw/include/rmw/topic_endpoint_info.h @@ -44,6 +44,8 @@ typedef struct RMW_PUBLIC_TYPE rmw_topic_endpoint_info_s uint8_t endpoint_gid[RMW_GID_STORAGE_SIZE]; /// QoS profile of the endpoint rmw_qos_profile_t qos_profile; + /// Serialized buffer backend metadata advertised for this endpoint + const char * buffer_backend_metadata; } rmw_topic_endpoint_info_t; /// Return zero initialized topic endpoint info data structure. @@ -363,6 +365,47 @@ rmw_topic_endpoint_info_set_qos_profile( rmw_topic_endpoint_info_t * topic_endpoint_info, const rmw_qos_profile_t * qos_profile); +/// Set serialized buffer backend metadata in the given topic endpoint info data structure. +/** + * Allocates memory and copies the serialized `buffer_backend_metadata` string + * into the topic endpoint info. + * + *
+ * Attribute | Adherence + * ------------------ | ------------- + * Allocates Memory | Yes + * Thread-Safe | No + * Uses Atomics | No + * Lock-Free | Yes + * + * \par Thread-safety + * Setting a member is a reentrant procedure, but access to the + * topic endpoint info data structure is not synchronized. + * It is not safe to read or write the `buffer_backend_metadata` member of the + * given `topic_endpoint` while setting it. + * Access to C-style string arguments is read-only but it is not synchronized. + * Concurrent `buffer_backend_metadata` reads are safe, but concurrent reads and writes are not. + * + * \pre Given `buffer_backend_metadata` is a valid C-style string i.e. NULL terminated. + * + * \param[inout] topic_endpoint_info Data structure to be populated. + * \param[in] buffer_backend_metadata Serialized buffer backend metadata to be set. + * \param[in] allocator Allocator to be used. + * \returns `RMW_RET_OK` if successful, or + * \returns `RMW_RET_INVALID_ARGUMENT` if `topic_endpoint_info` is NULL, or + * \returns `RMW_RET_INVALID_ARGUMENT` if `buffer_backend_metadata` is NULL, or + * \returns `RMW_RET_BAD_ALLOC` if memory allocation fails, or + * \returns `RMW_RET_ERROR` when an unspecified error occurs. + * \remark This function sets the RMW error state on failure. + */ +RMW_PUBLIC +RMW_WARN_UNUSED +rmw_ret_t +rmw_topic_endpoint_info_set_buffer_backend_metadata( + rmw_topic_endpoint_info_t * topic_endpoint_info, + const char * buffer_backend_metadata, + rcutils_allocator_t * allocator); + #ifdef __cplusplus } #endif diff --git a/rmw/src/topic_endpoint_info.c b/rmw/src/topic_endpoint_info.c index 10d819ae..f772eb29 100644 --- a/rmw/src/topic_endpoint_info.c +++ b/rmw/src/topic_endpoint_info.c @@ -61,6 +61,15 @@ _rmw_topic_endpoint_info_fini_topic_type( return _rmw_topic_endpoint_info_fini_str(&topic_endpoint_info->topic_type, allocator); } +rmw_ret_t +_rmw_topic_endpoint_info_fini_buffer_backend_metadata( + rmw_topic_endpoint_info_t * topic_endpoint_info, + rcutils_allocator_t * allocator) +{ + return _rmw_topic_endpoint_info_fini_str( + &topic_endpoint_info->buffer_backend_metadata, allocator); +} + rmw_ret_t rmw_topic_endpoint_info_fini( rmw_topic_endpoint_info_t * topic_endpoint_info, @@ -90,6 +99,10 @@ rmw_topic_endpoint_info_fini( if (ret != RMW_RET_OK) { return ret; } + ret = _rmw_topic_endpoint_info_fini_buffer_backend_metadata(topic_endpoint_info, allocator); + if (ret != RMW_RET_OK) { + return ret; + } *topic_endpoint_info = rmw_get_zero_initialized_topic_endpoint_info(); @@ -246,3 +259,21 @@ rmw_topic_endpoint_info_set_qos_profile( topic_endpoint_info->qos_profile = *qos_profile; return RMW_RET_OK; } + +rmw_ret_t +rmw_topic_endpoint_info_set_buffer_backend_metadata( + rmw_topic_endpoint_info_t * topic_endpoint_info, + const char * buffer_backend_metadata, + rcutils_allocator_t * allocator) +{ + RCUTILS_CAN_RETURN_WITH_ERROR_OF(RMW_RET_INVALID_ARGUMENT); + + if (!topic_endpoint_info) { + RMW_SET_ERROR_MSG("topic_endpoint_info is null"); + return RMW_RET_INVALID_ARGUMENT; + } + return _rmw_topic_endpoint_info_copy_str( + &topic_endpoint_info->buffer_backend_metadata, + buffer_backend_metadata, + allocator); +} diff --git a/rmw/test/test_topic_endpoint_info.cpp b/rmw/test/test_topic_endpoint_info.cpp index 2a3603d3..09bf4d4b 100644 --- a/rmw/test/test_topic_endpoint_info.cpp +++ b/rmw/test/test_topic_endpoint_info.cpp @@ -12,11 +12,16 @@ // See the License for the specific language governing permissions and // limitations under the License. +#include +#include +#include +#include + #include "gmock/gmock.h" #include "osrf_testing_tools_cpp/scope_exit.hpp" #include "rcutils/allocator.h" - #include "rmw/error_handling.h" +#include "rmw/impl/cpp/buffer_backend_metadata.hpp" #include "rmw/topic_endpoint_info.h" #include "rmw/types.h" @@ -217,6 +222,61 @@ TEST(test_topic_endpoint_info, set_qos_profile) { false) << "Unequal avoid namespace conventions"; } +TEST(test_topic_endpoint_info, set_buffer_backend_metadata) { + rmw_topic_endpoint_info_t topic_endpoint_info = rmw_get_zero_initialized_topic_endpoint_info(); + rcutils_allocator_t allocator = rcutils_get_default_allocator(); + char * val = get_mallocd_string("backends:test:version=1.0"); + + rmw_ret_t ret = + rmw_topic_endpoint_info_set_buffer_backend_metadata(&topic_endpoint_info, val, nullptr); + EXPECT_EQ(ret, RMW_RET_INVALID_ARGUMENT) << "Expected invalid argument for null allocator"; + rmw_reset_error(); + + ret = rmw_topic_endpoint_info_set_buffer_backend_metadata( + &topic_endpoint_info, nullptr, &allocator); + EXPECT_EQ(ret, RMW_RET_INVALID_ARGUMENT) << "Expected invalid argument for null metadata"; + rmw_reset_error(); + + ret = rmw_topic_endpoint_info_set_buffer_backend_metadata(nullptr, val, &allocator); + EXPECT_EQ(ret, RMW_RET_INVALID_ARGUMENT) << + "Expected invalid argument for null topic_endpoint_info"; + rmw_reset_error(); + + OSRF_TESTING_TOOLS_CPP_SCOPE_EXIT( + { + allocator.deallocate( + const_cast(topic_endpoint_info.buffer_backend_metadata), + allocator.state); + }); + ret = rmw_topic_endpoint_info_set_buffer_backend_metadata( + &topic_endpoint_info, val, &allocator); + EXPECT_EQ(ret, RMW_RET_OK) << "Expected OK for valid arguments"; + free(val); + EXPECT_STREQ(topic_endpoint_info.buffer_backend_metadata, "backends:test:version=1.0"); +} + +TEST(test_topic_endpoint_info, buffer_backend_metadata_serialization) { + std::map metadata{ + {"test", "version=1.0"}, + {"cpu", ""}, + {"with:colon", "semi;slash/percent%"}, + }; + + const std::string serialized = + rmw::impl::cpp::serialize_buffer_backend_metadata(metadata); + EXPECT_EQ( + serialized, + "backends:cpu:;test:version=1.0;with%3Acolon:semi%3Bslash%2Fpercent%25"); + + EXPECT_EQ(rmw::impl::cpp::parse_buffer_backend_metadata(serialized.c_str()), metadata); + EXPECT_TRUE( + rmw::impl::cpp::serialize_buffer_backend_metadata( + std::map{}).empty()); + EXPECT_TRUE(rmw::impl::cpp::parse_buffer_backend_metadata(nullptr).empty()); + EXPECT_TRUE(rmw::impl::cpp::parse_buffer_backend_metadata("").empty()); + EXPECT_TRUE(rmw::impl::cpp::parse_buffer_backend_metadata("not-backends:cpu:").empty()); +} + TEST(test_topic_endpoint_info, zero_init) { rmw_topic_endpoint_info_t topic_endpoint_info = rmw_get_zero_initialized_topic_endpoint_info(); EXPECT_FALSE(topic_endpoint_info.node_name); @@ -245,6 +305,7 @@ TEST(test_topic_endpoint_info, zero_init) { EXPECT_EQ( topic_endpoint_info.qos_profile.avoid_ros_namespace_conventions, false) << "Non-zero avoid namespace conventions"; + EXPECT_FALSE(topic_endpoint_info.buffer_backend_metadata); } TEST(test_topic_endpoint_info, fini) { @@ -279,6 +340,9 @@ TEST(test_topic_endpoint_info, fini) { EXPECT_EQ(ret, RMW_RET_OK) << "Expected OK for valid node_name arguments"; ret = rmw_topic_endpoint_info_set_topic_type(&topic_endpoint_info, "type", &allocator); EXPECT_EQ(ret, RMW_RET_OK) << "Expected OK for valid topic_type arguments"; + ret = rmw_topic_endpoint_info_set_buffer_backend_metadata( + &topic_endpoint_info, "backends:test:", &allocator); + EXPECT_EQ(ret, RMW_RET_OK) << "Expected OK for valid buffer backend metadata"; ret = rmw_topic_endpoint_info_fini(&topic_endpoint_info, nullptr); EXPECT_EQ(ret, RMW_RET_INVALID_ARGUMENT) << "Expected invalid argument for null allocator"; rmw_reset_error(); @@ -314,4 +378,5 @@ TEST(test_topic_endpoint_info, fini) { "Non-zero liveliness lease duration nsec"; EXPECT_EQ(topic_endpoint_info.qos_profile.avoid_ros_namespace_conventions, false) << "Non-zero avoid namespace conventions"; + EXPECT_FALSE(topic_endpoint_info.buffer_backend_metadata); }