【问题标题】:How to expose C struct size without exposing its type?如何在不暴露其类型的情况下暴露 C 结构大小?
【发布时间】:2019-07-13 21:19:06
【问题描述】:

我在 C(不是 C++!)中有以下内容:

module.c
    struct Private {...};
    void foo(void* private, int param) {...}

module.h
    #define PRIVATE_SIZE ???;
    void foo(void* private, int param);

main.c
    char m1[PRIVATE_SIZE];
    char m2[PRIVATE_SIZE];

    int main()
    {
        foo(m1, 10);
        foo(m2, 20);
    }

如何在编译时公开 sizeof(Private) 以便应用程序可以静态分配其存储空间而不公开 Private 类型?

注意,这是一个非常有限的嵌入式系统,动态分配不可用。

【问题讨论】:

  • 为什么静态分配需要它?如果你仍然需要一个 init 函数,为什么不在该函数中动态分配内存呢?
  • 听起来你做不到:要确定你需要知道其所有成员的大小;隐藏其成员意味着您无法确定大小。
  • 这看起来像是一种信息隐藏模式。看看这个代码示例,或许对你有帮助:github.com/adamtornhill/PatternsInC/tree/master/1_FirstClassADT
  • 您可以在 module.c 中定义一个 (const) size_t 变量,该变量使用 sizeof(Private) (在那里已知)进行初始化,并通过 module.h 中的声明将其公开以供公众使用。 [实际上,该变量可能是返回其值的函数中的静态变量;这避免了静态初始化顺序问题。]顺便说一句,我还想知道您是否可以前向声明 Private 并进行更好的类型检查,例如带有Init()的参数。
  • 您不应该忽视对齐要求,因为您的业务只是展示尺寸。

标签: c


【解决方案1】:

您不能为这样的结构分配大小,因为它在编译时是未知的。即使您在运行时确实知道大小,您仍然会因为对齐而遇​​到问题。

有一个可能的解决方案涉及定义一个单独的结构,该结构具有与私有结构相同的大小和对齐要求。

例如:

模块.h:

#include <inttypes.h>

struct Public {
    uint64_t opaque1;
    uint64_t opaque2;
    uint64_t opaque3;
};

void init(struct Public *p);

module.c:

#include <assert.h>
#include <stdalign.h>
#include "module.h"

struct Private {
    int a;
    double b;
    float c;
};

static_assert(sizeof(struct Private)==sizeof(struct Public), "sizes differ");
static_assert(alignof(struct Private)==alignof(struct Public), "alignments differ");

void init(struct Public *p)
{
    struct Private *pr = (struct Private *)p;
    pr->a = 2;
    pr->b = 2.5;
    pr->c = 2.4f;
}

PublicPrivate 结构保证具有相同的大小,并且对齐方式应该 相同。用户可能会编写公共结构的“不透明”字段,在这种情况下,您可能会遇到有关有效类型的别名问题,但如果可以信任用户这样做,那么这应该可以工作。


另一个更强大的选择是,如果您对要支持的最大对象数量有所了解。如果是这种情况,您可以在实现文件中包含这些对象的静态数组,并且 init 函数将返回指向此列表中的对象之一的指针。然后你会有一个相关的清理函数来释放实例。

例如:

module.c:

struct Private {
    int a;
    double b;
    float c;
};

struct PrivateAllocator {
    struct Private obj;
    int used;
};

struct PrivateAllocator list[5] = {
    { { 0, 0, 0}, 0 },
    { { 0, 0, 0}, 0 },
    { { 0, 0, 0}, 0 },
    { { 0, 0, 0}, 0 },
    { { 0, 0, 0}, 0 }
};

struct Private *private_init()
{
    int i;
    for (i=0; i<5; i++) {
        if (!list[i].used) {
            list[i].used = 1;
            return &list[i].obj;
        }
    }
    return NULL;
}

void private_free(struct Private *p)
{
    int i;
    for (i=0; i<5; i++) {
        if (&list[i].obj == p) {
            list[i].used = 0;
            return;
        }
    }
}

【讨论】:

  • 嗨,dbush!您能否提供此处可能出现的对齐问题的示例?
  • @jackhab 在上面的示例中,struct Private 通常具有 8 的对齐要求。如果您刚刚创建了一个 char 数组以将结构分层,则该数组可能没有结盟。在某些系统上,未对齐会导致生成效率低下的代码,甚至导致内存故障。
  • 是的,您可以添加填充变量来填充空间。但无论如何,静态断言非常有效。
【解决方案2】:

C99 允许你使用变长数组。

private.h:

#include <stdio.h>
extern const size_t size;

private.c:

#include "private.h"
struct Private {
        int x;
        int y;
        int z;
};

const size_t size = sizeof(struct Private);

main.c:

#include <stdio.h>
#include "private.h"
int main(void) {
        char m1[size]; //variable length array
        printf("Size of m1 = %ld\n", sizeof(m1));
}

