静态分配不透明数据类型

Bar*_*art 39 c embedded opaque-pointers

在为嵌入式系统编程时,通常不允许使用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;
}
Run Code Online (Sandbox Code Playgroud)

你去:create_handle()执行malloc()来创建'实例'.通常用于防止必须使用malloc()的构造是更改create_handle()的原型,如下所示:

void create_handle(handle_t *handle);
Run Code Online (Sandbox Code Playgroud)

然后调用者可以这样创建句柄:

// 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!
}
Run Code Online (Sandbox Code Playgroud)

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

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

Pup*_*ppy 15

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

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

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

// Implementation
int get_size() {
    return sizeof(real_handle_type);
}
something* create_something(void* mem) {
    real_type* ptr = (real_type_ptr*)mem;
    // Fill out real_type
    return (something*)mem;
}
Run Code Online (Sandbox Code Playgroud)

您还可以使用某种对象池半堆的 - 如果你有目前可用的对象的最大数,那么你可以静态分配的所有内存对于他们,只是位移哪些是当前正在使用.

#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;
}
Run Code Online (Sandbox Code Playgroud)

我的位移有点偏,自从我做完以后已经很久了,但我希望你能明白这一点.

  • @onemasse和DeadMG:你是对的,我错过了`get_size()`让这个工作的关键部分.我仍然不是`alloca()`的一个巨大的,但这是问题中提出的问题的一个非常可行的选择. (3认同)

unw*_*ind 9

一种方法是添加类似的东西

#define MODULE_HANDLE_SIZE (4711)
Run Code Online (Sandbox Code Playgroud)

到公众module.h头.由于这会产生令人担忧的要求,即保持与实际大小同步,因此该行当然最好由构建过程自动生成.

另一种选择当然是实际公开结构,但将其记录为不透明,并通过任何其他方式禁止访问,而不是通过定义的API.通过执行以下操作可以更清楚地做到:

#include "module_private.h"

typedef struct
{
  handle_private_t private;
} handle_t;
Run Code Online (Sandbox Code Playgroud)

这里,模块的句柄的实际声明已被移动到一个单独的标题中,使其不太明显可见.然后,在该标头中声明的类型将简单地包装在所需的typedef名称中,确保指示它是私有的.

模块内部的函数handle_t *可以安全地private作为handle_private_t值访问,因为它是public struct的第一个成员.


Cli*_*ord 6

一种解决方案,如果要创建一个静态的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 ;
}
Run Code Online (Sandbox Code Playgroud)

有更快的方法可以找到一个未使用的句柄,例如,你可以保留一个静态索引,每次分配句柄时都会增加,当它到达MAX_HANDLES时会保持"环绕"; 对于在释放任何一个句柄之前分配了几个句柄的典型情况,这会更快.然而,对于少量手柄,这种强力搜索可能就足够了.

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

所以标题会有:

typedef int handle_t ;
Run Code Online (Sandbox Code Playgroud)

并且代码将更改如下:

// 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 ;
}
Run Code Online (Sandbox Code Playgroud)

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

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


Mic*_*urr 5

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

如果这还不够好,可能有以下几种选择:

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

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