简体   繁体   English

对于不完整的结构使用堆栈内存的C最佳实践

[英]C best practice for using stack memory for incomplete structs

There are times when I want to have a struct which is incomplete (only a single C file knows about its members), so I can define an API for any manipulations and so developers can't easily manipulate it outside the API. 有时我想要一个不完整的结构(只有一个C文件知道它的成员),所以我可以为任何操作定义一个API,因此开发人员不能轻易地在API之外操作它。

The problem with doing this, its it often means you need a constructor function, which allocates the data, and free it after (using malloc and free ). 这样做的问题,通常意味着你需要一个构造函数,它分配数据,然后释放它(使用mallocfree )。

In some cases this makes very little sense from a memory management perspective, especially if the struct is small, and its allocated and freed a lot. 在某些情况下,从内存管理的角度来看,这很有意义,特别是如果struct很小,并且它的分配和释放很多。

So I was wondering what might be a portable way to keep the members local to the C source file, and still use stack allocation. 所以我想知道什么是可移植的方法来保持成员本地的C源文件,并仍然使用堆栈分配。

Of course this is C, if someone wants to mess with the struct internals they can, but I would like it to warn or error if possible. 当然这是C,如果有人想弄乱struct内部,他们可以,但我希望它可能会发出警告或错误。

Example, of a simple random number generator (only include new/free methods for brevity). 例如,简单随机数生成器(为简洁起见,仅包括新/自由方法)。

Header: rnd.h 标题: rnd.h

struct RNG;
typedef struct RNG RNG;

struct RNG *rng_new(unsigned int seed);
void        rng_free(struct RNG *rng);

Source: rnd.c 资料来源: rnd.c

struct RNG {
    uint64_t X;
    uint64_t Y;
};

RNG *rng_new(unsigned int seed)
{
    RNG *rng = malloc(sizeof(*rng));
    /* example access */
    rng->X = seed;
    rng->Y = 1;
    return rng;
}

void rng_free(RNG *rng)
{
    free(rng);
}

Other source: other.c 其他来源: other.c

#include "rnd.h"
void main(void)
{
    RND *rnd;

    rnd = rnd_new(5);

    /* do something */

    rnd_free(rnd);
}

Possible solutions 可能的解决方案

I had 2 ideas how it could be done, both feel a bit of a kludge. 我有两个想法可以做到这一点,都觉得有点像kludge。

Declare the size only (in the header) 仅声明大小(在标题中)

Add these defines to the header. 将这些定义添加到标题中。

Header: rnd.h 标题: rnd.h

#define RND_SIZE      sizeof(uint64_t[2])
#define RND_STACK_VAR(var) char _##var##_stack[RND_SIZE]; RND *rnd = ((RND *)_##var##_stack)

void rnd_init(RND *rnd, unsigned int seed);

To ensure the sizes are in sync. 确保尺寸同步。

Source: rnd.c 资料来源: rnd.c

#include "rnd.h"

struct RNG {
    uint64_t X;
    uint64_t Y;
};

#define STATIC_ASSERT(expr, msg) \
    extern char STATIC_ASSERTION__##msg[1]; \
    extern char STATIC_ASSERTION__##msg[(expr) ? 1 : 2]

/* ensure header is valid */
STATIC_ASSERT(RND_SIZE == sizeof(RNG))

void rng_init(RNG *rng, unsigned int seed)
{
    rng->X = seed;
    rng->Y = 1;
}

Other source: other.c 其他来源: other.c

#include "rnd.h"

void main(void)
{
    RND_STACK_VAR(rnd);

    rnd_init(rnd, 5);

    /* do something */

    /* stack mem, no need to free */
}

Keeping the size in sync for large struct members may be a hassle, but for small struct's it's not such a problem. 保持大型struct成员的大小同步可能是一个麻烦,但对于小结构,它不是一个问题。

Conditionally hide the struct members (in the header) 有条理地隐藏struct成员(在标题中)

Using GCC's deprecated attribute, however if there is some more portable way to do this it would be good. 使用GCC的弃用属性,但是如果有一些更便携的方法可以做到这一点,那就太好了。

Header: rnd.h 标题: rnd.h

#ifdef RND_C_FILE
#  define RND_HIDE /* don't hide */
#else
#  define RND_HIDE __attribute__((deprecated))
#endif

struct RNG {
    uint64_t X RND_HIDE;
    uint64_t Y RND_HIDE;
};

