本文结合实际经验,参考ros2 rclcpp库中错误码定义及使用方式,梳理了一种基于C或C++开发的接口下错误码的定义及处理方式。{本文不涉及跨系统、跨服务的定义}
1.错误码如何定义?
系统一般是按模块划分的,模块与模块之间通过调用与被调用的关系,一般也会划分为多个层次,底层一般对接系统级API或者实现一些算法,上层调用底层的接口来处理业务。那么这些错误码如何定义呢?
首先要明白错误码是什么:
在笔者看来,错误码分为通用的系统级错误码和业务错误码,系统级错误码一般包括指针为空、内存分配失败、无效参数、超时等等,业务错误码和具体的模块业务有关系。
我们一般会定义一个头文件,统一放这些错误码,当然错误码也是要按模块来进行划分的,这样既方便了错误码的追溯,又可以让不同的开发人员方便维护自己模块的错误码。比如下面这样:
// 成功
#define ERR_OK 0
/// 通用的错误码定义
// 成功
#define ERR_OK 0
// 超时错误
#define ERR_TIMEOUT 1
// 内存分配失败
#define ERR_BAD_ALLOC 2
// A模块,1xx
#define ERR_XXX 100
#define ERR_XYX 101
// B模块,2xx
#define ERR_XXX2 200
上面这种定义,通用的错误码返回时,无法通过错误码看出是哪个模块产生的错误,那么可以修改为下面的定义方式:
// 成功
#define ERR_OK 0
/// 通用的错误码定义
// 通用错误码最大个数
#define COMMON_ERR_MAX 10
// 超时错误
#define ERR_TIMEOUT 1
// 内存分配失败
#define ERR_BAD_ALLOC 2
// A模块,1xx
//前面几个是通用错误,比如101是A模块的超时错误
#define ERR_A_1 COMMON_ERR_MAX + 100 //业务错误1
#define ERR_A_2 COMMON_ERR_MAX + 101 //业务错误2
// B模块,2xx,比如201是B模块的超时错误
#define ERR_B_1 COMMON_ERR_MAX + 200 //业务错误1
这种方法定义的错误码,在处理错误码时对通用的部分要单独取出来判断。
【注意】某些模块可能是一些通用的模块,不想纳入某个系统的错误码定义约定中,比如某些通用的算法模块,那么此时错误码的定义可以单独进行定义,维护自己的一套规则即可。
2.错误码处理方法
错误码定义完了,如何处理这些错误码是很关键也很头疼的一件事。这里C和C++处理方式可以按C的方式处理,也可以用不同的方法处理。
上层对底层返回的错误码,一般会有三种方法来处理:
- 直接透传到上层
- 错误码收敛后返回到上层
- 采用全局变量errno机制
2.1 C风格错误码分层处理方法
直接上代码:
#define OK 0
#define ERR_1 100
#define ERR_2 101
#define ERR_3 102
int funTop(){
return funMiddle(); //透传
}
int funMiddle{
int iRet = funBottom();
if(iRet == ERR_1 || iRet == ERR_2){
return ERR_2; //过滤转换
}
return OK;
}
int funBottom{
if (xx){
return ERR_1;
}
else{
return ERR_2;
}
if(yy){
return ERR_3;
}
}
int main(){
int iRet = funTop();
}
2.2 C++风格错误码分层处理方法
C++可以和C一样,也可以用try机制进行异常捕获的透传。
上代码:
class MyException : public std::exception{
//定义一个异常处理类,可以透传错误码
MyException(int err_code);
std::string what();
int err_code();
}
#define OK 0
#define ERR_1 100
#define ERR_2 101
#define ERR_3 102
void funTop(){
try{
funMiddle();
}
catch(MyException e){
//上层直接捕获到底层的错误码
e.err_code();
}
}
void funMiddle{
//中间层不处理异常,扔给上层
funBottom();
}
void funBottom{
if (xx){
//底层抛出异常
throw MyException(ERR_1);
}
else{
throw MyException(ERR_2);
}
return OK;
}
2.3 C++调用C
当然,底层可以用C来封装库,上层用CPP再次封装,如rclc和rclcpp的关系。
此时,调用底层时可以通过throw来抛出异常。
class MyException : public std::exception{
//定义一个异常处理类,可以透传错误码
MyException(int err_code);
std::string what();
int err_code();
}
#define OK 0
#define ERR_1 100
#define ERR_2 101
#define ERR_3 102
void funTop(){
try{
funMiddle();
}
catch(MyException e){
e.err_code();
}
}
void funMiddle{
int iRet = funBottom();
if(iRet == ERR_1 || iRet == ERR_2){
throw MyException(ERR_3); //抛出异常并收敛错误代码
}
}
int funBottom{
if (xx){
return ERR_1;
}
else{
return ERR_2;
}
if(yy){
return ERR_3;
}
}
2.4 函数返回值注释
如果采用函数返回错误码的方式来实现,那么函数的注释中,一般会标明该函数返回的错误码。如果底层的错误码没被收敛,透传到上层,那么此时应该把所有的可能错误码包括底层的错误码都列上,这样其实特别繁琐。比如rmw中间件封装库中函数的声明:
/// Allocate a rmw_network_flow_endpoint_array_t instance
/**
* \param[inout] network_flow_endpoint_array array to be allocated
* \param[in] size size of the array to be allocated
* \param[in] allocator the allcator for allocating memory
* \returns `RMW_RET_OK` on successfull initilization, or
* \returns `RMW_RET_INVALID_ARGUMENT` if `network_flow_endpoint_array` or `allocator` is NULL, or
* \returns `RMW_RET_BAD_ALLOC` if memory allocation fails, or
* \returns `RMW_RET_ERROR` when an unspecified error occurs.
* \remark RMW error state is set on failure
*/
RMW_PUBLIC
rmw_ret_t
rmw_network_flow_endpoint_array_init(
rmw_network_flow_endpoint_array_t * network_flow_endpoint_array,
size_t size,
rcutils_allocator_t * allocator);
有没有简单的方法呢?有的,比如rclc的函数声明:
/**
* Creates an rcl publisher with quality-of-service option best effort
* \param[inout] publisher a zero_initialized rcl_publisher_t
* \param[in] node the rcl node
* \param[in] type_support the message data type
* \param[in] topic_name the name of published topic
* \return `RCL_RET_OK` if successful
* \return `RCL_ERROR` (or other error code) if an error has occurred
*/
RCLC_PUBLIC
rcl_ret_t
rclc_publisher_init_best_effort(
rcl_publisher_t * publisher,
const rcl_node_t * node,
const rosidl_message_type_support_t * type_support,
const char * topic_name);
这里直接用or other error code
直接把其他错误码一起包含了,调用方只能按照true和flase的用法来用这个接口了,如果有些场景符合这种true或者false的使用,也未尝不可。
总之这两种方法各有利弊,重点是在使用时,某一层要统一一个规则。
2.5 全局变量errno机制
类似windows或linux,都有lasterror、errno错误码,它是全局变量,且是线程局部变量。
那么什么情况下用这种机制呢?为何会有这种手段?下面是笔者基于经验的猜测:
当模块很多,模块的调用层级也很多的时候,错误码此时也会有非常多,比如操作系统,比如一个SDK,底层产生的错误码,上层可能只判断是否ok,此时又想记录底层的错误码,那么就可以将这个错误码放到全局变量中,再往上抛一个固定的错误码(比如-1)即可。
想一下socket套接字编程,socket编程接口返回值非常有限,当它返回-1时,具体的错误信息是通过errno描述的,如果要从socket接口的返回值拿到这个错误,如果这个错误非常底层,可以想象一下上层处理起来得多么恐怖。当产生错误码时,将错误写到errno,上层的层层调用都返回-1,特别优雅。
3. 错误码追溯机制
层之间调用时,底层的接口可能会被多个中间层调用,底层产生错误时,可能有多种调用路径,那么如何跟踪这些调用路径,也就是说产生一个错误,如何知道错误是怎么一层层产生的呢?这个在某些时候还是非常重要的,如果只拿到底层的错误码,还是无法知道是哪个业务层调用时出的问题。
这里可以参考一下rcl的错误码生成机制,注意这里使用c实现的,如果用c++的异常来实现,可能需要某种特殊的办法来保存每一层的异常代码,否则中间层抛新的异常会把底层抛出的异常覆盖掉。
【核心定义】
//存储错误的结构
typedef struct
{
//错误消息
char chMsg[MAX_LEN];
//文件名
char fileName[MAX_LEN];
//行号
int rowNum;
}TErrInfo;
//全局错误定义
TErrInfo g_ErrInfo;
//根据当前全局错误,拼接错误串
char* get_global_err_format_string(){
//拼接 Msg fileName rowNum
//比如:“bad malloce! pro/app/test.c linenum:555!”
}
//重置错误信息
void reset_global_err(){
//清空g_ErrInfo
}
//设置错误信息
void set_err_info(Msg, fileName, rowNum){
if (Msg != g_ErrInfo){
//【关键处理1】如果错误信息和上次不同,则直接输出打印上次的错误
printf("last err: %s", get_global_err_format_string());
}
//更新全局错误信息 (示意代码)
g_ErrInfo.Msg = Msg;
g_ErrInfo.fileName;
g_ErrInfo.rowNum;
}
【如何使用】
//main中简单使用
int main(){
set_err_info("aaa");
//下面这句会打印上次的错误信息 aaa
set_err_info("bbb");
reset_global_err();
set_err_info("ccc");
//下面这些会在全局错误Msg中拼接错误调用栈信息
//比如全局错误Msg执行结束后为:“ccc! pro/app/main.c lineNum:30 pro/app/main.c lineNum:31”
set_err_info(get_global_err_format_string());
set_err_info(get_global_err_format_string());
}
//分层使用
#define OK 0
#define ERR_1 100
#define ERR_2 101
#define ERR_3 102
int funTop(){
int iRet = funMiddle();
if ( iRet != OK){
set_err_info(get_global_err_format_string());
return iRet;
}
return OK;
}
int funMiddle{
int iRet = funBottom();
if(iRet == ERR_1 || iRet == ERR_2){
set_err_info(get_global_err_format_string());
return ERR_2; //过滤转换
}
return OK;
}
int funBottom{
if (xx){
set_err_info("ccc");
return ERR_1;
}
else{
set_err_info("ddd");
return ERR_2;
}
if(yy){
set_err_info("eee");
return ERR_3;
}
}
int main(){
int iRet = funTop();
if ( iRet !=OK){
//这里处理类似True和False,出错后直接把错误码和具体的错误调用栈信息写道日志中去
LOG_ERR(iRet, get_global_err_format_string());
return 0;
}
else{
}
}
【总结】
最后的写日志,还是放到了上层中,底层并没有写日志的功能。
具体的错误码还是通过函数的返回值中得到的,错误的信息和错误栈可以从get_global_err_format_string()拿到,写道日志中。
3. 错误码追溯机制(2024.6补充)
最近项目中又遇到这个问题,采用的方法简单粗暴,当每层接口产生错误码时,直接往一个任务中扔出该错误码,这样就可以看到错误码的一个生成链或者错误码的日志。