静态分配不透明数据类型

问题描述 投票:38回答:9

在为嵌入式系统编程时,通常不允许使用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中使用静态全局变量,必须能够创建多个实例) )。

c embedded opaque-pointers
9个回答
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;
}

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

#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;
}

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


8
投票

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

#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值,因为它是公共结构的第一个成员。


6
投票

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

// 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 ;
}

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

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


5
投票

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

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

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

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


1
投票

它很简单,只需将结构放在privateTypes.h头文件中即可。它不再是不透明的,它仍然是程序员私有的,因为它在私有文件中。

这里有一个例子:Hiding members in a C struct


1
投票

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

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

我做的是以下内容:

/** 
 * 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 /*, ...*/);
}

0
投票

我有点困惑为什么你说你不能使用malloc()。显然,在嵌入式系统上,内存有限,通常的解决方案是拥有自己的内存管理器,它可以对大型内存池进行malloc,然后根据需要分配这些内存池。在我的时代,我已经看到了这个想法的各种不同的实现。

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

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


0
投票

我见过的最不严格的解决方案就是为调用者的使用提供一个不透明的结构,这个结构足够大,可能还有一点,同时提到真实结构中使用的类型,以确保不透明与真实的结构相比,结构将足够对齐:

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' anduint8_t' - 这就是上面联合中出现这两种类型的原因。)

加上可能是一个编译时断言,以确保RealThing的大小不超过Thing的大小:

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

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

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

    /* stuff with *t */
}

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

一个潜在的问题是字段可以插入到真正的结构中,这意味着它需要不透明结构不对齐的对齐,这不一定会使尺寸检查失效。许多此类更改将改变结构的大小,因此它们会被捕获,但不是全部。我不确定是否有任何解决方案。

或者,如果你有一个特殊的面向公众的头文件库,库永远不会包含它,那么你可能(根据你支持的编译器进行测试......)只需编写一种类型的公共原型和你的内部版本与另一个。构建标题仍然是一个好主意,以便库以某种方式看到面向公众的Thing结构,以便可以检查它的大小。


0
投票

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

所以这是一个例子:

// 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的情况下为未来的扩展留出空间。

缺点:从公共类型到私有类型的转换可以触发strict aliasing warnings

后来我发现这个方法与BSD套接字中的struct sockaddr有相似之处,它在严格的别名警告方面遇到了基本相同的问题。

© www.soinside.com 2019 - 2024. All rights reserved.