48

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

4

10 回答 10

16

您可以使用 _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;
}

我的位移有点不对劲,我已经很久没有做过了,但我希望你明白这一点。

于 2010-12-14T15:19:54.163 回答
9

一种方法是添加类似

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

于 2010-12-14T15:06:59.383 回答
7

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

如果这还不够好,一些选项可能是:

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

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

于 2010-12-14T15:52:51.423 回答
7

一种解决方案是创建一个静态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 ;
}

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

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

于 2010-12-14T17:15:39.463 回答
1

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

此处的示例: 在 C 结构中隐藏成员

于 2013-03-12T16:42:06.917 回答
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 /*, ...*/);
}
于 2013-05-22T09:40:35.943 回答
0

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

但是,要回答您的问题,您为什么不简单地在 module.c 中静态分配它们的固定大小数组,添加一个“in-use”标志,然后让 create_handle() 简单地将指针返回到第一个空闲元素。

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

于 2010-12-14T15:24:19.660 回答
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结构,以便可以检查其大小。

于 2010-12-14T18:25:54.873 回答
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 的情况下为未来的扩展留出空间。

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

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

于 2015-06-25T05:01:25.147 回答
0

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

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

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

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

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

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

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

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

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

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

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

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

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

于 2022-01-11T13:48:16.023 回答