文章目录
- 概述
- ros2/rcutils/src/error_handling
- 模块自身异常处理
- 错误状态结构与存储
- 本模块初始化
- 错误状态的设置
- 错误状态的获取
- 错误状态的清理
- 不丢失旧错误状态
- 把手段还原为目的
- 其他
概述
本文从如下几个方面对 ROS2.0 中 rcutils 库 error_handling 错误处理模块的源码进行解读:如何定义结构化错误信息、如何格式化错误信息、如何利用线程局部存储来支持多线程错误信息记录、如何设置和获取当前线程的错误状态和信息、如何清理旧错误状态、如何保证旧错误状态不被覆盖丢失、如何定义工具宏来简化编码过程。在此源码研习的基础上,对其设计思路形成一个大致的理解。对异常处理设计的进一步讨论将在《异常处理/非业务异常处理的设计和实现思路》文中展开。
@History,
截止20231002落笔本段,我并没有亲身使用ROS2的经历,只因对其异常处理部分感兴趣,而对其进行了小范围的研习。我在分析 Windows GetLastError 机制时,随缘的切换到了 rcutils 库错误处理模块的分析, 后来在整理《异常处理/非业务异常处理的设计和实现思路》的过程中,为了避免文章庞大繁杂、主次不清,分离出此篇文章。
如转载本文,请标明出处,
https://blog.csdn.net/quguanxin/category_12449068.html
ros2/rcutils/src/error_handling
如下,是其在Github上的预览,
rcutils 库是 “ROS C Utilities” 的缩写,即表示 rcutils 是 ROS 2.0 中实现通用C语言工具和功能的库(不是C++哦)。此次关注的 error_handling 异常处理模块仅仅是其中很小的部分功能,其提供了一种统一错误处理机制,方便开发者进行错误检查和错误消息的获取,并提供了一些宏和函数来简化错误处理的代码编写。源代码文件 github:ros2/rcutils/src/error_handling.h 和 github:ros2/rcutils/src/error_handling.c,可在Github浏览详细信息。接下来的章节中,参考数据和接口声明,详尽解读源码。
模块自身异常处理
没有想到一个十分满意的标题,所谓模块自身异常处理,是指,当rcutils 异常处理模块的接口被不规范调用时,才会执行到这部分代码。但实际上,这部分函数是完全可以独立地、直接地,使用在程序中用以调试过程的。该部分主要,涉及到下文中描述的4个宏函数,它们的本质功能都是向STDERR标准错误流输出程序的错误信息。
fwrite 是 C 语言标准库提供的一个函数,用于将数据块写入指定的文件流。它的原型如下:
//ptr#指向要写入的数据的指针 size#每个数据项的大小 count#要写入的数据项的数量 stream#要写入的文件流
size_t fwrite(const void *ptr, size_t size, size_t count, FILE *stream);
通过将错误消息写入 STDERR 流,我们可以将错误信息输出到标准错误流,这通常会在控制台或日志文件中显示。在默认情况下,STDERR 与控制台相关联,错误消息将直接显示在控制台上。但可以通过重定向标准错误流,可以将错误消息重定向到文件中,这在生产环境中很常见,开发人员可以将错误信息记录到日志文件中以进行故障排除和错误分析。暂不做扩展。
#ifdef __STDC_LIB_EXT1__
#define RCUTILS_SAFE_FWRITE_TO_STDERR(msg) \
do {fwrite(msg, sizeof(char), strnlen_s(msg, 4096), stderr);} while (0)
#else
#define RCUTILS_SAFE_FWRITE_TO_STDERR(msg) \
do {fwrite(msg, sizeof(char), strlen(msg), stderr);} while (0)
#endif
/// 对前文函数的进一步封装,借助 snprintf 可编程函数,生成格式化字符串,并写入标准错误流
/// @note 如果待写入的格式化字符串长度大于 RCUTILS_ERROR_MESSAGE_MAX_LENGTH,将会被没有明显征兆的截断丢弃
#define RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING(format_string, ...) \
do { \
char output_msg[RCUTILS_ERROR_MESSAGE_MAX_LENGTH]; \
int ret = rcutils_snprintf(output_msg, sizeof(output_msg), format_string, __VA_ARGS__); \
if (ret < 0) { \
RCUTILS_SAFE_FWRITE_TO_STDERR("Failed to call snprintf for error message formatting\n"); \
} else { \
RCUTILS_SAFE_FWRITE_TO_STDERR(output_msg); \
} \
} while (0)
如果可变参函数不够了解,可以参考《语言基础 /C&C++ 可变参函数设计与实践,变参函数的实现、使用、替代方法》 和 《异常处理/LINE 与 FILE 宏在调试和异常处理中的高级使用》 相关文章的讲述。rcutils_snprintf 等函数的实现也比较简单,有需要可直接从 Github 搞到。
/// Write the given msg out to stderr, limiting the buffer size in the `fwrite`, appending the previous error.
/**
* 当编译器支持C11标准中的库扩展功能时,__STDC_LIB_EXT1__宏将被定义为一个非零值,此时fwrite可以指定最大写入长度。
* 而 _SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING 更是明确限制了格式化字符串的缓冲区长度为RCUTILS_ERROR_MESSAGE_MAX_LENGTH
* 获取之前的错误信息,并将其重置;将msg写入stderr;以格式化字符串的形式,将之前的错误信息追加到消息后面。
* This will reset the previous error, if it exists. If there is no previous error, has same behavior as RCUTILS_SAFE_FWRITE_TO_STDERR.
*/
#define RCUTILS_SAFE_FWRITE_TO_STDERR_AND_APPEND_PREV_ERROR(msg) \
do { \
rcutils_error_string_t error_string = rcutils_get_error_string(); \
rcutils_reset_error(); \
RCUTILS_SAFE_FWRITE_TO_STDERR(msg); \
RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING(": %s", error_string.str); \
} while (0)
/// Set the error message to stderr using a format string and format arguments, appending the previous error.
/**
* 该宏与RCUTILS_SAFE_FWRITE_TO_STDERR_AND_APPEND_PREV_ERROR几乎一致,只是将待设置的msg换做了format_string用户格式化串
* \param[in] format_string The string to be used as the format of the error message.
* \param[in] ... Arguments for the format string.
*/
#define RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING_AND_APPEND_PREV_ERROR(format_string, ...) \
do { \
rcutils_error_string_t error_string = rcutils_get_error_string(); \
rcutils_reset_error(); \
RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING(format_string, __VA_ARGS__); \
RCUTILS_SAFE_FWRITE_TO_STDERR_WITH_FORMAT_STRING(": %s", error_string.str); \
} while (0)
无论是向模块内部存储写入错误信息,还是如上向STDERR写入,error_handling 都提供了 “在新msg后追加旧msg” 的操作功能。把新信息写在靠前的位置,旧信息写在后边,这也符合我们常见到的异常运行堆栈信息的格式。
错误状态结构与存储
理解了该异常处理模块的数据结构定义,其模块设计和实现机制也就理解了大半。
/// The maximum length a formatted number is allowed to have. //参见_error_state_t结构中line_number字段,表示代码行的行号
#define RCUTILS_ERROR_STATE_LINE_NUMBER_STR_MAX_LENGTH 20 // "18446744073709551615"
/// The maximum number of formatting characters allowed. //格式化字符即', at :'含空格共6个字符
#define RCUTILS_ERROR_FORMATTING_CHARACTERS 6 // ', at ' + ':'
/// The maximum formatted string length.
#define RCUTILS_ERROR_MESSAGE_MAX_LENGTH 1024
/// The maximum length for user defined error message @大河qu @CSDN
/** 链装形式的错误信息:按调用堆栈顺序叠加起来的错误信息
* Remember that "chained" errors will include previously specified file paths
* e.g. "some error, at /path/to/a.c:42, at /path/to/b.c:42"
*/
#define RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH 768
/* !@The calculated maximum length for the filename. //'错误记录位置'所在的代码文件的名称
* With RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH = 768, RCUTILS_ERROR_STATE_FILE_MAX_LENGTH == 229
*/
#define RCUTILS_ERROR_STATE_FILE_MAX_LENGTH \
(RCUTILS_ERROR_MESSAGE_MAX_LENGTH - RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH - RCUTILS_ERROR_STATE_LINE_NUMBER_STR_MAX_LENGTH - RCUTILS_ERROR_FORMATTING_CHARACTERS - 1)
/// Struct wrapping a fixed-size c string used for returning the formatted error string.
typedef struct rcutils_error_string_s {
/// The fixed-size C string used for returning the formatted error string.
char str[RCUTILS_ERROR_MESSAGE_MAX_LENGTH];
} rcutils_error_string_t; //该结构的长度与rcutils_error_state_t长度相同
/// Struct which encapsulates the error state set by RCUTILS_SET_ERROR_MSG().
typedef struct rcutils_error_state_s {
/// User message storage, limited to RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH characters.
char message[RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH];
/// __FILE__宏代表的代码文件名 File name, limited to what's left from RCUTILS_ERROR_STATE_MAX_SIZE characters after subtracting storage for others.
char file[RCUTILS_ERROR_STATE_FILE_MAX_LENGTH];
/// __LINE__宏代表的代码行号 Line number of error.
uint64_t line_number;
} rcutils_error_state_t; //该结构的长度与rcutils_error_string_t长度相同
// make sure our math is right... //编译时进行静态断言from C++ 11 @大河qu @CSDN @https://blog.csdn.net/quguanxin/category_12449068.htm
#if __STDC_VERSION__ >= 201112L
static_assert(
sizeof(rcutils_error_string_t) == ( /* 1024 == 768 + 229 + 20 + 6 + 1(null terminating character) */
RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH + RCUTILS_ERROR_STATE_FILE_MAX_LENGTH + RCUTILS_ERROR_STATE_LINE_NUMBER_STR_MAX_LENGTH + RCUTILS_ERROR_FORMATTING_CHARACTERS + 1),
"Maximum length calculations incorrect");
#endif
注释都写在代码里头了,不再过多的说明。这里定义了两种错误信息存储结构,rcutils_error_string_t 和 rcutils_error_state_t,其中前者是一个1024字节长度的字符串,后者是一个包含文件名、代码行号、状态描述的结构体。RCUTILS_ERROR_STATE_MESSAGE_MAX_LENGTH 的值是运算出来的,而不是拍脑袋出来的,自叹不如,自己写的代码里,没有这么严谨。结构 rcutils_error_state_t 用来记录和传递错误状态,以便进行错误处理和故障排除,而 rcutils_error_string_t 这个C字符串主要目的是用来存储错误消息,以便在需要时进行显示、日志记录或其他处理。在模块内部,状态格式的错误信息,最终通过格式化过程转换为字符串形式的错误信息,这种设计可以分离错误状态的管理和错误消息的存储,提供更灵活和模块化的错误处理能力。
本模块中的,所谓的格式化消息,有两种含义。其一是,上文提到的,将 rcutils_error_state_t 结构化消息,格式化为 rcutils_error_string_t 消息。其二,_WITH_FORMAT_STRING 系列宏函数体现出来的功能,在设置错误状态信息时,其利用 可变餐函数 rcutils_snprintf 直接将调用位置的上下文信息格式化到 format_string 中,作为实参传递给 rcutils_error_state_t 对象的 message 字段。
本模块初始化
与 Windows GetLastError机制一样,rcutils 库 error_handling 模块也使用了线程局部存储。在同一专栏下《异常处理/WSAGetLastError和FormatMessage分析与实践》有相关介绍可参考(可能尚未发布)。线程局部变量是一种特殊类型的变量,每个线程都有自己独立的变量副本,可以独立地访问和修改自己的变量,而不会影响其他线程的变量。使用__declspec(thread)修饰符,可以将变量声明为线程局部变量。在该单元的实现中,定义了如下线程局部变量,
#define RCUTILS_THREAD_LOCAL __declspec(thread)
// g_ is to global variable, as gtls_ is to global thread-local storage variable
RCUTILS_THREAD_LOCAL bool gtls_rcutils_thread_local_initialized = false; //是否已执行线程局部资源初始化过程
RCUTILS_THREAD_LOCAL rcutils_error_state_t gtls_rcutils_error_state; //线程局部对象,以error_state结构存储的错误信息
RCUTILS_THREAD_LOCAL bool gtls_rcutils_error_string_is_formatted = false; //用以判断error_state是否格式化成了error_string
RCUTILS_THREAD_LOCAL rcutils_error_string_t gtls_rcutils_error_string; //线程局部对象,以error_string格式化字符串存储的错误信息
RCUTILS_THREAD_LOCAL bool gtls_rcutils_error_is_set = false; //新状态被设置后,点亮'新错误状态已设置'标记
上述线程局部变量的初始化,主要在如下rcutils_initialize_error_handling_thread_local_storage函数中执行,
/// Forces initialization of thread-local storage if called in a newly created thread. /如果在新创建的线程中调用此函数,则强制初始化线程局部存储
/**
* 如果没有预先调用此函数,那么当首次设置 error state 或者 首次retrieve获取 error message 时,将使用默认分配器来分配线程局部存储。
*
* 此函数可能会分配内存,也可能不会分配内存。线程局部存储是用来在每个线程中存储特定数据的机制。
* 通常系统的线程局部存储实现在创建线程时可能需要分配内存来存储这些数据。由于系统无法事先知道将创建多少个线程,因此无法确定需要多少内存空间来存储线程的局部数据。
* 大多数实现(如C11、C++11和pthread)没有办法指定如何分配此内存,但如果实现允许,将使用此函数的给定分配器,否则将不使用。
* 通过重用线程池技术,可以避免在 "steady" 阶段发生线程局部存储内存分配,而使得分配过程仅发生在线程的创建和销毁阶段。 river.qu @ CSDN
* 值得考虑的是,重复创建和销毁线程将导致重复的内存分配,并可能导致内存碎片化。通常通过使用线程池来避免这种情况。
*
* In case an error is indicated by the return code, no error message will have been set. /一旦该函数返回错误代码,则不会设置任何错误消息。
*
* 如果该函数在一个线程中调用多次,或者在setting the error state过程中被隐式初始化后,它将仍然返回`RCUTILS_RET_OK`,即使输入的allocator是无效的。
* 本质上Essentially,如果线程局部存储已经被调用,此函数不会执行任何操作。如果已经初始化,将忽略给定的分配器,即使它与最初用于初始化线程局部存储的分配器不匹配。
* @note 关于内存分配器和线程局部存储的详细内容,在代码外的说明中会继续深入
*
* \param[in] allocator to be used to allocate and deallocate memory
* \return #RCUTILS_RET_OK if successful, or #RCUTILS_RET_INVALID_ARGUMENT if the allocator is invalid, or
* \return #RCUTILS_RET_BAD_ALLOC if allocating memory fails, or #RCUTILS_RET_ERROR if an unspecified error occurs.
*/
RCUTILS_PUBLIC RCUTILS_WARN_UNUSED rcutils_ret_t rcutils_initialize_error_handling_thread_local_storage(rcutils_allocator_t allocator);
rcutils_ret_t rcutils_initialize_error_handling_thread_local_storage(rcutils_allocator_t allocator) {
if (gtls_rcutils_thread_local_initialized) { //允许重复调用,不做任何处理。防止对客户端造成不必要的干扰。
return RCUTILS_RET_OK;
}
//注意思路,当出现了比异常处理模块更底层的错误时,将错误信息输出到stderr,并返回错误码
if (!rcutils_allocator_is_valid(&allocator)) { // check if the given allocator is valid
#if RCUTILS_REPORT_ERROR_HANDLING_ERRORS
RCUTILS_SAFE_FWRITE_TO_STDERR(
"[rcutils|error_handling.c:" RCUTILS_STRINGIFY(__LINE__)
"] rcutils_initialize_error_handling_thread_local_storage() given invalid allocator\n");
#endif
return RCUTILS_RET_INVALID_ARGUMENT; //返回代表'无效参数'的错误码
}
// right now the allocator is not used for anything but other future implementations may need to use it //在未来的一些实现中会使用到allocator内存分配器
// e.g. pthread which could only provide thread-local pointers would need to allocate memory to which those pointers would point
// forcing the values back to their initial state should force the thread-local storage to initialize and do any required memory allocation
gtls_rcutils_thread_local_initialized = true;
rcutils_reset_error(); //包含重置除上一行代码中tls以外的其他全部tls变量
RCUTILS_SET_ERROR_MSG("no error - initializing thread-local storage");
rcutils_error_string_t throw_away = rcutils_get_error_string();
(void)throw_away; //A variable that is not used
rcutils_reset_error();
// at this point the thread-local allocator, error state, and error string are all initialized
return RCUTILS_RET_OK;
} //https://blog.csdn.net/quguanxin/category_12449068.html @大河qu
在 error_handling 单元中,内存分配器 rcutils_allocator_t allocator 当前是未使用的,因为在大多数实现(如C11、C++11和pthread)没有办法指定如何分配此内存,但如果实现允许,将使用此函数的给定分配器,否则将不使用。下个月,又要开始一段时间的嵌入式软件的开发,用到FreeRTOS,我想移植这个模块去使用,那时候,可能要改造这个函数,会有类似内存分配器的操作…
错误状态的设置
本模块的对外接口中,错误状态的设置接口都是宏函数,但根本上是如下函数的封装,其中 参数 const char * file, size_t line_number 最终以 LINE 和 FILE 为实参,相关的用法可以参考 《异常处理/LINE 与 FILE 宏在调试和异常处理中的高级使用》 这篇已发布的文章。
/// Set the error message, as well as the file and line on which it occurred. 设置错误信息,以及发生该错误的代码文件名称和代码行号
/**
* This is not meant to be used directly, but instead via the RCUTILS_SET_ERROR_MSG(msg) macro. //该函数通过宏在客户端使用
* The error_msg parameter is copied into the internal error storage and must be null terminated.
* The file parameter is copied into the internal error storage and must be null terminated.
*
* \param[in] error_string The error message to set.
* \param[in] file The path to the file in which the error occurred.
* \param[in] line_number The line number on which the error occurred.
*/
RCUTILS_PUBLIC void rcutils_set_error_state(const char * error_string, const char * file, size_t line_number);
函数实现,
void rcutils_set_error_state(const char * error_string, const char * file, size_t line_number)
{
rcutils_error_state_t error_state;
if (NULL == error_string) { //通过stderror输出参数异常
#if RCUTILS_REPORT_ERROR_HANDLING_ERRORS
RCUTILS_SAFE_FWRITE_TO_STDERR(
"[rcutils|error_handling.c:" RCUTILS_STRINGIFY(__LINE__)
"] rcutils_set_error_state() given null pointer for error_string, error was not set\n");
#endif
return;
}
if (NULL == file) { //通过stderror输出参数异常
#if RCUTILS_REPORT_ERROR_HANDLING_ERRORS
RCUTILS_SAFE_FWRITE_TO_STDERR(
"[rcutils|error_handling.c:" RCUTILS_STRINGIFY(__LINE__)
"] rcutils_set_error_state() given null pointer for file string, error was not set\n");
#endif
return;
}
//note,这里的error_state是临时变量,并不是最终模块内存储错误状态的线程局部变量
__rcutils_copy_string(error_state.message, sizeof(error_state.message), error_string);
__rcutils_copy_string(error_state.file, sizeof(error_state.file), file);
error_state.line_number = line_number;
#if RCUTILS_REPORT_ERROR_HANDLING_ERRORS
// Only warn of overwritting if the new error is different from the old ones.
size_t characters_to_compare = strnlen(error_string, RCUTILS_ERROR_MESSAGE_MAX_LENGTH);
// assumption is that message length is <= max error string length
static_assert(
sizeof(gtls_rcutils_error_state.message) <= sizeof(gtls_rcutils_error_string.str),
"expected error state's max message length to be less than or equal to error string max");
// 如果旧消息状态尚未reset,且新旧消息存在不同,则通过stderr进行告警输出
if (gtls_rcutils_error_is_set && !__same_string(error_string, gtls_rcutils_error_string.str, characters_to_compare) && !__same_string(error_string, gtls_rcutils_error_state.message, characters_to_compare))
{
char output_buffer[4096];
__format_overwriting_error_state_message(output_buffer, sizeof(output_buffer), &error_state);
RCUTILS_SAFE_FWRITE_TO_STDERR(output_buffer);
}
#endif
//新设置的状态信息,无论是否有变动,都更新存储到线程局部变量中
gtls_rcutils_error_state = error_state;
//新状态被设置后,置零格式化字符串标记
gtls_rcutils_error_string_is_formatted = false;
//新状态被设置后,并清空之前的格式化字符串存储
gtls_rcutils_error_string = (const rcutils_error_string_t) { .str = "\0" };
//新状态被设置后,点亮'新错误状态已设置'标记
gtls_rcutils_error_is_set = true;
}
客户端最终使用的接口,宏函数,如下,
/// Set the error message, as well as append the current file and line number. 该宏函数是本模块的最基础的接口。
/**
* 如果先前已经设置了一个错误信息,并且在那之后没有调用过rcutils_reset_error重置操作,
* 且rcutils库是在RCUTILS_REPORT_ERROR_HANDLING_ERRORS宏开关开启的情况下编译的,则,先前的消息将通过stderr打印输出,(old_error_string + new_error_string + ...)
* Error state 是线程本地存储的,因此,相关联的所有函数也是(符合)线程本地(存储机制)的。
* \param[in] msg The error message to be set.
*/
#define RCUTILS_SET_ERROR_MSG(msg) \
do {rcutils_set_error_state(msg, __FILE__, __LINE__);} while (0)
/// Set the error message using a format string and format arguments. //使用格式化字符串和参数设置错误信息
/**
* 这个宏是对上述RCUTILS_SET_ERROR_MSG宏的扩展,即,将msg扩展为变参数构成的格式化字符串。
* 注意,不要将这里的格式化字符串并不是直接设置到gtls_rcutils_error_string中,而是生成用户定义的格式化字符串并以state-msg格式设置
* This function sets the error message using the given format string.
* The resulting formatted string is silently truncated at RCUTILS_ERROR_MESSAGE_MAX_LENGTH. //作为结果的格式化字符串会被默默截断
* \param[in] format_string The string to be used as the format of the error message.
* \param[in] ... Arguments for the format string.
* 一个调用案例如下,
* RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING("_putenv_s failed: %d", errno);
*/
#define RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING(format_string, ...) \
do { \
char output_msg[RCUTILS_ERROR_MESSAGE_MAX_LENGTH]; \
int ret = rcutils_snprintf(output_msg, sizeof(output_msg), format_string, __VA_ARGS__); \
if (ret < 0) { \
RCUTILS_SAFE_FWRITE_TO_STDERR("Failed to call snprintf for error message formatting\n"); \
} else { \
RCUTILS_SET_ERROR_MSG(output_msg); \
} \
} while (0)
一个感觉上很高级的宏的使用方案。也是一个很好的便利接口,设置错误信息,并附加一个返回值,
/// Indicate that the function intends to set an error message and return an error value.
/**
* \def RCUTILS_CAN_SET_MSG_AND_RETURN_WITH_ERROR_OF
* Indicating macro similar to RCUTILS_CAN_RETURN_WITH_ERROR_OF, that also sets an error message. /指示宏Indicating macro,是一种宏定义.
* 其作用在作用是向代码中插入一个特殊的标记,以便在编译时或运行时检测这个标记,并采取相应的处理逻辑。这些宏可能用于条件编译、错误处理、日志记录等方面。
* 而RCUTILS_CAN_RETURN_WITH_ERROR_OF定义在fault_injection模块,用以在测试环境中模拟和注入各种故障条件。
* 目前,那么这个宏仅简单地依赖于 fault_injection模块的RCUTILS_CAN_FAIL_WITH,设置通用错误信息并返回给定的error_return_value,且要求fault injection是开启的。
* \param error_return_value the value returned as a result of a given error.
*/
#define RCUTILS_CAN_SET_MSG_AND_RETURN_WITH_ERROR_OF(error_return_value) \
RCUTILS_CAN_FAIL_WITH( \
{ \
RCUTILS_SET_ERROR_MSG("Injecting " RCUTILS_STRINGIFY(error_return_value)); \
return error_return_value; \
})
两个判断参数有效性的便利接口,
/// Check an argument for a null value.
/**
* If the argument's value is `NULL`, set the error message saying so and return the `error_return_type`.
* 宏定义语法中的 #被称为字符串化运算符,用作将宏参数转换为字符串字面值
* \param[in] argument The argument to test.
* \param[in] error_return_type The type to return if the argument is `NULL`.
* 该函数的一个用例如下,
* rcutils_string_map_set(rcutils_string_map_t * string_map, const char * key, const char * value)
* RCUTILS_CHECK_ARGUMENT_FOR_NULL(value, RCUTILS_RET_INVALID_ARGUMENT); //#define RCUTILS_RET_INVALID_ARGUMENT 11
*/
#define RCUTILS_CHECK_ARGUMENT_FOR_NULL(argument, error_return_type) \
RCUTILS_CHECK_FOR_NULL_WITH_MSG( \
argument, #argument " argument is null", \
return error_return_type)
/// Check a value for null, with an error message and error statement. /通过错误信息和错误语句判定一个值是否为空
/**
* @ evaluate 评价,评估,估值;求(方程式,公式,函数)的数值,这里引申为'衡量'的意思
* If `value` is `NULL`, the error statement will be evaluated after setting the error message.
* \param[in] value The value to test.
* \param[in] msg The error message if `value` is `NULL`.
* \param[in] error_statement The statement语句 to evaluate if `value` is `NULL`. /衡量value是否为空的语句。
* 按照RCUTILS_CHECK_ARGUMENT_FOR_NULL中的调用,该参数被填充为一个return语句。
*/
#define RCUTILS_CHECK_FOR_NULL_WITH_MSG(value, msg, error_statement) \
do { \
if (NULL == value) { \
RCUTILS_SET_ERROR_MSG(msg); \
error_statement; \
} \
} while (0)
错误状态的获取
/// Return an rcutils_error_state_t which was set with rcutils_set_error_state().
/** 大河qu @ CSDN
* The returned pointer will be NULL if no error has been set in this thread. //返回空?
* 该函数返回的是模块内部存储错误状态的线程局部变量gtls_rcutils_error_state的地址,tls变量是在具体线程首次调用其相关函数时延迟创建的,
* 这种延时机制确保了每个线程都有自己的独立副本,并在需要时进行创建。因此在某线程内没有任何调用时,返回NULL空地址。
* 函数返回的const状态对象指针始终有效,除非在同一线程内调用RCUTILS_SET_ERROR_MSG, rcutils_set_error_state, rcutils_reset_error
* \return A pointer to the current error state struct. //注意函数返回的指针是const类型
*/
RCUTILS_PUBLIC RCUTILS_WARN_UNUSED const rcutils_error_state_t *rcutils_get_error_state(void);
/// Return the error message followed by `, at <file>:<line>` if set, else "error not set".
/**
* 该函数是安全的,因为其返回的是当前错误信息的副本(copy),或者是返回"error not set"字符串。
* 这样确保了副本由调用线程拥有,使得其不会被其他错误处理调用invalidated无效化,并且返回值值中的C字符串始终有效且以空字符结尾。
* \return The current error string, with file and line number, or "error not set" if not set. //char str[n];
*/
RCUTILS_PUBLIC RCUTILS_WARN_UNUSED rcutils_error_string_t rcutils_get_error_string(void);
函数实现,
rcutils_error_string_t rcutils_get_error_string(void)
{
//_error_is_set只在_set_error_state函数中有设置操作,即,所有客户端的错误信息设置操作只能以state形式设置到本单元
if (!gtls_rcutils_error_is_set) {
return (rcutils_error_string_t) {"error not set"}; // NOLINT(readability/braces)
}
//检查_error_state是否已格式化为_error_string
if (!gtls_rcutils_error_string_is_formatted) {
__rcutils_format_error_string(>ls_rcutils_error_string, >ls_rcutils_error_state);
gtls_rcutils_error_string_is_formatted = true;
}
return gtls_rcutils_error_string;
}
错误状态的清理
在理解 rcutils 异常处理基本实现机制前,错误状态的清理过程,是我一个非常关注的点。因为在此之前,在按照自己的思路编写异常处理单元、或者研究 Windows GetLastError 机制时,我都没有很好的理解如下问题:依照函数调用堆栈顺序,如果多个调用层次上都发生了错误,那么底层的错误不就很容易被上层错误给覆盖掉吗?或者,有什别的手段,如上层会对底层的错误信息进行处理或收敛操作,然后抛出新错误?绕的我有点头大。
/// Return `true` if the error is set, otherwise `false`.
/**
* 该函数返回的是 gtls_rcutils_error_is_set,该标记主要用以,
* 1、在新错误状态设置操作时,检查是否有旧状态未明确reset清理,提示有覆盖风险,并通过stderr输出旧+新错误信息
* 2、在已reset错误状态的情况下,使得 _get_error_string 返回 "error not set" 信息
*/
RCUTILS_PUBLIC RCUTILS_WARN_UNUSED bool rcutils_error_is_set(void);
一个错误信息被设置后,在下一个错误被设置前,该记录被期望尽快取走,并执行重置操作。
/// Reset the error state by clearing any previously set error state. //在设置新状态前,这是被期望调用的。
RCUTILS_PUBLIC void rcutils_reset_error(void);
函数实现,
//
bool rcutils_error_is_set(void) {
return gtls_rcutils_error_is_set;
}
//
void rcutils_reset_error(void) {
gtls_rcutils_error_state = (const rcutils_error_state_t) {
.message = {0}, .file = {0}, .line_number = 0
}; // NOLINT(readability/braces)
gtls_rcutils_error_string_is_formatted = false;
gtls_rcutils_error_string = (const rcutils_error_string_t) {
.str = "\0"
};
gtls_rcutils_error_is_set = false;
}
不丢失旧错误状态
在前文讲述,向STDERR标准错误流输出错误信息时,我们也提到了,在旧错误信息没有被清理重置的情况下,为了防止信息丢失,本模块的后缀为_AND_APPEND_PREV_ERROR 的接口,会试图在新状态信息后追加旧状态信息。在这种机制下,我们可以记录和追溯发生错误时的函数调用堆栈,这在很多情况下是十分必要的,尤其是对于一些靠近底层的功能模块。
/// Set the error message using RCUTILS_SET_ERROR_MSG and append the previous error. //比较关注的一个方向
/** 大河qu @ CSDN
* 前面已经提到过,_set_error_state过程若检测到上一个状态信息未reset,会通过stderr输出旧+新数据,以提示使用者
* 本宏函数的目的,同样是防止旧错误信息被覆盖丢失。直接允许我们将新+旧信息同时输出。这在分析错误发生时的函数调用堆栈很有用。
* If there is no previous error, has same behavior as RCUTILS_SET_ERROR_MSG.
* \param[in] msg The error message to be set. /旧状态被reset,且新信息输出在前,旧信息在后
* @note #define RCUTILS_EXPAND(x) x /如下使用此宏构建了用以指定输出格式的个数话字符串
*/
#define RCUTILS_SET_ERROR_MSG_AND_APPEND_PREV_ERROR(msg) \
do { \
rcutils_error_string_t error_string = rcutils_get_error_string(); \
rcutils_reset_error(); \
RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING( \
RCUTILS_EXPAND(msg ": %s"), error_string.str); \
} while (0)
/// Set the error message with RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING and append the previous error.
/**
* RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING宏函数将用户层的格式化字符串,通过RCUTILS_SET_ERROR_MSG宏,以错误状态的形式设置到本模块中,
* 并附件了在此之前的最后的错误信息,并且reset重置了这个旧错误状态。作为结果的上述格式化字符串,会以RCUTILS_ERROR_MESSAGE_MAX_LENGTH截断。
* 如果没有旧消息(如执行过reset操作),则该函数的行为与RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING保持一致.
* \param[in] format_string The string to be used as the format of the error message.
* \param[in] ... Arguments for the format string.
*/
#define RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING_AND_APPEND_PREV_ERROR(format_string, ...) \
do { \
rcutils_error_string_t error_string = rcutils_get_error_string(); \
rcutils_reset_error(); \
RCUTILS_SET_ERROR_MSG_WITH_FORMAT_STRING( \
RCUTILS_EXPAND(format_string ": %s"), __VA_ARGS__, error_string.str); \
} while (0)
此外,无论是向STDERR输出还是向模块内部存储输出错误信息,都提供了直接消息msg、j基于变参函数的格式化消息format_msg数据,其中,后者可以包含运行时上下文的值,毫无疑问地将更加有利于问题分析处理。
把手段还原为目的
在阅读上述代码的过程中,可以深切体会到其在异常处理过程中的干脆直接,感叹仅使用__LINE__和__FILE__宏,配合线程局部存储技术,几个简单的接口和宏定义的工具函数,就构建出来一套异常信息记录体系。在 error_handling 中并没有定义错误信息的最终输出方式,可以使用控制台、IDE输出窗、日志记录、消息框、界面提示等。我想表达的一点是,无论该信息以何种形式输出到哪里,它都不太合适作为一种推给用户直接浏览的信息。
大河qu @ CSDN
早些年在设计构建系统的异常处理功能的时候,一个较大的失误在于,总试图统一的管理针对用户层的业务异常和相对底层软硬件的系统错误信息。这直接导致了系统表现出来的异常信息,既不能快速直接反应问题本质,也不是受用户待见的简单明了。
大河qu @ CSDN
站在开发者的角度上,无论是何种形式的异常处理,都是手段,我们的根本目的始终是快速定位程序运行过程中的问题,并尽力地使其从问题中恢复正常运行。而用户,软件的使用者,其希望看到的告警信息应该是,可读性强、及时性好、清晰明了、具体详细的,最好是可操作的,不仅指出问题所在还应该提供解决方案或建议。用户绝不希望看到晦涩难懂的告警信息,而是希望能够快速地理解问题所在,因此给用户的告警信息必须是能简练和准确描述问题本质和原因的。同时若把给用户的信息详细到了哪个源文件、哪个代码行,这一定是背道而驰的。
大河qu @ CSDN
因此,ROS2.0的错误处理模块,更应该归类为是一种调试信息,是针对开发者的,而不是针对最终用户的。也就是说,最终用户不应该直接看到这些信息,而必须是在直接告警信息的更深层解析中。
其他
这次应该算是逐行读的源码,花费了不少时间,但挺值的。对 rcutils 部分源码的学习,也为我CSDN草稿箱中的一些其他文章提供了案例补充,如, 《语言基础 /do { … } while(0) 语句的使用》、《语言基础 /C&C++宏定义的初级和高级使用》、《异常处理 LINE 与 FILE 宏在调试和异常处理中的高级使用》、《异常处理/assert断言应用实践和注意事项》。好了,不早了,先这样吧!