本文结合实际经验,参考ros2 rclcpp库中错误码定义及使用方式,梳理了一种基于C或C++开发的接口下错误码的定义及处理方式。{本文不涉及跨系统、跨服务的定义}
系统一般是按模块划分的,模块与模块之间通过调用与被调用的关系,一般也会划分为多个层次,底层一般对接系统级API或者实现一些算法,上层调用底层的接口来处理业务。那么这些错误码如何定义呢?
首先要明白错误码是什么:
在笔者看来,错误码分为通用的系统级错误码和业务错误码,系统级错误码一般包括指针为空、内存分配失败、无效参数、超时等等,业务错误码和具体的模块业务有关系。
我们一般会定义一个头文件,统一放这些错误码,当然错误码也是要按模块来进行划分的,这样既方便了错误码的追溯,又可以让不同的开发人员方便维护自己模块的错误码。比如下面这样:
/// 通用的错误码定义
/// 成功
#define ERR_OK 0
/// 超时错误
#define ERR_TIMEOUT 1
/// 内存分配失败
#define ERR_BAD_ALLOC 2
// A模块,1xx
/// 错误1.
#define ERR_XXX 100
#define ERR_XYX 101
// B模块,2xx
/// 错误2.
#define ERR_XXX2 200
【注意】某些模块可能是一些通用的模块,不想纳入某个系统的错误码定义约定中,比如某些通用的算法模块,那么此时错误码的定义可以单独进行定义,维护自己的一套规则即可。
错误码定义完了,如何处理这些错误码是很关键也很头疼的一件事。这里C和C++处理方式可以按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();
}
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;
}
当然,底层可以用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;
}
}
如果采用函数返回错误码的方式来实现,那么函数的注释中,一般会标明该函数返回的错误码。如果底层的错误码没被收敛,透传到上层,那么此时应该把所有的可能错误码包括底层的错误码都列上,这样其实特别繁琐。比如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的使用,也未尝不可。
总之这两种方法各有利弊,重点是在使用时,某一层要统一一个规则。
层之间调用时,底层的接口可能会被多个中间层调用,底层产生错误时,可能有多种调用路径,那么如何跟踪这些调用路径,也就是说产生一个错误,如何知道错误是怎么一层层产生的呢?这个在某些时候还是非常重要的,如果只拿到底层的错误码,还是无法知道是哪个业务层调用时出的问题。
这里可以参考一下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()拿到,写道日志中。