ARTICLE · 1105693
Ros2源码(8) 从 init 看 Ros2 SDK 架构设计
Ros2源码(8) 从 init 看 Ros2 SDK 架构设计一,背景 之前写的一篇 Ros2源码(7)rclcpp::init() 感觉不太满意,回头自己再看两遍,感觉什么都说了,又像什么都没说。于是,决定重新写一篇,希望能角度更高,关键点描述也更清晰。 二,Ros2 SDK 架构 Ros2 的官网有 SDK 架构图。链接如下: SDK 架构图如下: 图 1 
我们经常提到的 rclcpp,rcl,rmw 这里都有体现。蓝色的就是 dds 中间件层,可以有多个 dds 实现选择。ros2 humble 版本默认用 fastdds。 每层简介: 1)rclcpp / rclpy:提供对用户的 c++ 或者 python 调用的接口,实现一些 ros2 的概念,比如 callback-group。 2)rcl:C 语言实现,对 rclcpp/rclpy 提供统一的接口,并实现一些 ros2 自己的特性,例如 命令行参数解析等。 3)rmw:C 语言实现,提供统一接口,抽象 dds 层提供的功能。比如创建发布器等。 4)dds 层:提供具体的 dds 中间件实现。这部分不是 ros2 自己的组件,是第三方开源 dds 实现。 此外,其实代码中的实现和这个架构图有一点出入,但是问题不大。代码里没有一个"把 rmw 接口实现出来"的目录叫 rmw。rmw 包只负责声明接口;真正实现对 DDS 的封装,分散在各自的实现包里(如 rmw_fastrtps_cpp)。进程实际用哪个中间件,由一个名为 rmw_implementation 的选择机制来决定。Ros2 源码中如何知道当前使用哪个 DDS 中间件,这篇文档有体现。 三,关键 class 和 struct 3.1 class rclcpp::InitOptions 
可以看到,它的定义是在 rcl/include/rcl 目录下。通常情况下,这种目录结构的时候,这个 struct 的定义是可以对外提供的,有公共的性质。 接下来,我们看一下 impl 的类型 rcl_init_options_impl_s 的信息。 代码块 3 图 3 
它的定义在 rcl/src/rcl 目录下,通常情况下,在 src 目录下定义的结构是模块内部自用的,外部模块是不会用到的。 而且,rcl_init_options_impl_s 内部引用了 rmw_init_options_t,很显然,内部定义引用了 rmw 层的 struct。 我们再回头看代码块 2,看看它这么做有什么好处: 1)typedef 类型别名,可以正常编译; 2)内部用 impl,它的大小固定,是一个指针的大小。这样的话,对于 rclcpp 层来说,rcl_init_options_t 这个 struct 的大小就是固定的。impl 后面如果有更改,不会影响 rclcpp 层的使用,abi 兼容。 3)通过名称,可以明显看出 SDK 的分层设计。 rmw_init_options_t 这个 struct 后面不再展开了,它的用法和原理跟 rcl_init_options_t 是一样的。 3.2 class rclcpp::Context Context 是一个运行时环境信息,是一个动态的,节点运行之后,其内部信息可能会有改变。 第 9 行,rclcpp::InitOptions init_options_; 这个 init_options_ 成员上面刚刚提到过它的类型,在初始化的时候,会保存一个副本。Context 也需要这些信息,从逻辑上 Context 和 InitOptions 的功能是不同的,所以这里并不是多余。 代码块 5 代码块 6 图 4 
可以看到,和 InitOptions 一样,定义 struct 的文件也是在 rcl/include/rcl 目录下。impl 成员使用也是同样的。typedef 和 impl 的原因同 InitOptions 一样,这里就不重复了。 四,rclcpp::Init() 函数原型 虽然之前的一篇 Ros2源码(7)rclcpp::init() 里也提到过这段代码,但是本篇还是会重新提及,只不过不会像之前那样流水账,会挑一些有意思代码说说。 代码块 7 这是 init() 函数的原型,后两个参数有默认参数。我们先看一下 InitOptions 的构造函数: 代码块 8 第 4 行,在 rclcpp 层只对成员赋值一个 0(zero); 第 5 行,调用 rcl 层的接口,做 rcl 层该做的事; 然后我们看看 rcl 层做了什么: 代码块 9 第 6 行,调用的 _rcl_init_options_zero_init() 接口有三句主要代码, 可以看到,也是主要做 rcl 层该做的事,impl 分配内存,然后做最基础赋值和 0 初始化。 第 11 行,调用 rmw_init_options_init() 去初始化属于 rmw 层的信息。 代码块 10 代码块 11 这里先简单提一下,eprosima_fastrtps_identifier 是个固定的字符串: 代码块 12 它是 dds 中间件的一个标记字符串。 以上 代码块 8 到 代码块 11 是 InitOptions::InitOptions() 所做的初始化操作,对初始化设置做一个静态的初始化。这些信息在程序运行时是不会再发生变化的。 五,rclcpp::Init() 函数实现 代码块 13 这里简单提一下,init_options 传入的实参是前面提到的默认构造函数生成的,后面还会经常看到它。 5.1 context 的初始化部分 代码块 14 第 10 行,创建一个 rcl_context_t 对象;第 15 行,对创建的 rcl_context_t 对象做 rcpcpp 成的基本初始化,成员赋 0 值; 第 17 行,对 rcl_context_t 对象做 rcl 层的初始化操作,是真正的初始化; 第 26 行,初始化完毕,和成员变量做交换,相当于对成员变量做初始化; 第 28 ~ 51 行,做日志初始化。get_logging_reference_count() 返回一个 static 变量,初始值为 0。所以,这里说明,日志只能初始化一次,它是进程级别的,单例。 第 57 行,传入的参数 init_options 做拷贝,拷贝到本地数据成员。这里已经能隐约体现出一点 init_options 是静态初始化选项,而 context 是动态。 第 58 ~ 59 行,将 Context 存储到一个全局的 vector 之中。 接下来,我们详细看看 rcl_init(): 代码块 15 第 15 行,context 的 global_arguments 做 0 值初始化; 第 19 行,context 的 impl 分配空间; 第 24 行,initoptions 拷贝到 context 本地; 第 33 ~ 61 行,处理命令行参数,存储到 context 本地的 global_arguments 数据项内; 第 64 行,获取当前 instance-id,然后对 instance-id 做自增; 第 75 行,保存下一个 instance-id 到 context 本地; 第 79 行,instance-id 保存到 context 的 rmw 数据成员中,保留后续为 rmw 层使用; 第 84 ~ 147 行,domain-id,local-host,安全等参数,也是保存到 context 本地和 rwm 层数据成员; 第 150 行,调用 rmw_init() 函数,然后我们看它的入参,都是 rmw_ 开头的数据成员,也就是说,rmw 层的数据成员,由 rmw 层的接口去处理。职能划分很清晰。 rmw_init() 函数不再展开,其主要工作就是用 rcl 层的初始化信息,初始化 rmw 层的运行时信息。相当于把上层准备好的 rmw_init_options,交给 rmw 层生成真正的 rmw_context,每一层都用自己的接口,初始化自己那一层的运行时。 这里再补充一点:rcl_init() 里参数解析的顺序是【命令行 > 环境变量 > 默认值】。 至此,Context 的初始化部分就已经完成。 通过 Context 的初始化过程,我们已经可以清晰一些情况: 1)Ros2 SDK 内部 struct 和 class 职能划分很清晰; 2)InitOptions 和 Context 一个是静态的,一个的运行时动态的; 3)Context 内部保存一份 InitOptions,是因为要记录当初是用什么参数初始化的,包括 rcl 和 rwm 层有看起来重复的数据成员,也是这个道理; 六,信号处理 代码块 13 的第 12 行: 这是信号处理,我们这篇没有相关内容。后面单独写一篇,我觉得它有两句非常牛的神来之笔,值得把全部信号处理代码都摘出来一起学习。 七,总结 这一篇小文,只能算是对 Ros2 SDK 架构的初探,只是通过 Init() 函数,把它的分层架构设计以及一些优点做个简单概括。后面学习的深入,会再继续分享。 就这样
https://docs.ros.org/en/humble/Concepts/Advanced/About-Internal-Interfaces.html
代码块 1
class InitOptions{public:/// 省略部分代码voidset_domain_id(size_t domain_id);private:std::unique_ptr<rcl_init_options_t> init_options_;bool initialize_logging_{true};};
1)InitOptions 是一个静态的初始化配置信息,其信息配置后,在后续的运行期间不会再发生变化。
2)第 9 行 init_options_ 的类型是 rcl_init_options_t,以 rcl_ 开头,说明它是一个 rcl 层定义的 struct 或者 class。这里,就已经体现出 SDK 的分层理念了。上层引用下层的数据定义,和接口,不必关注下层具体实现。
代码块 2
typedef struct rcl_init_options_impl_s rcl_init_options_impl_t;/// Encapsulation of init options and implementation defined init options.typedef struct rcl_init_options_s{/// Implementation specific pointer.rcl_init_options_impl_t * impl;} rcl_init_options_t;
rcl_init_options_t 内部仅仅一个 impl 成员。
然后,第 1 行代码可以看到 rcl_init_options_impl_t 是另一个类型的别名。我们等一下再去细看别名的详细信息,先看一下 rcl_init_options_t 是在哪个文件中定义的:
图 2

