C语言中的API设计陷阱


10

有哪些缺陷使您无法使用C API(包括标准库,第三方库和项目内的标头)?目的是确定C语言中的API设计陷阱,以便编写新C库的人们可以从过去的错误中学习。

解释为什么缺陷不好(最好举个例子),并尝试提出改进建议。尽管您的解决方案在现实生活中可能不切实际(修复为时已晚strncpy),但它应该提请未来的图书馆作家。

尽管此问题的重点是C API,但是仍然存在影响您以其他语言使用它们的能力的问题。

请为每个答案给出一个缺陷,以便民主可以对答案进行排序。


3
乔伊(Joey),这个问题正在通过要求建立人们讨厌的事情清单来证明不是建设性的。如果答案解释了他们指出的做法为何不好,并提供了有关如何改进它们的详细信息,则此问题很有用。为此,请将您的示例从问题移到其自身的答案中,并解释为什么这是一个问题/ malloc'd字符串如何解决该问题。我认为为第一个答案树立榜样确实可以使这个问题蓬勃发展。谢谢!
亚当李尔

1
@Anna Lear:感谢您告诉我为什么我的问题有问题。我试图通过举个例子并提出其他建议来保持其建设性。我想我确实需要一些示例来说明我的想法。
乔伊·亚当斯

@Joey Adams这样看。您在问一个应该以一般方式“自动”解决C API问题的问题。诸如StackOverflow之类的站点设计为可以在其中运行,以便可以轻松找到并回答编程方面的常见问题。StackOverflow会自然而然地为您的问题提供答案列表,但结构更易于搜索。
Andrew T Finnell

我投票结束了我自己的问题。我的目标是要有一系列答案,可以作为针对新C库的清单。到目前为止,这三个答案都使用了“不一致”,“不合逻辑”或“令人困惑”之类的词。不能客观地确定API是否违反了这些答案。
乔伊·亚当斯

Answers:


5

返回值不一致或不合逻辑的函数。两个很好的例子:

1)一些返回HANDLE的Windows函数将NULL / 0用于错误(CreateThread),一些将INVALID_HANDLE_VALUE / -1用于错误(CreateFile)。

2)POSIX的“时间”函数在出错时返回“(time_t)-1”,这是不合理的,因为“ time_t”可以是有符号或无符号类型。


2
实际上,(通常)对time_t进行了签名。但是,将1969年12月31日称为“无效”是不合逻辑的。我猜60年代很粗糙:-)认真地说,一种解决方案是返回错误代码,然后将结果通过指针传递,如:int time(time_t *out);BOOL CreateFile(LPCTSTR lpFileName, ..., HANDLE *out);
乔伊·亚当斯

究竟。这很奇怪,如果time_t的是无符号的,如果time_t的签署,它使一个时间无效在有效问卷的海洋中。
David Schwartz

4

具有非描述性或肯定性混淆名称的函数或参数。例如:

1)Windows API中的CreateFile实际上并没有创建文件,而是创建了文件句柄。如果通过参数要求,它可以像“打开”一样创建文件。此参数具有名为“ CREATE_ALWAYS”和“ CREATE_NEW”的值,它们的名称甚至不暗示其语义。(“ CREATE_ALWAYS”是否意味着如果文件存在会失败?或者它会在文件顶部创建一个新文件?“ CREATE_NEW”意味着它会始终创建一个新文件并且如果该文件已经存在会失败吗?还是会创建一个新文件?文件放在上面?)

2)POSIX pthreads API中的pthread_cond_wait,尽管其名称如此,但它是无条件的等待。


1
条件中的条件pthread_cond_wait并不意味着“有条件地等待”。它指的是您正在等待条件变量的事实。
乔纳森·莱因哈特

没错,这是一个无条件的等待一个条件。
David Schwartz

3

通过接口作为类型删除句柄传递的不透明类型。当然,问题在于编译器无法检查用户代码中是否有正确的参数类型。

这有多种形式和风味,包括但不限于:

  • void* 滥用

  • 使用int作为资源句柄(比如:CDI库)

  • 字符串型参数

不同的类型(=不能完全互换使用)映射到同一类型的已删除类型越糟。当然,补救措施只是沿(C示例)的行提供类型安全的不透明指针:

typedef struct Foo Foo;
typedef struct Bar Bar;

Foo* createFoo();
Bar* createBar();

int doSomething(Foo* foo);
void somethingElse(Foo* foo, Bar* bar);

void destroyFoo(Foo* foo);
void destroyBar(Bar* bar);

2

具有不一致且通常麻烦的字符串返回约定的函数。

例如,getcwd询问用户提供的缓冲区及其大小。这意味着应用程序必须在当前目录长度上设置一个任意限制,或者执行以下操作(来自CCAN):

 /* *This* is why people hate C. */
len = 32;
cwd = talloc_array(ctx, char, len);
while (!getcwd(cwd, len)) {
    if (errno != ERANGE) {
        talloc_free(cwd);
        return NULL;
    }
    cwd = talloc_realloc(ctx, cwd, char, len *= 2);
}

我的解决方案:返回一个malloced字符串。它简单,强大且效率不低。除了嵌入式平台和较旧的系统,malloc实际上是相当快的。


4
我不会称这种不好的作法,我称这种良好的作法。1)非常普遍,没有程序员对此感到惊讶。2)它将分配留给调用方,这排除了内存泄漏错误的许多可能性。3)与静态分配的缓冲区兼容。4)它使函数的实现更简洁,计算某些数学公式的函数不应该与诸如动态内存分配之类完全不相关的事物相关。您认为main变得更干净,但功能变得更混乱。5)在许多系统上甚至都不允许使用malloc。

@Lundin:问题是,这导致程序员创建不必要的硬编码限制,并且他们必须尽力避免这样做(请参见上面的示例)。对于snprintf(buf, 32, "%d", n)输出长度是可预测的(例如,除非您的系统上确实很大),输出长度是可预测的(例如,不超过30),int很好。确实,malloc在许多系统上都不可用,但是对于台式机和服务器环境而言,它确实是有效的。
乔伊·亚当斯

但是问题在于示例中的函数没有设置硬编码限制。这样的代码并不常见。在这里,main知道函数应该知道的有关缓冲区长度的信息。这一切都表明程序设计不佳。Main似乎甚至不知道getcwd函数的作用,因此它使用某种“蛮力”分配来查找。getcwd所在的模块与调用者之间的接口有些混乱。这并不意味着这种调用函数的方法不好,相反,经验表明,由于我已经列出的原因,它是好的。

1

按值获取/返回复合数据类型或使用回调的函数。

如果所说的类型是并集或包含位字段,则更糟。

从C调用者的角度来看,这些实际上是可以的,但是除非有必要,否则我不会用C或C ++编写,因此我通常通过FFI进行调用。大多数FFI不支持联合或位域,而某些FFI(例如Haskell和MLton)不支持按值传递的结构。对于那些可以处理按值结构的对象,至少将Common Lisp和LuaJIT强制设置为慢速路径-Lisp的Common Foreign Function Interface必须通过libffi进行慢速调用,而LuaJIT拒绝JIT编译包含该调用的代码路径。可能会回调到主机的函数也会触发LuaJIT,Java和Haskell上的慢速路径,而LuaJIT无法编译此类调用。

By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.