【问题标题】:Is this the right way to implement abstractions in a C API?这是在 C API 中实现抽象的正确方法吗?
【发布时间】:2020-07-06 22:12:24
【问题描述】:

我想用 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. 是否有直接(非骇客)的方法来隐藏我的 API 的实现?

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

【问题讨论】:

  • 如果你谈论抽象,你还需要谈论封装,当谈论封装时,结构应该只暴露在c文件中(不完整类型)
  • 为什么要使用extern?在这种情况下,您根本不需要使用它。只需定义没有外部的函数签名
  • 从方便的角度来看,使用这个东西时有一个typedef 可以跳过struct 部分。
  • @Adam 感谢您的建议。我已经从头文件中删除了 extern
  • @tadman 感谢您的建议!

标签: c abstraction


【解决方案1】:

“访问器”不是一个好的术语。该术语在面向对象编程中用于表示一种方法。

结构类型struct s_accessor 实际上是一种称为句柄的东西。它包含一个指向真实对象的指针。句柄是双重间接指针:应用程序传递指向句柄的指针,句柄包含指向对象的指针。

一句古老的格言说“计算机科学中的任何问题都可以通过添加另一层间接来解决”,其中句柄就是一个很好的例子。句柄允许对象从一个地址移动到另一个地址或被替换。但是,对于应用来说,句柄地址代表的是对象,所以当实现对象被重定位或替换时,对于应用来说,它仍然是同一个对象。

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

  • 有一个可以增长的矢量对象

  • 拥有可以明显改变其类的 OOP 对象

  • 重新定位可变长度对象,例如缓冲区和字符串,以压缩它们的内存占用

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

尽管如此,句柄在 C API 中往往是不寻常的,尤其是在较低级别的 API 中。给定一个不使用句柄的 API,您可以围绕它创建句柄。即使您认为对象的用户将从句柄中受益,最好将 API 分成两部分:一个只处理 s 的内部 API,另一个处理 struct s_handle 的外部 API。

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

出于这个原因,我倾向于远离设计句柄 API,而让这类事情成为应用程序的问题。对于多线程应用程序来说,正确使用设计良好的“请使用 s”API 可能比编写完全线程安全、健壮、高效的 struct s_handle 层更容易。

  1. 是否有直接(非骇客)的方法来隐藏我的 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);

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

实际上,对象更小;我们刚刚为未来的增长增加了储备金。我们可以使我们的实际对象更大而不必重新编译客户端,只要我们的对象不需要超过int [42]字节。

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

#define OPAQUE_IMPL // 抑制头部中的虚假定义 #include "opaque.h"

// 实际定义 结构不透明{ 无论如何; 字符*名称; };

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

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

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

typedef struct opaque opaque_t; // incomplete struct

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

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

对于应用程序或应用程序框架内部的 API,完全不透明可能不值得。它对于具有外部客户端的 API 非常有用,例如不同团队或组织中的应用程序开发人员。

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

【讨论】:

  • 非常感谢您的回答!必须从您的回答中学到很多东西,尤其是一般的句柄。根据您的建议,我不会编写struct s_handle 层,因为我不希望我的代码有不必要的(因为在 C 中一切都是开放的)开销。如果我打算只编写 s 的 API(没有 struct s_handle 层),你认为我应该添加哪些额外的函数来使它成为一个设计良好的 API?添加一个mutator函数就够了吗?
  • @RonakSharma 尝试开发 API 以及一些示例应用程序(用作单元测试)。 API 的使用将揭示需要哪些功能。有时,如果您过度预期需要什么,最终会得到未使用的 API 函数。这也是设计问题;把代码放在一边,制作一些关于应用程序如何与 API 交互的序列图。
【解决方案2】:

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

c 语言中的常量 在 evrey 函数中,尤其是那些你在 api 中公开的函数,不改变指针或指针指向的结构数据,更好,应该是const pointer。这将确保(以某种方式 :-) 您仍然可以在 c)中将其更改为 api 用户,您不会更改结构数据。您还可以通过 double const 指针来保护数据和地址,见下文:

#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,我不认为有办法可以隐藏实施细节!

【讨论】:

  • *p_s_accessor; 噗,我认为这是typedef pointer is usually considered bad style。前任。 api_print_func 可以接受 const s_acecssor*。由于我更像是一个 linux 内核编码风格的人,主观上我会选择struct s_accessor *,我认为这里不需要 typedef。
  • @KamilCuk - typedef 在 99% 的情况下使代码更清晰易读。关于 typedef 到指针类型,我同意你的看法。但同时 c 使我们能够这样做。
  • @KamilCuk 大部分 Linux 内核编码风格都是一堆废话,从 8 个空格硬制表符开始。
  • @Adam 感谢您的回答!将按照您的建议更改我的代码
  • @我的荣幸。谢谢。
猜你喜欢
  • 1970-01-01
  • 2012-11-11
  • 2019-09-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多