回调与错误处理:在回调中传递错误码,统一错误处理

写嵌入式程序这么多年,我踩过最大的坑之一,就是错误处理。

你想想看,一个系统里几十个模块,每个函数都返回错误码。主调函数拿到错误码后,要么直接忽略,要么打印一句 "Error!" 就完事。等到系统跑飞了,你连问题出在哪都不知道。

回调函数里的错误处理,更是重灾区。回调是异步的,错误发生的时候,主调函数可能早就跑远了。这时候怎么把错误信息传回来?怎么统一处理?

嗯,今天我们就来聊聊这个话题。

回调中的错误码传递:两种主流方式

我个人习惯把回调中的错误传递分成两类:同步回调异步回调。处理方式完全不同。

方式一:通过返回值传递错误码

这种方式适用于同步回调。回调函数执行完毕后,直接返回一个错误码给调用者。

// 定义回调类型:返回错误码
typedef int (*SensorReadCallback)(uint8_t* data, uint32_t len);

// 传感器驱动函数
int read_sensor(SensorReadCallback cb) {
    uint8_t buffer[64];
    int ret = cb(buffer, sizeof(buffer));
    if (ret != 0) {
        // 回调执行失败,统一处理
        log_error("Sensor read callback failed: %d", ret);
        return ret;
    }
    // 处理数据...
    return 0;
}

// 用户实现的回调
int my_sensor_callback(uint8_t* data, uint32_t len) {
    if (data == NULL || len == 0) {
        return -EINVAL;  // 参数无效
    }
    // 实际读取操作...
    if (/* 硬件错误 */) {
        return -EIO;     // IO错误
    }
    return 0;
}

这种方式简单直接。但有个问题:如果回调函数既要返回数据,又要返回错误码,怎么办?

我在项目中遇到过这种情况。当时我用了输出参数+返回值的组合方式:

typedef int (*DataProcessCallback)(uint8_t* input, uint32_t input_len, 
                                    uint8_t* output, uint32_t* output_len);

// 返回值:0成功,负值表示错误码
// output_len:输出数据的实际长度

方式二:通过上下文结构体传递错误码

异步回调就没这么简单了。回调执行的时候,主调函数可能已经返回了。这时候错误码不能通过返回值传递,得找个"永久"的地方存着。

我常用的做法是:定义一个上下文结构体,把错误码塞进去。

typedef struct {
    void* user_data;      // 用户自定义数据
    int   error_code;     // 错误码,初始化为0
    char  error_msg[64];  // 错误描述
} CallbackContext;

typedef void (*AsyncCallback)(CallbackContext* ctx, uint8_t* data, uint32_t len);

// 异步操作
void start_async_operation(AsyncCallback cb, CallbackContext* ctx) {
    // 启动异步任务...
    // 任务完成后调用 cb(ctx, data, len)
}

// 用户回调
void my_async_callback(CallbackContext* ctx, uint8_t* data, uint32_t len) {
    if (/* 发生错误 */) {
        ctx->error_code = -ETIMEDOUT;
        snprintf(ctx->error_msg, sizeof(ctx->error_msg), "Operation timed out");
        return;
    }
    // 正常处理...
}

这样做的好处是:错误信息跟着上下文走,不会丢失。主调函数可以在任意时刻检查 ctx->error_code。

统一错误处理:一个中心化的错误分发器

说实话,早期我写代码时,每个回调函数里都自己处理错误。结果就是:有的打印日志,有的直接复位,有的默默忽略。系统出问题时,排查起来特别痛苦。

后来我学乖了。设计了一个统一的错误处理中心,所有回调里的错误都交给它处理。

核心思想:回调函数只负责检测错误,不负责处理错误。错误处理由专门的模块统一完成。

// 错误处理中心
typedef void (*ErrorHandler)(int error_code, const char* module, const char* msg);

static ErrorHandler g_error_handler = NULL;

// 注册全局错误处理器
void register_error_handler(ErrorHandler handler) {
    g_error_handler = handler;
}

