C中多层实现的错误处理

Error handling for multi-layer implementation in C

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

  1. 通过 UART 发送和接收数据的通信层。
  2. 块层:该层enables/disables 设备上的某些块,通过 UART 将数据写入设备。
  3. 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;
}

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

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

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

例如:“UART framing error”可能对调用UART驱动的代码有用,但对上层应用没用。 “可能不正确的波特率设置”可能是您应该传递的更相关的错误描述。尽管在某些情况下,您甚至希望在更高层也能获得详细的错误信息。

您可能想要这样做的一个原因是,在顶层有一个集中式错误处理程序是一种常见且通常很好的设计,它可以从一个单一的状态更改、print/log 错误等做出决定放在代码中。而不是从各地这样做。您经常会发现微控制器应用程序的顶层看起来像这样:

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);
    }
  }
}

至于你的具体代码,它看起来很好,而且基本上与我上面写的内容不矛盾。 return 错误代码总是好的 - 比 goto 更容易混淆,甚至更糟:大量嵌套语句 and/or 带有错误条件标志的循环。

你的代码没问题。实际上,在 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() 的第 4 次调用时失败。调整变量 left 为 select 其他调用。

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

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