【讨论】:

  • 很遗憾我的编译器 (Microchip XC8) 不支持 VLA
【解决方案3】:

如何在不暴露其类型的情况下暴露 C 结构体的大小?

如果能够妥协一点:(静态 --> main() local)
对于可变长度数组 (C99),使用辅助函数并将数组放入main()

module.h
  size_t foo_size(void);

main.c
  int main() {
    char m1[foo_size()];
    foo(m1, 10);
  }

需要额外的工作来解决对齐问题。

考虑将您的目标放宽为suggested

【讨论】:

    【解决方案4】:

    在符合标准的 C 代码中,即使您在编译时知道其大小(即使您知道对齐方式也不行),您也无法创建任意未知类型的静态实例。

    假设您无论如何都尝试这样做。考虑到宏或枚举PRIVATE_SIZE 的大小,你会怎么做?

    unsigned char obj[PRIVATE_SIZE];
    

    然后你会将(void*)obj 传递到任何需要的地方,对吗? 好吧,这打破了别名规则。虽然您可以合法地访问任何对象中的任何单个字符/字节,但您不能反过来说这些字符不是字符,它们只是存储在其他类型之后。也就是说,您不能合法地将short int 叠加在obj[2]obj[3] 之上,通过smarty-pants 强制转换(例如((struct Private*)obj)-&gt;my_short = 2;)。进行此类操作的唯一合法方法是通过memcpy(),例如memcpy(&amp;temp, obj, sizeof temp); 修改后再返回。或者您需要使用 obj[] 的单个字符。

    有两种可能的方法来做到这一点。一个在另一个答案中描述,基本上定义类型已知的实例,但只让外界有一个指向它的指针。

    另一个,非常相似,用汇编代码定义它,再一次,让外部世界有一个指向它的指针。组装方式的“美”在于您实际上只需要一个名称、一个对齐方式和一个大小来为命名对象分配空间。

    如果你把实例放到一个特殊的数据段中(见 gcc 的段属性和链接器脚本),你甚至可以把所有的实例放在同一个地方(想想,数组),甚至找出它们的累积大小因此计数。

    在没有明确违反任何 C 规则的情况下要做的另一件事是仍然使用这个 unsigned char obj[PRIVATE_SIZE] 技巧,但通过 C 编译器无法查看的汇编函数将其原封不动地传递给它,例如像

    // struct Private* launder(unsigned char*);
    .text
    .globl launder
    launder:
        move %first_param_reg, %return_reg
        ret
    

    但是您确实需要将 unsigned char obj[PRIVATE_SIZE] 更改为可以与您的架构正确对齐的内容,例如double obj[PRIVATE_SIZE / sizeof(double)](如果您更喜欢这种方式,也可以使用 long long)。

    至于PRIVATE_SIZE,你可以在编译时检查它是否匹配类型的大小,例如

    #include "mod.h" // mod.h defines PRIVATE_SIZE
    struct Private { ... };
    extern char StAtIcAsSeRt[sizeof(struct Private) == PRIVATE_SIZE];
    

    【讨论】:

      【解决方案5】:

      您不应该将结构的大小暴露给调用者,因为这首先破坏了私有封装的整个目的。分配您的私人数据与呼叫者无关。另外,避免使用void*,因为它们完全缺乏类型安全性。

      这就是您在 C 中编写私有封装的方式:

      • 在module.h中,前向声明一个不完整的类型typedef struct module module;
      • 在module.c 中,放置此结构的结构定义。它只对 module.c 可见,而对调用者不可见。这称为不透明类型
      • 调用者只能分配指向该结构的指针,不能分配对象。
      • 调用者代码可能如下所示:

        #include "module.h"
        ...
        module* m;
        result = module_init(&m)
        
      • module_init 函数充当“构造函数”,在module.h 中声明并在module.c 中定义:

        bool module_init (module** obj)
        {
          module* m = malloc(sizeof *m);
          ...
          m->something = ...; // init private variables if applicable
        
          *obj = m;
          return true;
        }
        
      • 如果调用者确实需要知道对象的大小,那只是为了硬拷贝等目的。如果需要,提供一个封装分配和复制的复制函数(“复制构造函数” ),例如:

        result module_copy (module** dst, const module* src);
        

      编辑:

      请注意,分配方式是一个单独的问题。您不必为上述设计使用动态分配。例如,在嵌入式系统中,通常使用静态内存池来代替。见Static allocation of opaque data types

      【讨论】:

      • 请看我在问题中的@A.R.C 评论
      • @jackhab 在答案底部添加了一条注释。特别是,请参阅链接帖子中 Clifford 的答案。
      猜你喜欢
      • 2010-09-14
      • 1970-01-01
      • 2018-06-19
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2015-08-08
      • 2023-03-12
      • 1970-01-01
      相关资源
      最近更新 更多