Source: rnd.c 资料来源: rnd.c

#define RND_C_FILE
#include "rnd.h"

void main(void)
{
    RND rnd;

    rnd_init(&rnd, 5);

    /* do something */

    /* stack mem, no need to free */
}

This way you can use RND as a regular struct defined on the stack, just not access its members without some warning/error. 这样,您可以将RND用作在堆栈上定义的常规结构,只是在没有警告/错误的情况下不访问其成员。 But its GCC only. 但它只是GCC。

You can accomplish this in standard C in a manner similar to your first example, though not without going through a great deal of pain to evade aliasing violations. 你可以用类似于你的第一个例子的方式在标准C中完成这个,尽管不是没有经历过很大的痛苦来逃避混叠违规。

For now let's just look at how to define the type. 现在让我们来看看如何定义类型。 In order to keep it fully opaque we'll need to use a VLA that takes the size from a function at runtime. 为了使其完全不透明,我们需要使用在运行时从函数中获取大小的VLA。 Unlike the size, alignment can't be done dynamically, so we have to maximally align the type instead. 与大小不同,对齐不能动态完成,因此我们必须最大程度地对齐类型。 I'm using C11's alignment specifiers from stdalign.h , but you can substitute your favorite compiler's alignment extensions if you want. 我正在使用stdalign.h C11对齐说明stdalign.h ,但如果需要,可以替换您喜欢的编译器对齐扩展。 This allows the type to freely change without breaking ABI just like a typical heap-allocated opaque type. 这允许类型在不破坏ABI的情况下自由更改,就像典型的堆分配的opaque类型一样。

//opaque.h
size_t sizeof_opaque();
#define stack_alloc_opaque(identifier) \
    alignas(alignof(max_align_t)) char (identifier)[sizeof_opaque()]

//opaque.c
struct opaque { ... };
size_t sizeof_opaque(void) { return sizeof(struct opaque); }

Then, to create an instance blackbox of our faux type, the user would use stack_alloc_opaque(blackbox); 然后,要创建我们的虚拟类型的实例blackbox stack_alloc_opaque(blackbox); ,用户将使用stack_alloc_opaque(blackbox);

Before we can go any further we need to determine how the API is going to be able to interact with this array masquerading as a struct. 在我们进一步讨论之前,我们需要确定API如何能够与伪装成结构的数组进行交互。 Presumably we also want our API to accept heap allocated struct opaque* s, but in function calls our stack object decays to a char* . 据推测,我们还希望我们的API接受堆分配的struct opaque* s,但在函数调用中,我们的堆栈对象衰减为char* There are a few conceivable options: 有一些可以想到的选择:

  • Force the user to compile with an equivalent of -Wno-incompatible-pointer-types 强制用户使用等效的-Wno-incompatible-pointer-types进行编译
  • Force the user to manually cast in every call like func((struct opaque*)blackbox); 强制用户手动转换每个调用,如func((struct opaque*)blackbox);
  • Resort to redefining stack_alloc_opaque() to use a throwaway identifier for the array, and then assign that to a struct opaque pointer within the macro. 尝试重新定义stack_alloc_opaque()以使用数组的一次性标识符,然后将其分配给宏中的struct opaque指针。 But now our macro has multiple statements and we're polluting the namespace with an identifier the user doesn't know about. 但是现在我们的宏有多个语句,我们用一个用户不知道的标识符来污染命名空间。

All of those are pretty undesirable in their own way, and none address the underlying problem that while char* may alias any type, the inverse is not true. 所有这些都以他们自己的方式非常不受欢迎,并且没有解决潜在的问题,即虽然char*可能为任何类型别名,但反之则不正确。 Even though our char[] is perfectly aligned and sized for a struct opaque , reinterpreting it as one through a pointer cast is verboten. 尽管我们的char[]完全对齐并且大小为struct opaque ,但是通过指针转换将其重新解释为一个是verboten。 And we can't use a union to do it, because struct opaque is an incomplete type. 而且我们不能使用联合来做它,因为struct opaque是一个不完整的类型。 Unfortunately that means that the only alias-safe solution is: 不幸的是,这意味着唯一的别名安全解决方案是:

  • Have every method in our API accept a char* or typedef to char* rather than struct opaque* . 我们的API中的每个方法都接受char*或typedef到char*而不是struct opaque* This allows the API to accept both pointer types, while losing all semblance of type safety in the process. 这允许API接受两种指针类型,同时在过程中丢失所有类型安全性。 To make matters worse, any operations within the API will require memcpy ing the function's argument into and back out of a local struct opaque . 更糟糕的是,API 的任何操作都需要memcpy荷兰国际集团函数的参数进和退了出去局部的struct opaque

