这是在 C API 中实现抽象的正确方法吗?

Is this the right way to implement abstractions in a C API?

我想在 C 中创建一个 API。我的目标是实现抽象以访问和改变 API 中定义的 struct 变量。

API的头文件:

#ifndef API_H
#define API_H

struct s_accessor {
        struct s* s_ptr;
};

void api_init_func(struct s_accessor *foo);
void api_mutate_func(struct s_accessor *foo, int x);
void api_print_func(struct s_accessor *foo);

#endif

API'实现文件:

#include <stdio.h>
#include "api.h"

struct s { 
        int internal; 
        int other_stuff;
};

void api_init_func(struct s_accessor* foo) {
        foo->s_ptr = NULL;
}

void api_print_func(struct s_accessor *foo)
{
        printf("Value of member 'internal' = %d\n", foo->s_ptr->internal);
}

void api_mutate_func(struct s_accessor *foo, int x)
{
        struct s bar;
        foo->s_ptr = &bar;
        foo->s_ptr->internal = x;
}

使用API的客户端程序:

#include <stdio.h>
#include "api.h"


int main()
{
        struct s_accessor foo;
        api_init_func(&foo);  // set s_ptr to NULL

        api_mutate_func(&foo, 123); // change value of member 'internal' of an instance of struct s

        api_print_func(&foo);   // print member of struct s
}

我对我的代码有以下疑问:

  1. 是否有直接(非 hackish)方式来隐藏我的 API 的实现?

  2. 这是为客户端创建抽象以使用我的 API 的正确方法吗?如果没有,我该如何改进它使其变得更好?

这是在 C 应用程序中进行抽象和封装的正确方法。 使用 C 语言中的不完整类型来隐藏结构细节。您可以定义结构、联合和枚举,而无需列出它们的成员(或值,在枚举的情况下)。这样做会导致类型不完整。您不能声明不完整类型的变量,但可以使用指向这些类型的指针

c 语言中的常量 在每个函数中,特别是那些你在 api 中公开的函数,它们不会改变指针或指针指向的结构数据,更好,应该是 const pointer。这将确保(不知何故 :-) 您仍然可以在 c) 中将其更改为 api 用户您没有更改结构数据。您还可以通过双常量指针来保护数据和地址,请参见下文:

#ifndef API_H
#define API_H

typedef struct s_accessor s_accessor, *p_s_accessor;

void api_init_func(p_s_accessor p_foo);
void api_mutate_func(p_s_accessor p_foo, int x);
void api_print_func(const p_s_accessor const p_foo);

#endif

在api.c你可以完成结构类型:

struct s { 
        int internal; 
        int other_stuff;
};

所有辅助函数在 api.c 中应该是静态的(将函数范围限制为 api.c!

尽量减少 api.h 中的包含。

关于问题 1,我认为您没有办法隐藏实施细节!

“访问器”不是一个好术语。这个术语在面向object的编程中用来表示一种方法。

结构类型 struct s_accessor 实际上是一种叫做 handle 的东西。它包含一个指向真实 object 的指针。句柄是一个双重间接指针:应用程序将指针传递给句柄,句柄包含指向 objects.

的指针

一句古老的谚语说“计算机科学中的任何问题都可以通过添加另一层间接来解决”,句柄就是一个典型的例子。句柄允许 objects 从一个地址移动到另一个地址或被替换。然而,对于应用程序而言,句柄地址代表 object,因此当实现 object 被重新定位或替换时,对于应用程序而言,它仍然是相同的 object .

有了句柄,我们可以做如下事情:

  • 有一个向量object可以增长

  • 有 OOP objects 显然可以改变他们的 class

  • 重新定位 variable-length object 缓冲区和字符串等以压缩它们的内存占用空间

所有都没有 object 更改其内存地址并因此更改身份。因为当这些变化发生时句柄保持不变,所以应用程序不必查找 object 指针的每个副本并用一个新指针替换它;手柄有效地在一个地方解决了这个问题。

尽管如此,句柄在 C API 中并不常见,尤其是 lower-level 中。给定一个不使用句柄的 API,您可以围绕它创建句柄。即使您认为 object 的用户将从句柄中获益,将 API 分成两部分可能会更好:一个只处理 s 的内部用户,以及外部一个 struct s_handle.

如果您正在使用线程,那么句柄需要仔细的并发编程。所以也就是说,即使从应用程序的角度来看,你可以改变handle-referenced object,这很方便,它需要同步。假设我们有一个由句柄引用的向量 object。应用程序代码正在使用它,所以我们不能突然用指向不同向量的指针替换向量(以调整它的大小)。另一个线程正在处理原始指针。通过句柄访问向量或将值存储到向量中的操作必须与替换操作同步。即使所有这些都做对了,它也会增加很多开销,因此应用程序人员可能会注意到一些性能问题并要求在 API 中寻找逃生舱口,比如某些函数函数要“固定”向下一个手柄,这样 object 就不能移动,而有效的操作直接与其中的 s object 一起工作。

出于这个原因,我倾向于远离设计句柄 API,并使这类事情成为应用程序的问题。对于 multi-threaded 应用程序来说,仅使用 well-designed“请只 s”API 可能比完全编写 thread-safe 更容易,健壮、高效的 struct s_handle 层。

  1. Is there a direct (non-hackish) way to hide the implementation of my API?

基本上,在 C 中隐藏 API 实现的“规则 #1”不允许 init 操作,客户端应用程序声明一些内存和您的 API初始化它。也就是说,可能是这样的:

typedef struct opaque opaque_t;

#ifndef OPAQUE_IMPL

struct opaque {
  int dummy[42]; // big enough for all future extension
} opaque_t;

#endif

void opaque_init(opaque_t *o);

在此声明中,我们没有向客户透露任何内容,只是我们的 object 是需要 int 对齐的内存缓冲区,并且至少为 42 int

实际上,object更小;我们刚刚为未来的增长增加了储备金。只要我们的 object 不需要超过 int [42] 字节,我们就可以使我们的实际 object 更大而不必 re-compile 客户端。

为什么我们有 #ifndef 是因为实现代码会做这样的事情:

#define OPAQUE_IMPL // 抑制 header 中的假定义 #include “opaque.h”

//实际定义 结构不透明 { 随便什么; 字符 *名称; };

这种事情不符合 ISO C 的“法律”,因为客户端和实现实际上使用了 struct opaque 类型的不同定义。

允许客户端自己分配 objects 会产生一定的效率,因为在自动存储中分配 objects(即将它们声明为局部变量)可以将它们放入堆栈中,开销很小与动态内存分配相比。

更常见的不透明方法是根本不提供init操作,只提供分配新object并销毁它的操作:

typedef struct opaque opaque_t; // incomplete struct

opaque_t *opaque_create(/* args .... */);
void opaque_destroy(opaque_t *o);

现在调用者什么都不知道,除了“不透明”object 表示为一个指针,在其整个生命周期中都是同一个指针。

总o对于应用程序或应用程序框架内部的 API,水性可能不值得。它对拥有外部客户的 API 很有用,例如不同团队或组织中的应用程序开发人员。

问问自己这个问题:这个 API 的客户端及其实现是否会单独发布和升级?如果答案是否定的,那么就减少了对完全不透明的需求。