【问题标题】:Handling error codes from multiple libraries in C在 C 中处理来自多个库的错误代码
【发布时间】:2014-08-28 18:12:57
【问题描述】:

很多时候,我必须使用多个以不同方式处理错误的库,或者为错误定义自己的枚举。这使得编写可能必须处理来自不同来源的错误然后返回自己的错误代码的函数变得困难。例如:

int do_foo_and_bar()
{
    int err;
    if ((err = libfoo_do_something()) < 0) {
        // return err and indication that it was caused by foo
    }
    if ((err = libbar_do_something()) < 0) {
        // return err and indication that it was caused by bar
    }
    // ...
    return 0;
}

我想到了两种可能的解决方案:

  • 使用int translate_foo_error(int err) 之类的函数创建我自己的错误代码列表并将这些错误代码转换为新代码,然后我将为每个错误编写自己的字符串表示形式。
  • 创建一个struct my_error,其中包含一个标识库的枚举和错误代码。字符串的翻译将委托给每个库的适当函数。

这似乎是一个经常出现的问题,所以我很好奇,这通常是如何处理的?似乎前者是大多数图书馆所做的,但后者的工作量较少,并且可以使用已经提供的工具。大多数教程只是向 stderr 打印一条消息并在出现任何错误时退出,这无济于事。我宁愿让每个函数指出哪里出了问题,调用者可以据此决定如何处理。

【问题讨论】:

  • 您还必须注意那些与惯例相反的情况:1 表示成功,0 表示失败,然后您需要调用另一个例程来找出失败的原因。每个函数都有一个错误是一个不错的选择,但是当库中有添加时会发生什么 - 您是在底部添加它还是将其他所有内容向下移动或为每个库分配代码块。每种技术都有自己的问题。

标签: c error-handling libraries custom-error-handling


【解决方案1】:

答案是,这取决于代码的约束。

collectd 打印到标准错误,然后在遇到致命错误时退出。

OpenGL 将设置一些您可以查询的共享状态。忽略此错误通常会导致未定义的行为。

Collectd 有很多线程问题,程序无法修复或恢复大多数错误。例如,如果一个插件依赖于某个库,并且对该库的调用失败,则该插件最了解如何从该错误中恢复。冒泡该错误并没有帮助,因为 collectd 核心永远不会知道插件 N+1

另一方面,OpenGL 应用程序通常对遇到的任何错误负责,并且可以尝试纠正错误。例如,如果他们试图编译一个着色器,但可能有一个针对特定供应商或平台的特殊着色器。

根据您的程序设计,考虑以下几点:

  1. 冒泡错误会让您做出更好的决定吗?
    • 调用者知道你的实现吗?他们应该吗?
  2. 您的应用程序是否可以纠正可能出现的错误?
    • 例如如果您无法打开套接字或文件,您可以重试或失败,仅此而已。
  3. 如果您创建GetLastError() 函数,是否存在全局状态限制?

更新:

使用全局状态选项,您可能会遇到这样的情况:

#include <stdlib.h>
#include <stdio.h>
#include <string.h>
#include <math.h>

char* last_err = NULL;

void set_err(char* error_message) {
    if(last_err)
        free(last_err);
    /* Make a deep copy to be safe. 
     * The error string might be dynamically allocated by an external library. 
     * We can't know for sure where it came from. 
     */
    last_err = strdup(error_message); 
}

int can_sqrt(int a) {
    if(a < 0) {
        set_err("We can't take the square root of a negative number");
        return 0;
    }
    return 1;
}

int main(int argc, char* argv[]) {
    int i = 1;
    for(i = 1; i < argc; i++) {
      int square = atoi(argv[i]);
      if(can_sqrt(square)) {
        fprintf(stdout, "the square root of %d is: %.0f\n", square, sqrt(square));
      } else {
        fprintf(stderr, "%s\n", last_err);
      }
    }
    return 0;
}

运行上述程序

$ ./a.out -1 2 -4 0 -6 4
We can't take the square root of a negative number
the square root of 2 is: 1
We can't take the square root of a negative number
the square root of 0 is: 0
We can't take the square root of a negative number
the square root of 4 is: 2

【讨论】:

  • 谢谢,我认为这清除了我的很多想法。如果您希望失败只是导致停止当前操作并向用户打印消息,您将如何处理?我可以考虑如何使用 C++ 异常来执行此操作,但我很想知道它是如何在 C 中处理的。全局状态听起来像是最小化函数之间传递的数据的方法,因为我对线程没有任何问题.
  • @JamesHagborg 我已经用一个工作示例更新了我的答案,说明了如何使用全局状态来跟踪错误消息。
  • @JamesHagborg 显然这个例子很简单,你可以像你建议的那样将全局数据包装在一个结构中。如果您需要获得一些并发性,您甚至可以在读取/写入错误字符串时添加互斥锁,尽管每个进程或线程都可以拥有自己的最后一个错误数据。
【解决方案2】:

我喜欢使用 Thread-Local-Storage (TLS) 来存储在库深处检测到的错误。它们快速且线程安全。唯一真正的问题是错误属于调用生成错误的函数的线程。这可能是某些线程模型中的问题(例如,线程池中的匿名线程)。其他线程看不到错误,除非您有某种方法可以将错误从一个线程传播到另一个线程。但是也有办法做到这一点,而且这种错误传播方法快速高效,并且可以在库中生成更优雅的代码(我相信)。一般的理念是将错误报告以及恢复决策向上推向界面。错误恢复可以在调用堆栈中的任何级别进行处理(例如在接口之前的中间层级别),但总体思路是库中的每个函数都将责任向上推向调用者的方向。每个功能都应该承担一点责任,然后将其余部分传递回调用链。 例如,库中的每个(可能是大多数)函数都可以将任何错误记录到 TLS 并返回一个布尔值,指示函数/操作成功给调用者。然后,调用者可以查看返回的布尔值,如果操作不成功,要么决定对它做点什么(比如重试),要么中止,清理堆栈并返回 false。如果您存储在 TLS 中的信息是一个结构,您可以在调用链上聚合错误信息(以及采取的任何补救措施)。然后这个过程可以一直持续到接口级别。在任何时候,调用者都可以询问最后一个错误并根据指示的错误决定做任何事情。 显然,您的库需要提供顶级的 SetLastError()/GetLastError() 接口函数对。此外,您可能会在库中的每个接口函数(当然除了 SetLastError()/GetLastError() 之外)都有代码,该代码会在调用接口函数时重置最后一个错误状态。

【讨论】:

    猜你喜欢
    • 1970-01-01
    • 2016-04-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2010-09-18
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多