嵌入式c语言错误状态码为返回值函数是否需要对错误状态码进行细分定义并处理?
前段时间写过一篇文章里面提到嵌入式c语言函数定义根据返回值来区分分为三种风格其中风格3就是返回值为错误状态码在父函数中调用该子函数子函数的返回值往往直接参与逻辑运算最常见的就是直接参与if()条件判断或者将返回值直接赋值给父函数函数体定义的一个临时栈变量常用的临时栈变量取名例如ret、rc、res、err、state、rv等等总之变量名有返回值、错误码、状态等含义要言之有物。然后这个临时栈变量直接返回或者参与逻辑运算最常见的就是if条件判断。我以前的认为以错误状态码为返回值的这种风格3函数编写和函数调用方式。需要提前使用宏定义定义好所有可能得错误状态码然后在函数定义的函数体内编写每一种错误状态码为返回值的代码往往最后才编写没有错误的代码最后在父函数中去调用子函数然后去处理每一种可能错误状态码。我这种编写风格3的编写方式强调每一个细节都想到因此定义非常多错误状态码并且在函数定义的函数体内编写很多代码去处理这些错误状态码因此代码量非常大最后在函数调用中对每一种错误状态码都进行处理。这整个流程显得特别冗长特别繁琐如果按照这个方式编写代码工作量和难度会变得非常大尤其是项目工程比较大例如uboot、linux kernel等大工程。问题以风格3方式编写函数调用函数是否需要将错误状态码细分定义并处理呢我现在的答案为不需要也就是不需要将错误状态码细分定义并处理。首先前面提到错误状态码细分定义并处理的方式编写风格3的函数和函数调用加大了代码编写难度。其次我最近粗略阅读以前同事写的stm32单片机程序、uboot源码、linux内核源码、freeRTOS源码、stm32 HAL库函数中以风格3编写的函数以及函数定义。发现他们并没有将错误状态码进行细化有很多代码都没有专门定义错误状态码直接使用0表示没问题-1、-2、1、2等非零数字表示错误码并返回在父函数中往往以if(子函数(参数传参))进行错误处理或者在父函数中定义临时栈变量rc,rc子函数(参数传参),if(rc)处理错误或者if(!rc)处理正确有些甚至直接将错误状态码使用return直接上报。在这些源码中“错误状态码细分定义并处理”的函数特别小众。99%以上的代码都是“错误状态码粗略定义并处理”。因此我认为“错误状态码细分定义并处理”的方式变代码编写过程中没有必要完全可以用“错误状态码粗略定义并处理”的方式编写代码。在以上源码中甚至以整个工程使用一套错误状态码例如linux kernel源码中大量函数直接使用头文件errno-base.h中定义的统一错误码linux kernel源码中有大量代码使用了布尔类型作为函数返回值。uboot源码中大量函数直接使用了头文件errno.h中定义的统一错误码而没有根据不同模块定义各自的状态码。stm32 HAL库的模块中也使用了同一套错误状态码例如linux kernel源码#define EPERM 1 /* Operation not permitted */ #define ENOENT 2 /* No such file or directory */ #define ESRCH 3 /* No such process */ #define EINTR 4 /* Interrupted system call */ #define EIO 5 /* I/O error */ #define ENXIO 6 /* No such device or address */ #define E2BIG 7 /* Argument list too long */ static int kthread(void *_create) { /* Copy data: its on kthreads stack */ struct kthread_create_info *create _create; int (*threadfn)(void *data) create-threadfn; void *data create-data; struct kthread self; int ret; self.should_stop 0; init_completion(self.exited); current-vfork_done self.exited; /* OK, tell user were spawned, wait for stop or wakeup */ __set_current_state(TASK_UNINTERRUPTIBLE); create-result current; complete(create-done); schedule(); ret -EINTR; if (!self.should_stop) ret threadfn(data); /* we cant just return, we must preserve self on stack */ do_exit(ret); }// 布尔bool类型定义 typedef _Bool bool; enum { false 0, true 1 }; // 子函数定义主要关注返回值 static bool init_root_id(struct cgroupfs_root *root) { int ret 0; do { if (!ida_pre_get(hierarchy_ida, GFP_KERNEL)) return false; spin_lock(hierarchy_id_lock); /* Try to allocate the next unused ID */ ret ida_get_new_above(hierarchy_ida, next_hierarchy_id, root-hierarchy_id); if (ret -ENOSPC) /* Try again starting from 0 */ ret ida_get_new(hierarchy_ida, root-hierarchy_id); if (!ret) { next_hierarchy_id root-hierarchy_id 1; } else if (ret ! -EAGAIN) { /* Can only get here if the 31-bit IDR is full ... */ BUG_ON(ret); } spin_unlock(hierarchy_id_lock); } while (ret); return true; } // 父函数调用子函数主要关系子函数调用参与的逻辑判断 if (!init_root_id(root)) { // 返回值为false的情况也就是为0的情况 kfree(root); return ERR_PTR(-ENOMEM); }uboot源码#define EROFS 30 /* Read-only file system */ int ubi_eba_unmap_leb(struct ubi_device *ubi, struct ubi_volume *vol, int lnum) { int err, pnum, vol_id vol-vol_id; if (ubi-ro_mode) return -EROFS; err leb_write_lock(ubi, vol_id, lnum); if (err) return err; pnum vol-eba_tbl[lnum]; if (pnum 0) /* This logical eraseblock is already unmapped */ goto out_unlock; dbg_eba(erase LEB %d:%d, PEB %d, vol_id, lnum, pnum); vol-eba_tbl[lnum] UBI_LEB_UNMAPPED; err ubi_wl_put_peb(ubi, pnum, 0); out_unlock: leb_write_unlock(ubi, vol_id, lnum); return err; }stm32 HAL库源码typedef enum { HAL_OK 0x00U, HAL_ERROR 0x01U, HAL_BUSY 0x02U, HAL_TIMEOUT 0x03U } HAL_StatusTypeDef; HAL_StatusTypeDef HAL_FLASH_Unlock(void) { if (HAL_IS_BIT_SET(FLASH-CR, FLASH_CR_LOCK)) { /* Authorize the FLASH Registers access */ WRITE_REG(FLASH-KEYR, FLASH_KEY1); WRITE_REG(FLASH-KEYR, FLASH_KEY2); } else { return HAL_ERROR; } return HAL_OK; }各个模块返回值类型都是HAL_StatusTypeDef而没有在各个模块gpio、can、flash定义各自的错误状态码。根据以上例子像linux内核源码、uboot源码、stm32 HAL库这么大的项目也没有对错误返回值进行细粉定义并处理因此我们在写自己代码过程中也完全没必要对错误状态码分的太细粗略划分即可有时候甚至不用宏定义明确定义错误状态码直接使用012、-1-2这种常量代表错误状态码即可。父函数中子函数调用错误状态码的处理1如果错误状态码为非0常数可以这样处理这种使用if条件判断写法是最常用的。// 方式1 ret son_func(argc, argv); if(ret) /* 卫语句的写法 */ { // 错误处理; return xxx; } ... // 没有错误的代码 // 方式2:方式1的简略写法 if (son_func(argc, argv)) // 卫语句写法 { // 错误处理; return xxx; } ... // 没有错误的代码也可以按照这种写法写不过不常用因为“没有错误的代码”被一个if条件判断给嵌套了。// 方式1的写法 ret son_func(argc, argv); if (!ret) { ... // 没有遇到错误的处理 } ... // 后面的代码 return 0; // 成功处理完并返回 // 方式2方式1的简略写法 if (!son_func(argc, argv)) { ... // 正确代码处理 } ... // 剩余代码处理 return 0; // 成功处理完并返回2在linux内核源码、uboot、freeRTOS源码中有大量代码父函数直接调用以风格3写的子函数也就是直接上报子函数的返回值给父函数的父函数而没有进行处理这种方式也很常见。// 方式1父函数中 rec func_son(argc, argv); return rec; // 方式2父函数中,方式1的简略写法 return func_son(argc, argv);知识记录1父函数中定义临时栈变量去接收子函数的错误状态码这个临时栈变量变量名要能够体现错误状态码这个含义也就说要言之有物例如常用变量名有rec、res、err、stat、rv、rc、ret等。同样的如果返回值为某个数据结果这个临时变量名也要有所体现。2函数中定义char *xxx这个变量名xxx常用的有哪些我搜素了uboot、linux kernel源码发现尺使用最多的就是c、s、str等表示字符或者字符串含义的变量名。