struct rcl_init_options_impl_s{rcl_allocator_t allocator;rmw_init_options_t rmw_init_options;};

代码块 4
classContext : publicstd::enable_shared_from_this<Context>{public:///// 省略部分代码private:std::shared_ptr<rcl_context_t> rcl_context_;rclcpp::InitOptions init_options_;///// 省略部分代码};
typedef struct rcl_context_s{rcl_arguments_t global_arguments;rcl_context_impl_t * impl;RCL_ALIGNAS(8) uint8_t instance_id_storage[RCL_CONTEXT_ATOMIC_INSTANCE_ID_STORAGE_SIZE];} rcl_context_t;
typedef struct rcl_arguments_impl_s rcl_arguments_impl_t;/// Hold output of parsing command line arguments.typedef struct rcl_arguments_s{/// Private implementation pointer.rcl_arguments_impl_t * impl;} rcl_arguments_t;

voidinit(int argc,char const * const * argv,const InitOptions & init_options = InitOptions(),SignalHandlerOptions signal_handler_options = SignalHandlerOptions::All);
InitOptions::InitOptions(rcl_allocator_t allocator): init_options_(new rcl_init_options_t){*init_options_ = rcl_get_zero_initialized_init_options();rcl_ret_t ret = rcl_init_options_init(init_options_.get(), allocator);if (RCL_RET_OK != ret) {rclcpp::exceptions::throw_from_rcl_error(ret, "failed to initialize rcl init options");}}
rcl_ret_trcl_init_options_init(rcl_init_options_t * init_options, rcl_allocator_t allocator){/// 省略部分参数合法性检测代码rcl_ret_t ret = _rcl_init_options_zero_init(init_options,allocator);//// 省略部分代码rmw_ret_t rmw_ret = rmw_init_options_init(&(init_options->impl->rmw_init_options),allocator);//// 省略部分代码return RCL_RET_OK;}
init_options->impl = allocator.allocate(sizeof(rcl_init_options_impl_t), allocator.state);init_options->impl->allocator = allocator;init_options->impl->rmw_init_options = rmw_get_zero_initialized_init_options();
rmw_ret_trmw_init_options_init(rmw_init_options_t * init_options,rcutils_allocator_t allocator){return rmw_fastrtps_shared_cpp::rmw_init_options_init(eprosima_fastrtps_identifier, init_options, allocator);}
rmw_ret_trmw_init_options_init(const char * identifier, rmw_init_options_t * init_options, rcutils_allocator_t allocator){///// 省略参数合法性检测代码init_options->instance_id = 0;init_options->implementation_identifier = identifier;init_options->allocator = allocator;init_options->impl = nullptr;init_options->enclave = NULL;init_options->domain_id = RMW_DEFAULT_DOMAIN_ID;init_options->security_options = rmw_get_default_security_options();init_options->localhost_only = RMW_LOCALHOST_ONLY_DEFAULT;return RMW_RET_OK;}
const char * const eprosima_fastrtps_identifier= "rmw_fastrtps_cpp";
voidinit(int argc,char const * const * argv,const InitOptions & init_options,SignalHandlerOptions signal_handler_options){using rclcpp::contexts::get_global_default_context;get_global_default_context()->init(argc, argv, init_options);// Install the signal handlers.install_signal_handlers(signal_handler_options);}
voidContext::init(int argc, charconst * const * argv,const rclcpp::InitOptions & init_options){std::lock_guard<std::recursive_mutex> init_lock(init_mutex_);if (this->is_valid()) {throw rclcpp::ContextAlreadyInitialized();}this->clean_up();rcl_context_t * context = new rcl_context_t;if (!context) {throw std::runtime_error("failed to allocate memory for rcl context");}*context = rcl_get_zero_initialized_context();rcl_ret_t ret = rcl_init(argc, argv,init_options.get_rcl_init_options(),context);if (RCL_RET_OK != ret) {delete context;rclcpp::exceptions::throw_from_rcl_error(ret, "failed to initialize rcl");}rcl_context_.reset(context, __delete_context);if (init_options.auto_initialize_logging()){logging_mutex_ = get_global_logging_mutex();std::lock_guard<std::recursive_mutex> guard(*logging_mutex_);size_t & count = get_logging_reference_count();if (0u == count){ret = rcl_logging_configure_with_output_handler(&rcl_context_->global_arguments,rcl_init_options_get_allocator(init_options.get_rcl_init_options()),rclcpp_logging_output_handler);if (RCL_RET_OK != ret){rcl_context_.reset();rclcpp::exceptions::throw_from_rcl_error(ret, "failed to configure logging");}}else{// 省略告警日志输出}++count;}try{/// 省略部分参数处理代码init_options_ = init_options;weak_contexts_ = get_weak_contexts();weak_contexts_->add_context(this->shared_from_this());}catch (const std::exception & e){// 省略部分代码}}
rcl_ret_trcl_init(int argc,char const * const * argv,const rcl_init_options_t * options,rcl_context_t * context){rcl_ret_t fail_ret = RCL_RET_ERROR;//// 省略参数合法性检测代码rcl_allocator_t allocator = options->impl->allocator;//// 省略参数合法性检测代码context->global_arguments= rcl_get_zero_initialized_arguments();// Setup impl for context.// use zero_allocate so the cleanup function will not try to clean up uninitialized parts latercontext->impl = allocator.zero_allocate(1, sizeof(rcl_context_impl_t), allocator.state);RCL_CHECK_FOR_NULL_WITH_MSG(context->impl, "failed to allocate memory for context impl", return RCL_RET_BAD_ALLOC);// Zero initialize rmw context first so its validity can by checked in cleanup.context->impl->rmw_context = rmw_get_zero_initialized_context();// Store the allocator.context->impl->allocator = allocator;// Copy the options into the context for future reference.rcl_ret_t ret = rcl_init_options_copy(options,&(context->impl->init_options));if (RCL_RET_OK != ret) {fail_ret = ret; // error message already setgoto fail;}// Copy the argc and argv into the context, if argc >= 0.context->impl->argc = argc;context->impl->argv = NULL;if (0 != argc && argv != NULL){context->impl->argv = (char **)allocator.zero_allocate(argc, sizeof(char *), allocator.state);RCL_CHECK_FOR_NULL_WITH_MSG(context->impl->argv,"failed to allocate memory for argv",fail_ret = RCL_RET_BAD_ALLOC; goto fail);int64_t i;for (i = 0; i < argc; ++i) {size_t argv_i_length = strlen(argv[i]) + 1;context->impl->argv[i] = (char *)allocator.allocate(argv_i_length, allocator.state);RCL_CHECK_FOR_NULL_WITH_MSG(context->impl->argv[i],"failed to allocate memory for string entry in argv",fail_ret = RCL_RET_BAD_ALLOC; goto fail);memcpy(context->impl->argv[i], argv[i], argv_i_length);}}// Parse the ROS specific arguments.ret = rcl_parse_arguments(argc, argv,allocator,&context->global_arguments);if (RCL_RET_OK != ret) {fail_ret = ret;RCUTILS_LOG_ERROR_NAMED(ROS_PACKAGE_NAME, "Failed to parse global arguments");goto fail;}// Set the instance id.uint64_t next_instance_id =rcutils_atomic_fetch_add_uint64_t(&__rcl_next_unique_id, 1);if (0 == next_instance_id) {// Roll over occurred, this is an extremely unlikely occurrence.RCL_SET_ERROR_MSG("unique rcl instance ids exhausted");// Roll back to try to avoid the next call succeeding, but there's a data race here.rcutils_atomic_store(&__rcl_next_unique_id, -1);goto fail;}rcutils_atomic_store((atomic_uint_least64_t *)(&context->instance_id_storage),next_instance_id);context->impl->init_options.impl->rmw_init_options.instance_id = next_instance_id;size_t * domain_id = &context->impl->init_options.impl->rmw_init_options.domain_id;if (RCL_DEFAULT_DOMAIN_ID == *domain_id) {// Get actual domain id based on environment variable.ret = rcl_get_default_domain_id(domain_id);if (RCL_RET_OK != ret) {fail_ret = ret;goto fail;}}rmw_localhost_only_t * localhost_only =&context->impl->init_options.impl->rmw_init_options.localhost_only;if (RMW_LOCALHOST_ONLY_DEFAULT == *localhost_only) {// Get actual localhost_only value based on environment variable, if needed.ret = rcl_get_localhost_only(localhost_only);if (RCL_RET_OK != ret) {fail_ret = ret;goto fail;}}if (context->global_arguments.impl->enclave) {context->impl->init_options.impl->rmw_init_options.enclave = rcutils_strdup(context->global_arguments.impl->enclave,context->impl->allocator);} else {context->impl->init_options.impl->rmw_init_options.enclave = rcutils_strdup("/", context->impl->allocator);}if (!context->impl->init_options.impl->rmw_init_options.enclave) {RCL_SET_ERROR_MSG("failed to set context name");fail_ret = RCL_RET_BAD_ALLOC;goto fail;}int validation_result;size_t invalid_index;ret = rcl_validate_enclave_name(context->impl->init_options.impl->rmw_init_options.enclave,&validation_result,&invalid_index);if (RCL_RET_OK != ret) {RCL_SET_ERROR_MSG("rcl_validate_enclave_name() failed");fail_ret = ret;goto fail;}if (RCL_ENCLAVE_NAME_VALID != validation_result) {RCL_SET_ERROR_MSG_WITH_FORMAT_STRING("Enclave name is not valid: '%s'. Invalid index: %zu",rcl_enclave_name_validation_result_string(validation_result),invalid_index);fail_ret = RCL_RET_ERROR;goto fail;}rmw_security_options_t * security_options =&context->impl->init_options.impl->rmw_init_options.security_options;ret = rcl_get_security_options_from_environment(context->impl->init_options.impl->rmw_init_options.enclave,&context->impl->allocator,security_options);if (RCL_RET_OK != ret) {fail_ret = ret;goto fail;}// Initialize rmw_init.rmw_ret_t rmw_ret = rmw_init(&(context->impl->init_options.impl->rmw_init_options),&(context->impl->rmw_context));if (RMW_RET_OK != rmw_ret) {RCL_SET_ERROR_MSG(rmw_get_error_string().str);fail_ret = rcl_convert_rmw_ret_to_rcl_ret(rmw_ret);goto fail;}TRACEPOINT(rcl_init, (const void *)context);return RCL_RET_OK;fail:__cleanup_context(context);return fail_ret;}
install_signal_handlers(signal_handler_options);