C 中多层实现的错误处理

Posted

技术标签:

【中文标题】C 中多层实现的错误处理【英文标题】:Error handling for multi-layer implementation in C 【发布时间】:2021-11-27 13:40:25 【问题描述】:

我正在用 C 语言为设备编写固件。该软件允许 PC 通过串行接口 (UART) 与该设备进行通信。固件包含以下多层:

    通过 UART 发送和接收数据的通信层。 块层:此层通过 UART 向设备写入数据来启用/禁用设备上的某些块。 API 层:这包含对块层中例程的一系列调用。它用于启用或禁用设备上的一组块。

我的问题是错误处理,因为在 C 中没有异常。以下是我实现固件的方式,我正在尝试看看是否有更有效和更紧凑的方式来构建它,同时仍然有效地处理错误。我想避免在每一层检查下层调用的状态。 下面的代码非常紧凑,实际上,我在块层中有很长的 send_uart_commands 序列。


// Communication layer

operation_status_t send_uart_command(command_id_t id, command_value_t value)

    // Send data over UART
    // Return success if the operation is successful; otherwise failure


// Block layer

operation_status_t enable_block1(void)

    if (send_uart_command(BLOCK1_COMMAND_1, 10) != operation_success)
           return operation_failure;

    if (send_uart_command(BLOCK1_COMMAND_2, 20) != operation_success)
           return operation_failure;

    // A list of sequences

    if (send_uart_command(BLOCK1_COMMAND_N, 15) != operation_success)
           return operation_failure;

    return operation_success;


operation_status_t enable_block2(void)

    if (send_uart_command(BLOCK2_COMMAND_1, 1) != operation_success)
           return operation_failure;

    if (send_uart_command(BLOCK2_COMMAND_2, 8) != operation_success)
           return operation_failure;

    return operation_success;


// API layer

operation_status_t initialize(void)

    if (enable_block1() != operation_success)
           return operation_failure;

    if (enable_block2() != operation_success)
           return operation_failure;

   // A list of calls to the functions in the block layer

    return operation_success;


【问题讨论】:

提供的代码到底有什么问题?我很确定任何能够内联 ang squashing goto 的合理编译器都会生成最佳程序集 @tstanisl 我的问题是重复的错误处理,我必须为每个函数做。 见***.com/questions/69361515/… @tstanisl 嗯,我记得那个。到处都有很多不好的建议。我会怀着极大的怀疑态度阅读那里的所有内容。 重复不一定是坏事;它可以使代码更容易阅读和调试,而不是试图变得“聪明”。 【参考方案1】:

在 C++ 中处理异常的许多大问题之一是异常可能会像炮弹一样在所有层中崩溃。因此,当您编写一些完全不相关的代码时,您会突然面对炮弹:“UART 帧错误!”当你甚至没有接触到 UART 代码时......

因此“我想避免在每一层检查下层调用的状态”是错误的。相反,您应该这样做:

检查每一层的错误。 尽可能靠近错误源处理错误。 仅将错误转发给调用者,以防它们对调用者真正有意义。 您可以重命名/更改错误类型以适应调用者的需要。

例如:“UART 帧错误”可能对调用 UART 驱动程序的代码有用,但对更高层的应用程序无用。 “可能不正确的波特率设置”可能是您应该传递的更相关的错误描述。尽管在某些情况下,您希望即使在较高层也能提供详细的错误信息。

您可能想要这样做的一个原因是,在顶层有一个集中式错误处理程序是一种常见且通常很好的设计,它可以从代码中的一个位置做出状态更改、打印/记录错误等决策.而不是到处这样做。您经常会发现微控制器应用程序的顶层如下所示:

void main (void)

  /* init & setup code called */

  for(;;)
  
    kick_watchdog(); // the only place in the program where you do this
      
    result = state_machine[state]();
    
    if(result != OK)
    
      state = error_handler(result);
    
  


至于您的具体代码,它看起来还不错,而且与我上面写的内容几乎没有任何矛盾。出错时返回错误代码总是好的 - 比 goto 更容易混淆,甚至更糟:大量嵌套的语句和/或带有错误条件标志的循环。

【讨论】:

【参考方案2】:

您的代码很好。实际上,避免在 C 中进行显式错误检查是一种不好的做法。但如果你真的想要它,你可以使用longjmp。但是你应该非常小心地使用它。

此功能允许跳过堆栈跳过任意数量的嵌套调用。

您可以在下面找到模拟 send_uart_command() 的示例。

#include <setjmp.h>
#include <stdio.h>

jmp_buf env;

void send_uart_command_impl(const char *cmd, int val) 
    static int left = 3;
    if (left-- == 0) 
        printf("%s(%d): failed\n", cmd, val);
        longjmp(env, 1);
    
    printf("%s(%d): success\n", cmd, val);


#define send_uart_command(name, val) send_uart_command_impl(#name, val)

void enable_block1(void) 
    send_uart_command(BLOCK1_COMMAND_1, 10);
    send_uart_command(BLOCK1_COMMAND_2, 20);
    send_uart_command(BLOCK1_COMMAND_N, 15);


void enable_block2(void) 
    send_uart_command(BLOCK2_COMMAND_1, 1);
    send_uart_command(BLOCK2_COMMAND_2, 8);


int initialize(void) 
    if (setjmp(env)) return -1;
    enable_block1();
    enable_block2();
    return 0;


int main() 
    if (initialize() != 0)
        puts("initialize failed");
    else
        puts("initialize success");

程序构造为在第四次调用send_uart_command() 时失败。调整变量left 以选择其他调用。

程序逻辑非常精简,打印出预期的输出:

BLOCK1_COMMAND_1(10): success
BLOCK1_COMMAND_2(20): success
BLOCK1_COMMAND_N(15): success
BLOCK2_COMMAND_1(1): failed
initialize failed

【讨论】:

或者...您可以使用内联汇编器手动破解堆栈框架!我投了反对票,因为这教了非常糟糕的做法。说真的,不要教别人/你最大的敌人使用setjmp。相比之下,它使带有 goto 的意大利面条式编程看起来像最先进的代码。除了意大利面条之外,还有许多与这些功能相关的 UB 形式。 Which functions from the standard library must (should) be avoided? 同样从固件的角度来看,setjmp 可以以非常有创意的方式对程序进行修改,例如禁用中断、删除中断的返回地址,以便它们在任何时候崩溃。试图返回。等等等等。只是不要使用它。 @Lundin 这就是为什么我添加了一个警告,即隐藏流逻辑是一种不好的做法,但答案解决了 OP 的问题 我相信 SO 的目的不仅仅是盲目地回答问题,而是指出相关问题并引导人们使用好的做法,远离坏的做法。在这种情况下,您正在将它们从相当不错的代码转向具有整个 C 语言中最臭名昭著的语言特性之一的意大利面条代码...... @Lundin,我同意setjmp 的东西是危险的工具。但如果使用得当,它仍然可以简化很多代码。它对于跳出深层嵌套的递归调用很有用。广泛用于 PSIM 模拟器,GCC 工具的一部分。

以上是关于C 中多层实现的错误处理的主要内容,如果未能解决你的问题,请参考以下文章

Java多层嵌套异常处理的基本流程

python异常处理

Go-错误异常处理详解

求批处理命令解决多层文件夹嵌套的问题

扩展Python模块系列----异常和错误处理

Java的异常处理机制(下)