// 错误上报接口
void report_error(int error_code, const char* module, const char* msg) {
    if (g_error_handler) {
        g_error_handler(error_code, module, msg);
    }
}

// 回调中使用
void my_callback(void* data, uint32_t len) {
    if (data == NULL) {
        report_error(-EINVAL, "my_module", "data is NULL");
        return;
    }
    // 正常处理...
}

这样设计后,整个系统的错误处理就统一了。你可以根据不同的错误等级做不同的事情:

错误等级 错误码范围 处理方式
INFO 0 ~ -99 记录日志,继续运行
WARNING -100 ~ -499 记录日志,尝试恢复
ERROR -500 ~ -999 记录日志,通知主控
FATAL -1000 以下 记录日志,系统复位

实战案例:一个带错误处理的定时器回调

我记得有一次做工业控制项目,定时器回调里经常出现超时错误。一开始每个回调自己处理,代码乱成一锅粥。后来我重构了一下:

// 定时器回调上下文
typedef struct {
    TimerHandle timer;
    int         timeout_count;
    int         last_error;
} TimerContext;

// 统一的定时器错误处理
void timer_error_handler(int error_code, const char* module, const char* msg) {
    // 记录到环形缓冲区
    log_to_ring_buffer(error_code, module, msg);
    
    // 如果是严重错误,触发看门狗
    if (error_code < -500) {
        trigger_watchdog();
    }
}

// 定时器回调
void on_timer_tick(TimerContext* ctx) {
    int ret = check_hardware_status();
    if (ret != 0) {
        // 只上报错误,不处理
        report_error(ret, "timer", "Hardware check failed");
        ctx->last_error = ret;
        return;
    }
    // 正常处理...
}

// 主循环中检查错误
void main_loop() {
    while (1) {
        if (g_timer_ctx.last_error != 0) {
            // 统一处理累积的错误
            handle_accumulated_errors(&g_timer_ctx);
            g_timer_ctx.last_error = 0;
        }
        // 其他任务...
    }
}

小技巧:在回调中只设置错误码,不要直接调用复位或重启函数。把错误处理延迟到主循环或专门的错误处理线程中,可以避免在中断上下文中做危险操作。

避坑指南:我曾经犯过的错

我曾经在一个项目中,回调函数里直接调用了 printf 打印错误信息。结果在中断上下文中,printf 导致系统死锁。从那以后,我定了一条铁律:回调函数里绝对不做 IO 操作

还有一次,我在回调里用了一个全局变量来传递错误码。结果多个回调同时触发时,错误码被覆盖了。嗯,后来我改用线程局部存储(TLS)或者上下文结构体,才彻底解决这个问题。

警告:不要在回调函数中直接调用 exit()、abort() 等终止函数。回调通常运行在库或框架的上下文中,直接终止会导致资源泄漏。正确的做法是设置一个"致命错误"标志,让主循环安全地退出。

知识体系:回调错误处理的核心逻辑

下面这张图,是我多年经验总结出来的回调错误处理流程。你看一眼就能明白整个体系:

回调错误处理核心流程 主调函数 回调函数 有错误? 正常返回数据 返回错误码0 上报错误 report_error() 错误处理中心 统一分发处理 记录日志 尝试恢复 系统复位 回调只检测错误 不处理错误 错误处理统一 交给中心模块

说白了,这个流程的核心就一句话:回调只负责"报错",不负责"处理"。错误处理统一交给中心模块,根据错误等级做不同的事情。

总结

回调中的错误处理,其实就三个要点:

  • 传递方式要选对:同步回调用返回值,异步回调用上下文结构体
  • 处理要统一:设计一个错误处理中心,所有回调都通过它上报错误
  • 回调要轻量:只检测和上报错误,不做 IO、不复位、不退出

做到这三点,你的系统在出问题时,排查起来会轻松很多。嗯,至少不用像我当年那样,对着几百行日志一个一个翻了。


公众号:蓝海资料掘金营,微信deep3321