Which is rather monstrous. 这是相当可怕的。 Even if we disregard strict aliasing, the only way to maintain the same API for heap and stack objects in this situation is the first item (don't do that). 即使我们忽略严格别名,在这种情况下为堆和堆栈对象维护相同API的唯一方法是第一项(不要这样做)。

On the matter of disregarding the standard, there is one other thing: 在无视标准的问题上,还有另一件事:

  • alloca

It's a bad word, but I'd be remiss not to mention it. 这是一个坏词,但我不能不提它。 Unlike a char VLA, and like malloc , alloca returns a void pointer to untyped space. 与char VLA不同,和malloc一样, alloca返回一个指向无类型空间的void指针。 Since it has roughly the same semantics as malloc , its use doesn't require any of the gymnastics listed above. 由于它与malloc具有大致相同的语义,因此它的使用不需要任何上面列出的体操。 Heap and stack API could happily live side by side, differing only in object (de)allocation. 堆栈和堆栈API可以愉快地并存,仅在对象(de)分配方面有所不同。 But alloca is nonstandard, the returned objects have a slightly different lifetime than a VLA, and its use is near universally discouraged. 但是alloca是非标准的,返回的对象与VLA的生命周期略有不同,并且它的使用几乎被普遍阻止。 Unfortunate that it is otherwise well suited to this problem. 不幸的是,它非常适合这个问题。

As far as I can see, there is only one correct solution (#4), only one clean solution (#5), and no good solution. 据我所知,只有一个正确的解决方案(#4),只有一个清洁解决方案(#5),没有好的解决方案。 The way you define the rest of the API depends on which of those you choose. 定义API其余部分的方式取决于您选择的那些。

In some cases this makes very little sense from a memory management perspective, especially if the struct is small, and its allocated and freed a lot. 在某些情况下,从内存管理的角度来看,这很有意义,特别是如果结构很小,并且它的分配和释放很多。

I don't see the problem here. 我没有在这里看到问题。 In your example, someone probably will only use one RND for the lifetime of their program, or at least, a small number of them. 在您的示例中,某人可能只在其程序的生命周期中使用一个RND ,或者至少使用少量RND

And if the struct is allocated and freed a lot then it makes no performance difference whether your library does all the allocating and freeing, or whether their code does it. 如果结构被分配并释放了很多,那么无论你的库是否完成所有的分配和释放,或者它们的代码是否完成,它都没有性能差异。

If you want to permit automatic allocation, then the caller will have to know the size of your struct. 如果要允许自动分配,则调用者必须知道结构的大小。 There is no way of getting around this. 没有办法解决这个问题。 Also, this somewhat defeats the purpose of hiding your implementation, as it means you can't change the size of your struct without breaking the client code. 此外,这有点违背了隐藏实现的目的,因为这意味着您无法在不破坏客户端代码的情况下更改结构的大小。

Further, they will have to allocate memory that is correctly aligned for your struct (ie they can't just go char foo[SIZE_OF_RND]; RND *rng = (RND *)foo; because of alignment issues). 此外,他们必须为你的结构分配正确对齐的内存(即它们不能只是去char foo[SIZE_OF_RND]; RND *rng = (RND *)foo;因为对齐问题)。 Your RND_STACK_VAR example ignores this problem. 您的RND_STACK_VAR示例忽略了此问题。

Perhaps you could publish a SIZE_OF_RND that is the actual size, plus some allowance for alignment. 也许你可以发布一个实际大小的SIZE_OF_RND,加上一些对齐余量。 Then your "new" function uses some hacks to find the right alignment location in that memory and returns a pointer. 然后你的“新”函数使用一些hacks来在该内存中找到正确的对齐位置并返回一个指针。

If it feels kludgey, that's because it is. 如果它感觉像kludgey,那是因为它。 And there is nothing stopping them just writing the bytes inside the RND anyway. 而且无论如何都没有什么能阻止它们在RND中写入字节。 I would just use your first suggestion of RND_new() etc. unless there were a very strong reason why it wasn't suitable. 我会使用您对RND_new()等的第一个建议,除非有一个非常强烈的理由说明它不合适。

In the design of a C-based API, it makes little sense not to have a default functionality of allocation and initialization bundled together, ready for use - just like in C++. 在基于C语言的API的设计中,没有将分配和初始化的默认功能捆绑在一起,随时可以使用就没有多大意义 - 就像在C ++中一样。 Not offering it as the default means of "getting" an instance of an object makes it all too easy to use uninitialized storage. 不提供它作为“获取”对象实例的默认方式使得使用未初始化的存储非常容易。 If one sticks to this rule, there's no need to expose any sizes at all. 如果坚持这个规则,就没有必要暴露任何尺寸。

Atomic stack allocation and initialization works great for classes that don't require destruction. 原子堆栈分配和初始化非常适用于不需要销毁的类。 For such objects, an alloca -based factory function is a viable option in addition to the "default" malloc -based factory function. 对于这样的对象,除了基于malloc的“默认”工厂函数之外,基于alloca的工厂函数是一个可行的选项。

The use of destruction-requiring classes is less obvious, since the "instinct" with alloca-allocated variables is not to have to free them. 使用需要破坏的类不太明显,因为具有alloca-allocated变量的“本能”不必释放它们。 At least if one sticks to the "factory" APIs for both object construction and destruction, it's rather easy to ensure by policy and code checking that the destruction either happens, or the object leaks. 至少如果一个人坚持使用“工厂”API来进行对象构建和破坏,那么通过策略和代码检查确保破坏发生或者对象泄漏是相当容易的。 An alloca -ted object's memory won't ever leak, but it may be forgotten to be destructed and its resources (including additional memory!) can certainly leak. 一个alloca -ted对象的内存永远不会泄漏,但它可能被遗忘被破坏和资源 (包括额外的内存!)肯定会泄漏。

Suppose we have an interface for a 24 bit arithmetic type, written in the style of C interfaces and Implementations . 假设我们有一个24位算术类型的接口,C接口和实现的方式编写。

#ifndef INT24_INCLUDED
#define INT24_INCLUDED
#define T Int24_T
typedef struct T *T;
extern T Int24_new(void);
extern void Int24_free(T**);
extern void Int24_set_fromint(T, int);
extern void Int24_add(T a, T b);
extern int Int24_toint(T);
...
#undef T
#endif

The Int24_new function returns a new 24-bit integer allocated on the heap, and there's nothing that needs to be done to destruct it when freeing it: Int24_new函数返回在堆上分配的新的24位整数,并且在释放它时不需要做任何破坏它的操作:

struct T {
  int val:24;
};    

T Int24_new(void) {
  T int24 = malloc(sizeof(struct T));
  int24->val = 0;
  return int24;
}

void Int24_free(T ** int24) {
  assert(int24);
  free(*int24);
  *int24 = NULL;
}

We can have an Int24_auto macro that does the same, but allocates on the stack. 我们可以有一个相同的Int24_auto宏,但在堆栈上分配。 We can't call alloca() within the function, since the the moment we return it, it's a dangling pointer - return from the function "deallocates" the memory. 我们不能在函数中调用alloca() ,因为当我们返回它时,它是一个悬空指针 - 从函数“释放”内存返回。 The use of Int24_free on such an object would be an error. 在这样的对象上使用Int24_free将是一个错误。

#define Int24_auto() Int24_auto_impl(alloca(sizeof(struct T)))
T Int24_auto_impl(void * addr) {
  T int24 = addr;
  int24->val = 0;
  return int24;
}

The use is straightforward, there's no destruction to be forgotten about, but the API is not consistent: we must not free objects gotten through Int24_auto . 使用很简单,没有可遗忘的破坏,但API不一致:我们不能 free通过Int24_auto获得的对象。

void test(void) {
  Int24_T a = Int24_auto();
  Int24_T b = Int24_auto();
  Int24_set_fromint(a, 1);
  Int24_set_fromint(b, 2);
  Int24_add(a, b);
  assert(Int24_toint(a) == 3);
}

If we can live with the overhead, it's desirable to add a flag to the implementation that lets the free method destruct the instance without treating it as if it were allocated on the heap. 如果我们可以忍受开销,那么最好在实现中添加一个标志,让free方法破坏实例而不将其视为在堆上分配。

struct T {
  int val:24;
  int is_auto:1;
};    

T Int24_new(void) {
  T int24 = malloc(sizeof(struct T));
  int24->val = 0;
  int24->is_auto = 0;
  return int24;
}

#define Int24_auto() Int24_auto_impl(alloca(sizeof(struct T)))
T Int24_auto_impl(void * addr) {
  T int24 = addr;
  int24->val = 0;
  int24->is_auto = 1;
  return int24;
}

void Int24_free(T ** int24) {
  assert(int24);
  if (!(*int24)->is_auto) free(*int24);
  *int24 = NULL;
}

This makes the heap- and stack-allocated uses consistent: 这使得堆和堆栈分配的用途保持一致:

void test(void) {
  Int24_T a = Int24_auto();
  ...
  Int24_free(&a);
  a = Int24_new();
  ...
  Int24_free(&a);
}

We can, of course, have an API that returns the size of an opaque type, and exposes the init and release methods that construct an object in-place, and destruct it, respectively. 当然,我们可以使用返回opaque类型大小的API,并公开构造对象的initrelease方法,并分别对其进行破坏。 The use of such methods is more verbose, and requires more care. 这种方法的使用更加冗长,需要更多的关注。 Suppose we have an array type: 假设我们有一个数组类型:

#ifndef ARRAY_INCLUDED
#define ARRAY_INCLUDED
#define T Array_T
typedef struct T *T;
extern size_t Array_alloc_size(void);
extern void Array_init(T, int length, int size);
extern void Array_release(T);
...
#undef T
#endif

This allows the flexibility in choosing the allocator that we want, at the expense of 1 or 2 extra lines of code per each used object. 这允许灵活地选择我们想要的分配器,代价是每个使用的对象有1或2个额外的代码行。

void test(void) {
  Array_T a = alloca(Array_alloc_size());
  Array_init(a, 10, sizeof(int));
  ...
  Array_release(a);

  a = malloc(Array_alloc_size());
  Array_init(a, 5, sizeof(void*));
  ...
  Array_release(a);
  free(a);
}

I'd consider such an API to be too error prone, especially that it makes certain kinds of future implementation detail changes rather cumbersome. 我认为这样的API太容易出错,特别是它使得某些类型的未来实现细节变化相当麻烦。 Suppose that we were to optimize our array by allocating all storage in one go. 假设我们通过一次性分配所有存储来优化我们的阵列。 This would require the alloc_size method to take the same parameters as init . 这将要求alloc_size方法采用与init相同的参数。 This seems outright stupid when the new and auto factory methods can take care of it in one go, and retain the binary compatibility in spite of implementation changes. newauto工厂方法可以一次性处理它时,这似乎是完全愚蠢的,并且尽管实现了更改,仍然保持二进制兼容性。

A third solution: split your header file into a public and private parts, and declare the struct in the public part and define it in the private, non-exportable one. 第三种解决方案:将头文件拆分为公共和私有部分,并在公共部分声明结构,并在私有的,不可导出的部分中定义它。

Thus external users of your library won't get exact implementation, while your library internals would use a common definition w/o additional efforts. 因此,您的库的外部用户将无法获得准确的实现,而您的库内部将使用通用定义而无需额外的工作。

Here's yet another way to do it. 这是另一种方法。 Just as in your first solution, it's important to keep the size in sync, or really bad things happen. 就像在第一个解决方案中一样,重要的是保持大小同步,或者发生非常糟糕的事情。


main.c main.c中

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

int main( void )
{
    SomeStruct test;

    InitSomeStruct( &test );
    ShowSomeStruct( &test );
}

somestruct.h somestruct.h

#define SOME_STRUCT_SIZE ((sizeof(int) * 2 + sizeof(long double) - 1) / sizeof(long double))

typedef struct
{
    union
    {
        long double opaque[SOME_STRUCT_SIZE];

#ifdef _SOME_STRUCT_SOURCE_
        struct
        {
            int a;
            int b;
        };
#endif

    };
}
    SomeStruct;

void InitSomeStruct( SomeStruct *someStruct );
void ShowSomeStruct( SomeStruct *someStruct );

somestruct.c somestruct.c

#include <stdio.h>
#define _SOME_STRUCT_SOURCE_
#include "somestruct.h"

void InitSomeStruct( SomeStruct *someStruct )
{
    someStruct->a = 55;
    someStruct->b = 99;
}

void ShowSomeStruct( SomeStruct *someStruct )
{
    printf( "a=%d b=%d\n", someStruct->a, someStruct->b );
}

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM