不透明数据类型的静态分配

发布于 2024-10-07 12:41:22 字数 1201 浏览 2 评论 0原文

在嵌入式系统编程时,malloc() 通常是绝对不允许的。大多数时候我都能很好地处理这个问题,但有一件事让我恼火:它阻止我使用所谓的“不透明类型”来启用数据隐藏。通常我会这样做: 就是这样

// In file module.h
typedef struct handle_t handle_t;

handle_t *create_handle();
void operation_on_handle(handle_t *handle, int an_argument);
void another_operation_on_handle(handle_t *handle, char etcetera);
void close_handle(handle_t *handle);


// In file module.c
struct handle_t {
    int foo;
    void *something;
    int another_implementation_detail;
};

handle_t *create_handle() {
    handle_t *handle = malloc(sizeof(struct handle_t));
    // other initialization
    return handle;
}

:create_handle() 执行 malloc() 来创建一个“实例”。经常用于防止必须使用 malloc() 的构造是像这样更改 create_handle() 的原型:

void create_handle(handle_t *handle);

然后调用者可以通过这种方式创建句柄:

// In file caller.c
void i_am_the_caller() {
    handle_t a_handle;    // Allocate a handle on the stack instead of malloc()
    create_handle(&a_handle);
    // ... a_handle is ready to go!
}

但不幸的是,这段代码显然是无效的,handle_t 的大小未知!

我从来没有真正找到解决方案来以正确的方式解决这个问题。我很想知道是否有人有一种正确的方法来做到这一点,或者可能是一种完全不同的方法来启用 C 中的数据隐藏(当然,不使用 module.c 中的静态全局变量,必须能够创建多个实例)。

Very often malloc() is absolutely not allowed when programming for embedded systems. Most of the time I'm pretty able to deal with this, but one thing irritates me: it keeps me from using so called 'opaque types' to enable data hiding. Normally I'd do something like this:

// In file module.h
typedef struct handle_t handle_t;

handle_t *create_handle();
void operation_on_handle(handle_t *handle, int an_argument);
void another_operation_on_handle(handle_t *handle, char etcetera);
void close_handle(handle_t *handle);


// In file module.c
struct handle_t {
    int foo;
    void *something;
    int another_implementation_detail;
};

handle_t *create_handle() {
    handle_t *handle = malloc(sizeof(struct handle_t));
    // other initialization
    return handle;
}

There you go: create_handle() performs a malloc() to create an 'instance'. A construction often used to prevent having to malloc() is to change the prototype of create_handle() like this:

void create_handle(handle_t *handle);

And then the caller could create the handle this way:

// In file caller.c
void i_am_the_caller() {
    handle_t a_handle;    // Allocate a handle on the stack instead of malloc()
    create_handle(&a_handle);
    // ... a_handle is ready to go!
}

But unfortunately this code is obviously invalid, the size of handle_t isn't known!

I never really found a solution to solve this in a proper way. I'd very like to know if anyone has a proper way of doing this, or maybe a complete different approach to enable data hiding in C (not using static globals in the module.c of course, one must be able to create multiple instances).

如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。

扫码二维码加入Web技术交流群

发布评论

需要 登录 才能够评论, 你可以免费 注册 一个本站的账号。

评论(11

笔落惊风雨 2024-10-14 12:41:22

您可以使用_alloca函数。我相信它并不完全是标准的,但据我所知,几乎所有常见的编译器都实现了它。当您将它用作默认参数时,它会从调用者的堆栈中分配。

// Header
typedef struct {} something;
size_t get_size();
something* create_something(void* mem);

// Usage
something* ptr = create_something(_alloca(get_size())); // or define a macro.

// Implementation
size_t get_size() {
    return sizeof(real_handle_type);
}
something* create_something(void* mem) {
    real_handle_type* ptr = (real_handle_type*)mem;
    // Fill out real_type
    return (something*)mem;
}

您还可以使用某种对象池半堆 - 如果您有当前可用对象的最大数量,那么您可以静态地为它们分配所有内存,并且只需对当前正在使用的对象进行位移。

#define MAX_OBJECTS 32
real_type objects[MAX_OBJECTS];
unsigned int in_use; // Make sure this is large enough
something* create_something() {
     for(int i = 0; i < MAX_OBJECTS; i++) {
         if (!(in_use & (1 << i))) {
             in_use |= (1 << i);
             return &objects[i];
         }
     }
     return NULL;
}

我的位移有点偏离,自从我这样做以来已经很长时间了,但我希望你能明白我的意思。

You can use the _alloca function. I believe that it's not exactly Standard, but as far as I know, nearly all common compilers implement it. When you use it as a default argument, it allocates off the caller's stack.

// Header
typedef struct {} something;
size_t get_size();
something* create_something(void* mem);

// Usage
something* ptr = create_something(_alloca(get_size())); // or define a macro.

// Implementation
size_t get_size() {
    return sizeof(real_handle_type);
}
something* create_something(void* mem) {
    real_handle_type* ptr = (real_handle_type*)mem;
    // Fill out real_type
    return (something*)mem;
}

You could also use some kind of object pool semi-heap - if you have a maximum number of currently available objects, then you could allocate all memory for them statically, and just bit-shift for which ones are currently in use.

#define MAX_OBJECTS 32
real_type objects[MAX_OBJECTS];
unsigned int in_use; // Make sure this is large enough
something* create_something() {
     for(int i = 0; i < MAX_OBJECTS; i++) {
         if (!(in_use & (1 << i))) {
             in_use |= (1 << i);
             return &objects[i];
         }
     }
     return NULL;
}

My bit-shifting is a little off, been a long time since I've done it, but I hope that you get the point.

梦里寻她 2024-10-14 12:41:22

一种方法是

#define MODULE_HANDLE_SIZE (4711)

向公共 module.h 标头添加类似内容。由于这会产生一个令人担忧的要求,即使其与实际尺寸保持同步,因此该行当然最好由构建过程自动生成。

当然,另一种选择是实际公开该结构,但将其记录为不透明,并禁止通过定义的 API 以外的任何其他方式进行访问。通过执行以下操作可以使这一点更加清晰:

#include "module_private.h"

typedef struct
{
  handle_private_t private;
} handle_t;

这里,模块句柄的实际声明已移至单独的标头中,以使其不那么明显可见。然后,将在该标头中声明的类型简单地包装在所需的 typedef 名称中,确保表明它是私有的。

模块内采用 handle_t * 的函数可以安全地将 private 作为 handle_private_t 值进行访问,因为它是公共结构的第一个成员。

One way would be to add something like

#define MODULE_HANDLE_SIZE (4711)

to the public module.h header. Since that creates a worrying requirement of keeping this in sync with the actual size, the line is of course best auto-generated by the build process.

The other option is of course to actually expose the structure, but document it as being opaque and forbidding access through any other means than through the defined API. This can be made more clear by doing something like:

#include "module_private.h"

typedef struct
{
  handle_private_t private;
} handle_t;

Here, the actual declaration of the module's handle has been moved into a separate header, to make it less obviously visible. A type declared in that header is then simply wrapped in the desired typedef name, making sure to indicate that it is private.

Functions inside the module that take handle_t * can safely access private as a handle_private_t value, since it's the first member of the public struct.

桃扇骨 2024-10-14 12:41:22

不幸的是,我认为处理这个问题的典型方法是简单地让程序员将对象视为不透明 - 完整的结构实现位于标头中并且可用,程序员的责任只是不直接使用内部,而只使用内部结构。通过为对象定义的 API。

如果这还不够好,可能有一些选择:

  • 使用 C++ 作为“更好的 C”并将结构的内部声明为 private
  • 在标头上运行某种预处理器,以便声明结构的内部结构,但名称不可用。具有良好名称的原始标头将可用于管理结构的 API 的实现。我从未见过使用这种技术 - 这只是我脑海中的一个想法,可能是可能的,但似乎麻烦远远超过其价值。
  • 让使用不透明指针的代码将静态分配的对象声明为外部(即全局变量)然后有一个特殊的模块可以访问实际声明这些对象的对象的完整定义。由于只有“特殊”模块可以访问完整定义,因此不透明对象的正常使用仍然是不透明的。然而,现在你必须依靠你的程序员不要滥用这些对象是全局的这一事实。您还增加了命名冲突的更改,因此需要对其进行管理(可能不是一个大问题,只是它可能会无意中发生 - 哎呀!)。

我认为总的来说,仅仅依靠程序员遵循这些对象的使用规则可能是最好的解决方案(尽管在我看来使用 C++ 的子集也不错)。依靠程序员遵循不使用结构内部的规则并不完美,但它是一个常用的可行解决方案。

Unfortunately, I think the typical way to deal with this problem is by simply having the programmer treat the object as opaque - the full structure implementation is in the header and available, it's just the responsibility of the programmer to not use the internals directly, only through the APIs defined for the object.

If this isn't good enough, a few options might be:

  • use C++ as a 'better C' and declare the internals of the structure as private.
  • run some sort of pre-processor on the headers so that the internals of the structure are declared, but with unusable names. The original header, with good names, will be available to the implementation of the APIs that manage the structure. I've never seen this technique used - it's just an idea off the top of my head that might be possible, but seems like far more trouble than it's worth.
  • have your code that uses opaque pointers declare the statically allocated objects as extern (ie., globals) Then have a special module that has access to the full definition of the object actually declare these objects. Since only the 'special' module has access to the full definition, the normal use of the opaque object remains opaque. However, now you have to rely on your programmers to not abuse the fact that thee objects are global. You have also increased the change of naming collisions, so that need to be managed (probably not a big problem, except that it might occur unintentionally - ouch!).

I think overall, just relying on your programmers to follow the rules for the use of these objects might be the best solution (though using a subset of C++ isn't bad either in my opinion). Depending on your programmers to follow the rules of not using the structure internals isn't perfect, but it's a workable solution that is in common use.

滿滿的愛 2024-10-14 12:41:22

一种解决方案是创建一个 struct handle_t 对象的静态池,然后根据需要提供。有很多方法可以实现这一点,但下面是一个简单的说明性示例:

// In file module.c
struct handle_t 
{
    int foo;
    void* something;
    int another_implementation_detail;

    int in_use ;
} ;

static struct handle_t handle_pool[MAX_HANDLES] ;

handle_t* create_handle() 
{
    int h ;
    handle_t* handle = 0 ;
    for( h = 0; handle == 0 && h < MAX_HANDLES; h++ )
    {
        if( handle_pool[h].in_use == 0 )
        {
            handle = &handle_pool[h] ;
        }
    }

    // other initialization
    return handle;
}

void release_handle( handle_t* handle ) 
{
    handle->in_use = 0 ;
}

有更快的方法来查找未使用的句柄,例如,您可以保留一个静态索引,该索引在每次分配句柄时都会递增,并在分配句柄时“环绕”达到 MAX_HANDLES;对于在释放任何一个句柄之前分配多个句柄的典型情况来说,这会更快。然而,对于少量句柄,这种强力搜索可能就足够了。

当然,句柄本身不再需要是指针,而可以是隐藏池的简单索引。这将增强数据隐藏和保护池免受外部访问。

因此,标头将具有:

typedef int handle_t ;

并且代码将更改如下:

// In file module.c
struct handle_s 
{
    int foo;
    void* something;
    int another_implementation_detail;

    int in_use ;
} ;

static struct handle_s handle_pool[MAX_HANDLES] ;

handle_t create_handle() 
{
    int h ;
    handle_t handle = -1 ;
    for( h = 0; handle != -1 && h < MAX_HANDLES; h++ )
    {
        if( handle_pool[h].in_use == 0 )
        {
            handle = h ;
        }
    }

    // other initialization
    return handle;
}

void release_handle( handle_t handle ) 
{
    handle_pool[handle].in_use = 0 ;
}

因为返回的句柄不再是指向内部数据的指针,并且好奇或恶意的用户无法通过句柄访问它。

请注意,如果要在多个线程中获取句柄,则可能需要添加一些线程安全机制。

One solution if to create a static pool of struct handle_t objects, and provide then as neceessary. There are many ways to achieve that, but a simple illustrative example follows:

// In file module.c
struct handle_t 
{
    int foo;
    void* something;
    int another_implementation_detail;

    int in_use ;
} ;

static struct handle_t handle_pool[MAX_HANDLES] ;

handle_t* create_handle() 
{
    int h ;
    handle_t* handle = 0 ;
    for( h = 0; handle == 0 && h < MAX_HANDLES; h++ )
    {
        if( handle_pool[h].in_use == 0 )
        {
            handle = &handle_pool[h] ;
        }
    }

    // other initialization
    return handle;
}

void release_handle( handle_t* handle ) 
{
    handle->in_use = 0 ;
}

There are faster faster ways of finding an unused handle, you could for example keep a static index that increments each time a handle is allocated and 'wraps-around' when it reaches MAX_HANDLES; this would be faster for the typical situation where several handles are allocated before releasing any one. For a small number of handles however, this brute-force search is probably adequate.

Of course the handle itself need no longer be a pointer but could be a simple index into the hidden pool. This would enhance data hiding and protection of the pool from external access.

So the header would have:

typedef int handle_t ;

and the code would change as follows:

// In file module.c
struct handle_s 
{
    int foo;
    void* something;
    int another_implementation_detail;

    int in_use ;
} ;

static struct handle_s handle_pool[MAX_HANDLES] ;

handle_t create_handle() 
{
    int h ;
    handle_t handle = -1 ;
    for( h = 0; handle != -1 && h < MAX_HANDLES; h++ )
    {
        if( handle_pool[h].in_use == 0 )
        {
            handle = h ;
        }
    }

    // other initialization
    return handle;
}

void release_handle( handle_t handle ) 
{
    handle_pool[handle].in_use = 0 ;
}

Because the handle returned is no longer a pointer to the internal data, and inquisitive or malicious user cannnot gain access to it through the handle.

Note that you may need to add some thread-safety mechanisms if you are getting handles in multiple threads.

勿忘初心 2024-10-14 12:41:22

要扩展此处注释中的一些旧讨论,您可以通过提供分配器函数作为构造函数调用的一部分来实现此目的。

  • 给定一些不透明类型typedef struct opaque opaque;,那么

  • 为分配器函数定义一个函数类型typedef void* alloc_t (size_t bytes);。在本例中,出于兼容性目的,我使用了与 malloc/alloca 相同的签名。

  • 构造函数的实现看起来像这样:

     结构不透明
      {
        int foo; // 一些私有成员
      };
    
      不透明* opaque_construct (alloc_t* alloc, int some_value)
      {
        不透明* obj = alloc(sizeof *obj);
        if(obj == NULL) { 返回 NULL; }
    
        // 初始化成员
        obj->foo = some_value;
    
        返回对象;
      }
    

    也就是说,分配器从已知的构造函数内部获取不透明对象的大小。

  • 对于像嵌入式系统中那样的静态存储分配,我们可以创建一个简单的静态内存池类,如下所示:

    <前><代码>#define MAX_SIZE 100
    静态 uint8_t 内存池 [MAX_SIZE];
    静态 size_t mempool_size=0;

    void* static_alloc(size_t 大小)
    {
    uint8_t* 结果;

    if(mempool_size + 大小 > MAX_SIZE)
    {
    返回空值;
    }

    结果 = &mempool[mempool_size];
    mempool_size += 大小;
    返回结果;
    }

    (这可能会分配在 .bss 中或您自己的自定义部分中,无论您喜欢什么。)

  • <现在,调用者可以决定如何分配每个对象,并且资源受限的微控制器中的所有对象可以共享相同的内存池。用法:

    opaque* obj1 = opaque_construct(malloc, 123);
    不透明* obj2 = opaque_construct(static_alloc, 123);
    不透明* obj3 = opaque_construct(alloca, 123); // 如果支持的话
    

这对于节省内存很有用。如果微控制器应用程序中有多个驱动程序,并且每个驱动程序都隐藏在 HAL 后面,那么它们现在可以共享相同的内存池,而驱动程序实现者无需推测需要每种不透明类型的多少个实例。

举例来说,我们有用于 UART、SPI 和 CAN 硬件外设的通用 HAL。它们可以共享一个集中的部分,而不是驱动程序的每个实现都提供自己的内存池。通常我会通过在 uart.h 中公开一个常量(例如 UART_MEMPOOL_SIZE 5)来解决这个问题,以便用户可以在需要多少个 UART 对象后更改它(例如某些 MCU 上当前 UART 硬件外设的数量,或某些 CAN 实现所需的 CAN 总线消息对象的数量等)。使用#define 常量是一种不幸的设计,因为我们通常不希望应用程序程序员弄乱提供的标准化 HAL 标头。

To expand on some old discussion in comments here, you can do this by providing an allocator function as part of the constructor call.

  • Given some opaque type typedef struct opaque opaque;, then

  • Define a function type for an allocator function typedef void* alloc_t (size_t bytes);. In this case I used the same signature as malloc/alloca for compatibility purposes.

  • The constructor implementation would look something like this:

      struct opaque
      {
        int foo; // some private member
      };
    
      opaque* opaque_construct (alloc_t* alloc, int some_value)
      {
        opaque* obj = alloc(sizeof *obj);
        if(obj == NULL) { return NULL; }
    
        // initialize members
        obj->foo = some_value;
    
        return obj;
      }
    

    That is, the allocator gets provided the size of the opaque object from inside the constructor, where it is known.

  • For static storage allocation like done in embedded systems, we can create a simple static memory pool class like this:

    #define MAX_SIZE 100
    static uint8_t mempool [MAX_SIZE];
    static size_t mempool_size=0;
    
    void* static_alloc (size_t size)
    {
      uint8_t* result;
    
      if(mempool_size + size > MAX_SIZE)
      {
        return NULL;
      }
    
      result = &mempool[mempool_size];
      mempool_size += size;
      return result;
    }
    

    (This might be allocated in .bss or in your own custom section, whatever is preferred.)

  • Now the caller can decide how each object is allocated and all objects in for example a resource-constrained microcontroller can share the same memory pool. Usage:

    opaque* obj1 = opaque_construct(malloc, 123);
    opaque* obj2 = opaque_construct(static_alloc, 123);
    opaque* obj3 = opaque_construct(alloca, 123); // if supported
    

This is useful for the purpose of saving memory. In case you have multiple drivers in a microcontroller application and each makes sense to hide behind a HAL, they can now share the same memory pool without the driver implementer having to speculate how many instances of each opaque type that will be needed.

Say for example that we have generic HAL for hardware peripherals to UART, SPI and CAN. Rather than each implementation of the driver providing its own memory pool, they can all share a centralized section. Normally I would otherwise solve that by having a constant such as UART_MEMPOOL_SIZE 5 exposed in uart.h so that the user may change it after how many UART objects they need (like the the number of present UART hardware peripherals on some MCU, or the number of CAN bus message objects required for some CAN implementation etc etc). Using #define constants is an unfortunate design since we typically don't want application programmers to mess around with provided standardized HAL headers.

孤星 2024-10-14 12:41:22

我在实现数据结构时遇到了类似的问题,其中数据结构的标头是不透明的,包含需要从一个操作转移到另一个操作的所有各种数据。

由于重新初始化可能会导致内存泄漏,因此我想确保数据结构实现本身永远不会真正覆盖堆分配内存的点。

我所做的如下:

/** 
 * In order to allow the client to place the data structure header on the
 * stack we need data structure header size. [1/4]
**/
#define CT_HEADER_SIZE  ( (sizeof(void*) * 2)           \
                        + (sizeof(int) * 2)             \
                        + (sizeof(unsigned long) * 1)   \
                        )

/**
 * After the size has been produced, a type which is a size *alias* of the
 * header can be created. [2/4] 
**/        
struct header { char h_sz[CT_HEADER_SIZE]; };
typedef struct header data_structure_header;

/* In all the public interfaces the size alias is used. [3/4] */
bool ds_init_new(data_structure_header *ds /* , ...*/);

在实现文件中:

struct imp_header {
    void *ptr1, 
         *ptr2;
    int  i, 
         max;
    unsigned long total;
};

/* implementation proper */
static bool imp_init_new(struct imp_header *head /* , ...*/)
{
    return false; 
}

/* public interface */
bool ds_init_new(data_structure_header *ds /* , ...*/) 
{
    int i;

    /* only accept a zero init'ed header */
    for(i = 0; i < CT_HEADER_SIZE; ++i) {
        if(ds->h_sz[i] != 0) {
            return false;
        }
    }

    /* just in case we forgot something */
    assert(sizeof(data_structure_header) == sizeof(struct imp_header));

    /* Explicit conversion is used from the public interface to the
     * implementation proper.  [4/4]
     */
    return imp_init_new( (struct imp_header *)ds /* , ...*/); 
}

客户端:

int foo() 
{
    data_structure_header ds = { 0 };

    ds_init_new(&ds /*, ...*/);
}

I faced a similar problem in implementing a data structure in which the header of the data structure, which is opaque, holds all the various data that needs to be carried over from operation to operation.

Since re-initialization might cause a memory leak, I wanted to make sure that data structure implementation itself never actually overwrite a point to heap allocated memory.

What I did is the following:

/** 
 * In order to allow the client to place the data structure header on the
 * stack we need data structure header size. [1/4]
**/
#define CT_HEADER_SIZE  ( (sizeof(void*) * 2)           \
                        + (sizeof(int) * 2)             \
                        + (sizeof(unsigned long) * 1)   \
                        )

/**
 * After the size has been produced, a type which is a size *alias* of the
 * header can be created. [2/4] 
**/        
struct header { char h_sz[CT_HEADER_SIZE]; };
typedef struct header data_structure_header;

/* In all the public interfaces the size alias is used. [3/4] */
bool ds_init_new(data_structure_header *ds /* , ...*/);

In the implementation file:

struct imp_header {
    void *ptr1, 
         *ptr2;
    int  i, 
         max;
    unsigned long total;
};

/* implementation proper */
static bool imp_init_new(struct imp_header *head /* , ...*/)
{
    return false; 
}

/* public interface */
bool ds_init_new(data_structure_header *ds /* , ...*/) 
{
    int i;

    /* only accept a zero init'ed header */
    for(i = 0; i < CT_HEADER_SIZE; ++i) {
        if(ds->h_sz[i] != 0) {
            return false;
        }
    }

    /* just in case we forgot something */
    assert(sizeof(data_structure_header) == sizeof(struct imp_header));

    /* Explicit conversion is used from the public interface to the
     * implementation proper.  [4/4]
     */
    return imp_init_new( (struct imp_header *)ds /* , ...*/); 
}

client side:

int foo() 
{
    data_structure_header ds = { 0 };

    ds_init_new(&ds /*, ...*/);
}
蔚蓝源自深海 2024-10-14 12:41:22

这是一个老问题,但由于它也困扰着我,我想在这里提供一个可能的答案(我正在使用)。

这是一个例子:

// file.h
typedef struct { size_t space[3]; } publicType;
int doSomething(publicType* object);

// file.c
typedef struct { unsigned var1; int var2; size_t var3; } privateType;

int doSomething(publicType* object)
{
    privateType* obPtr  = (privateType*) object;
    (...)
}

优点
publicType 可以在堆栈上分配。

请注意,必须选择正确的基础类型以确保正确对齐(即不要使用 char)。
另请注意 sizeof(publicType) >= sizeof(privateType)
我建议使用静态断言来确保始终检查此条件。
最后一点,如果您认为您的结构以后可能会发展,请毫不犹豫地将公共类型做得更大一些,以便为未来的扩展留出空间,而不会破坏 ABI。

缺点
从公共类型转换为私有类型可能会触发严格别名警告

后来我发现这个方法和BSD socket中的struct sockaddr有相似之处,基本上遇到了严格别名警告的相同问题。

This is an old question, but since it's also biting me, I wanted to provide here a possible answer (which I'm using).

So here is an example :

// file.h
typedef struct { size_t space[3]; } publicType;
int doSomething(publicType* object);

// file.c
typedef struct { unsigned var1; int var2; size_t var3; } privateType;

int doSomething(publicType* object)
{
    privateType* obPtr  = (privateType*) object;
    (...)
}

Advantages :
publicType can be allocated on stack.

Note that correct underlying type must be selected in order to ensure proper alignment (i.e. don't use char).
Note also that sizeof(publicType) >= sizeof(privateType).
I suggest a static assert to make sure this condition is always checked.
As a final note, if you believe your structure may evolve later on, don't hesitate to make the public type a bit bigger, to keep room for future expansions without breaking ABI.

Disadvantage :
The casting from public to private type can trigger strict aliasing warnings.

I discovered later on that this method has similarities with struct sockaddr within BSD socket, which meets basically the same problem with strict aliasing warnings.

飞烟轻若梦 2024-10-14 12:41:22

我有点困惑为什么你说不能使用 malloc()。显然,在嵌入式系统上,您的内存有限,通常的解决方案是拥有自己的内存管理器,它会分配一个大内存池,然后根据需要分配其中的块。我在我的时代见过这个想法的各种不同的实现。

为了回答你的问题,为什么你不简单地在 module.c 中静态分配一个固定大小的数组,添加一个“使用中”标志,然后让 create_handle() 简单地返回指向第一个空闲元素的指针。

作为这个想法的扩展,“句柄”可以是一个整数索引而不是实际的指针,这避免了用户试图通过将其转换为自己的对象定义来滥用它的任何机会。

I'm a little confused why you say you can't use malloc(). Obviously on an embedded system you have limited memory and the usual solution is to have your own memory manager which mallocs a large memory pool and then allocates chunks of this out as needed. I've seen various different implementations of this idea in my time.

To answer your question though, why don't you simply statically allocate a fixed size array of them in module.c add an "in-use" flag, and then have create_handle() simply return the pointer to the first free element.

As an extension to this idea, the "handle" could then be an integer index rather than the actual pointer which avoids any chance of the user trying to abuse it by casting it to their own definition of the object.

铜锣湾横着走 2024-10-14 12:41:22

我见过的最简单的解决方案是提供一个不透明的结构供调用者使用,该结构足够大,再加上一点,并提及实际结构中使用的类型,以确保不透明的结构与真实结构相比,结构将足够好对齐:

struct Thing {
    union {
        char data[16];
        uint32_t b;
        uint8_t a;
    } opaque;
};
typedef struct Thing Thing;

然后函数采用指向其中之一的指针:

void InitThing(Thing *thing);
void DoThingy(Thing *thing,float whatever);

在内部,不作为 API 的一部分公开,有一个具有真正内部结构的结构:(

struct RealThing {
    uint32_t private1,private2,private3;
    uint8_t private4;
};
typedef struct RealThing RealThing;

这个结构只有 uint32_t' 和uint8_t'——这就是上面联合中出现这两种类型的原因。)

加上可能的编译时断言以确保 RealThing 的大小不受影响不超过 Thing 的范围:

typedef char CheckRealThingSize[sizeof(RealThing)<=sizeof(Thing)?1:-1];

然后库中的每个函数在要使用它的参数时都会对其参数进行强制转换:

void InitThing(Thing *thing) {
    RealThing *t=(RealThing *)thing;

    /* stuff with *t */
}

有了这个,调用者就可以在堆栈上创建正确大小的对象,并针对它们调用函数,该结构仍然是不透明的,并且需要检查不透明版本是否足够大。

一个潜在的问题是字段可以插入到真实的结构中,这意味着它需要一种不透明结构不需要的对齐方式,并且这不一定会导致大小检查失败。许多此类更改都会改变结构的大小,因此它们会被捕获,但不是全部。我不确定有什么解决办法。

或者,如果您有一个特殊的面向公众的标头,而该库从未包含其自身,那么您可能可以(根据您支持的编译器进行测试......)只使用一种类型和内部类型编写您的公共原型与另一个。不过,构建标头以便库以某种方式看到面向公众的 Thing 结构仍然是一个好主意,以便可以检查其大小。

The least grim solution I've seen to this has been to provide an opaque struct for the caller's use, which is large enough, plus maybe a bit, along with a mention of the types used in the real struct, to ensure that the opaque struct will be aligned well enough compared to the real one:

struct Thing {
    union {
        char data[16];
        uint32_t b;
        uint8_t a;
    } opaque;
};
typedef struct Thing Thing;

Then functions take a pointer to one of those:

void InitThing(Thing *thing);
void DoThingy(Thing *thing,float whatever);

Internally, not exposed as part of the API, there is a struct that has the true internals:

struct RealThing {
    uint32_t private1,private2,private3;
    uint8_t private4;
};
typedef struct RealThing RealThing;

(This one just has uint32_t' anduint8_t' -- that's the reason for the appearance of these two types in the union above.)

Plus probably a compile-time assert to make sure that RealThing's size doesn't exceed that of Thing:

typedef char CheckRealThingSize[sizeof(RealThing)<=sizeof(Thing)?1:-1];

Then each function in the library does a cast on its argument when it's going to use it:

void InitThing(Thing *thing) {
    RealThing *t=(RealThing *)thing;

    /* stuff with *t */
}

With this in place, the caller can create objects of the right size on the stack, and call functions against them, the struct is still opaque, and there's some checking that the opaque version is large enough.

One potential issue is that fields could be inserted into the real struct that mean it requires an alignment that the opaque struct doesn't, and this won't necessarily trip the size check. Many such changes will change the struct's size, so they'll get caught, but not all. I'm not sure of any solution to this.

Alternatively, if you have a special public-facing header(s) that the library never includes itself, then you can probably (subject to testing against the compilers you support...) just write your public prototypes with one type and your internal ones with the other. It would still be a good idea to structure the headers so that the library sees the public-facing Thing struct somehow, though, so that its size can be checked.

小巷里的女流氓 2024-10-14 12:41:22

很简单,只需将结构放入 privateTypes.h 头文件中即可。它不再是不透明的,但它对程序员来说是私有的,因为它位于私有文件内。

这里有一个例子:
隐藏 C 结构体中的成员

It is simple, simply put the structs in a privateTypes.h header file. It will not be opaque anymore, still, it will be private to the programmer, since it is inside a private file.

An example here:
Hiding members in a C struct

衣神在巴黎 2024-10-14 12:41:22

为了进一步扩展,我发现简单性和不透明性之间的这种妥协就足够了:

/* header.h */
typedef struct
{
    uint8_t public_value_0;
    uint8_t public_value_1;
    uint8_t opaque_internal_reserved_area[3];
} my_object_t;

void fun(my_object_t* object);
/* module.c */
typedef struct
{
    uint8_t public_external_area[2];
    uint8_t private_member_0;
    uint8_t private_member_1;
    uint8_t private_member_2;
} my_private_object_t;

typedef union
{
    my_object_t public;
    my_private_object_t private;
}my_private_object_tu;

void fun(my_object_t* object)
{
    my_private_object_tu* work_obj = (my_private_object_tu*) object;
    work_obj->private.private_member_1;
    work_obj->public.public_value_1;
}

这有多种限制:依​​赖于调用者的初始化,复制必须保持同步的分配区域大小......

To expand further, I find this compromise between simplicity and opacity sufficient:

/* header.h */
typedef struct
{
    uint8_t public_value_0;
    uint8_t public_value_1;
    uint8_t opaque_internal_reserved_area[3];
} my_object_t;

void fun(my_object_t* object);
/* module.c */
typedef struct
{
    uint8_t public_external_area[2];
    uint8_t private_member_0;
    uint8_t private_member_1;
    uint8_t private_member_2;
} my_private_object_t;

typedef union
{
    my_object_t public;
    my_private_object_t private;
}my_private_object_tu;

void fun(my_object_t* object)
{
    my_private_object_tu* work_obj = (my_private_object_tu*) object;
    work_obj->private.private_member_1;
    work_obj->public.public_value_1;
}

This has multiple limitations : rely on the initialisation from the caller, duplicate the allocated area size that have to be kept in sync...

~没有更多了~
我们使用 Cookies 和其他技术来定制您的体验包括您的登录状态等。通过阅读我们的 隐私政策 了解更多相关信息。 单击 接受 或继续使用网站,即表示您同意使用 Cookies 和您的相关数据。
原文