在C语言中的可重入库设计

6

假设我正在用C语言构建一个库,用于spork quuxes。

要成功spork quuxes,需要两个状态变量:

static int quux_state;
static char* quux_address;

/* function to spork quuxes found in a file, 
   reads a line from the file each time it's called. */
void spork_quux(FILE*);

如果我将数据存储为全局变量,那么只有一个客户端能够同时 spork quuxes,否则状态变量将被第二个调用者搞乱,可能会导致灾难。
问题是如何在C语言中设计可重入库?
我已经考虑了以下情况,但没有得出令人满意的结论。
在下面的情况中,问题是如何将每个状态与客户端关联起来?
/* library handles all state data allocation */
static int* quux_state; 
static char** quux_address;

在以下情况下,客户端可以操纵状态,这是非常不希望的。
/* let each client store state */
typedef struct { int state; char* address; } QuuxState; 
QuuxState spork_quux(FILE*);

所以,如何正确地做到这一点呢?
3个回答

21

使用结构体,但不要将其定义暴露给客户端。

即,在.h头文件中放置以下内容:

typedef struct QuuxState QuuxState;

QuuxState *spork_quux(FILE*);

并且在.c实现文件中:

struct QuuxState
{
    int state;
    char* address;
};

QuuxState *spork_quuxFILE *f)
{
    QuuxState *quuxState = calloc(1, sizeof(*quuxState));
    if (!quuxState)
        return NULL;

    quuxState->state = ....;

    ........

    return quuxState;
}

这种方法的优点是:

  1. 您可以更改结构的内容,而无需重新编译所有客户端代码。
  2. 客户端代码无法访问成员,即quuxState->state会产生编译器错误。
  3. 调试器仍然可以完全看到QuuxState结构体,因此您可以轻松查看值并设置监视点等。
  4. 不需要转换类型。
  5. 返回的类型是特定类型,因此您将获得一些编译器检查以确保正确的东西被传递(与void*指针相比)。

唯一的缺点是您必须分配一个内存块-但是假设您的库正在执行任何非平凡操作(如果执行文件I/O,则肯定是非平凡的),那么单个malloc的开销是可以忽略不计的。

您可能希望将上述函数重命名为类似“QuuxSpork_create”的名称,并添加更多函数以处理逐行工作并在完成后销毁状态。

void QuuxSpork_readLine(QuuxState *state)
{
    ....
}

void QuuxSpork_destroy(QuuxState *state)
{
    free(state);
}

一个随机的示例库,大致运作方式类似于POSIX线程库,即pthreads。


2
回复:‘缺点’:代码可以静态分配一个struct QuuxState结构的数组,并每次返回一个新的,直到耗尽为止。当程序完成了对其quux的操作后,它调用'unspork_quux()'或类似的函数来返回值。这将对可以同时被操作的quux数量设置一个有限界限。如果仍然存在问题,则可以在静态分配用尽后动态分配,这时测试返回的指针是否在静态数组的边界内,可能会导致(名义上而非实际上的)未定义行为的风险。 - Jonathan Leffler
谢谢!那绝对是可行的方法 - 当然,避免过早优化和保持代码简单的通常规则仍然适用,但如果您在紧密循环或其他非常性能关键(或嵌入式系统)中使用此模式,则像您所概述的预分配方法肯定是明智且可能是必要的。 - JosephH
4
这几乎是C语言中用来封装对象的标准模式,你会在很多地方看到它被使用。解释得非常好。 - JeremyP
顺便说一下,如果你发现自己正在分配和释放大量的Quuxs,你可以在结构定义中添加一个指向Quux的指针,而不是释放它们,将它们添加到未使用的Quux列表中。当你需要一个新的Quux时,只需从列表顶部取出一个,除非它为空,在这种情况下只需malloc一个新的Quux即可。 - JeremyP

3
使用结构体,但不要告诉客户端它是一个结构体。传递一个不透明指针-void*,或者最好传递一个空的虚拟结构体指针,并在需要时进行类型转换。

5
或者是一个指向不完整类型的指针,即在您的头文件中放置struct quux;,并返回struct quux *。定义可以保留在私有头文件中,仅供库代码使用/可见。 - R.. GitHub STOP HELPING ICE

1
大多数库函数处理此问题的方式是将状态信息以用户需要的任何数据类型返回给用户,对于您的情况,可以使用结构体(例如 strtok 和 strtok_r)。我认为这树立了一个先例,即您应该将其传回给用户。void * 很好用,甚至可以 typedef 它,使其看起来漂亮。
此外,strtok_r 是通过编辑命令行参数来实现的,而不是返回指向状态的指针。我希望我使用的任何可重入函数都遵循类似的格式。当然,我的大脑已经被一些非常疯狂的 C 代码扭曲了。
void spork_quux(FILE*, QuuxState **);

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接