【问题标题】:Static allocation of opaque data types不透明数据类型的静态分配
【发布时间】:2011-05-25 08:07:15
【问题描述】:

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

【问题讨论】:

  • 也许我错过了什么。为什么不知道handle_t 的大小? “create_handle”接受一个“handlet_t*”类型的参数,所以它应该知道它的大小。我认为如果你传递一个数组,那将是另一回事。
  • @onemasse 在 caller.c 中不知道 handle_t 的大小,只能使用指向 handle_t 的指针。只有在module.c中才知道handle_t的大小
  • @onemasse 前向声明和指针允许使用不透明类型,以便只有实现知道大小,而不是客户端。

标签: c embedded opaque-pointers


【解决方案1】:

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

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

【讨论】:

  • alloca() 没有解决不透明句柄问题 - 需要知道对象的大小,因此对象不能是不透明的。内存池经常被使用。
  • @Michael 大小是通过 get_size() 获得的,它只是“sizeof(struct handle_t)”的包装。如果不支持 alloca,您始终可以使用 C99 可变长度数组。
  • @onemasse 和 DeadMG:你说得对,我错过了 get_size() 如何让这项工作发挥作用的关键部分。我仍然不是alloca() 的大佬,但对于问题中提出的问题,这是一个非常可行的选择。
  • 我永远不会仅仅为了使字段不透明而采用堆或堆等效的内存分配系统,这似乎不是一个好的权衡。
  • 你想设置标志的时候可能是in_use |= (1 &lt;&lt; i);
【解决方案2】:

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

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

【讨论】:

  • 你甚至可以添加一些宏来表示元素“private”根据.c文件包含的不同名称定义;这样,当代码在做不应该做的事情时(例如h-&gt;do_not_use_thisfrom_anywhere_ever.num++)就会变得更加明显,并且还可以稍微轻松地查找违规行为...
  • 我可以接受这个解决方案,但仍然有缺点,如果仅由实现使用的头文件发生更改,则使用的 .c 文件也必须重新编译。对于使用 .c 进行编译,也需要与编译实现相同的包含路径。
【解决方案3】:

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

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

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

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

【讨论】:

    【解决方案4】:

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

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

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

    【讨论】:

      【解决方案5】:

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

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

      【讨论】:

      • 这不是一个好主意,因为私有封装的主要原因不是担心程序员故意做坏事,而是程序员不小心做了坏事,如果结构声明是全局可见的。在 IDE 代码完成时代尤其如此,您可以键入 myfoo.,然后 IDE 很乐意为您提供一些可供选择的替代方案。
      • @Lundin 这个想法得到了诸如“嵌入式 C 的 TDD”等书籍的辩护。我同意您提到的缺点,并且我相信真正的私有将使您的软件设计更加困难或影响运行时修改,例如采用 malloc。
      • 此线程中的许多答案(例如 Clifford 发布的答案)表明,通过实现一个简单的私有内存池来保持不透明类型非常简单——这对于嵌入式系统来说是理想的。好吧,我确实曾经短暂地读过那本书,并没有留下很深刻的印象,它几乎不是一本规范的参考书。
      • 我们可以争论很多,这是一个品味问题。如果我确实需要一个内存池,我会使用 Clifford 解决方案,这不仅仅是为了不透明。你看不一样,没关系,我不认为你的观点不是一个好主意,这些都是品味问题。我可以争辩说您正在增加复杂性,并且您可以争辩说我没有提供任何安全性。我认为我们可以跳过尝试找出哪个更好;)
      • 我在实际应用程序中所做的是,如果它只是一些简单的结构,则保持它是公开的,但如果它是像带有 HAL 的驱动程序这样更复杂的东西,则保持它不透明。此外,您可以使用带有私有标头的 opaque 类型实现,您只允许 opaque 类型的派生类访问。这样您就可以在 C 中实现多态性。
      【解决方案6】:

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

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

      我所做的如下:

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

      【讨论】:

      • +1:但CT_HEADER_SIZE 可以小于sizeof(struct imp_header),因为结构中可能会出现填充。对我来说,它需要为 CT_HEADER_SIZE 工作很多多余的 handish
      • struct header 如果静态分配可能无法正确对齐:它没有与struct imp_header 相同的对齐要求。见stackoverflow.com/a/17619016/611560
      【解决方案7】:

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

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

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

      【讨论】:

      • malloc() 在嵌入式系统上经常被禁止使用静态分配,因为它会引入难以或不可能测试的碎片和场景。特别是对于具有较长“正常运行时间”要求的系统。如果您的对象是静态分配的,那么系统构建时内存分配不会失败。
      • 也许我应该把它作为一个问题,这样你就可以回答了。我们的系统存在一些碎片问题。我们有一种内存池类型,它具有某种可移动块系统(不太确定它是如何工作的),因此您可以对内存进行碎片整理,但据我所知没有人使用它。
      • 避免在嵌入式系统上使用 malloc() 的另一个原因是代码大小。通常,libc malloc 实现并不小,并且它包含许多其他代码,如果您遇到代码大小边界,您宁愿不这样做。
      【解决方案8】:

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

      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 结构,以便可以检查其大小。

      【讨论】:

      • 由于对齐方面的考虑,您的方法有问题。不透明结构需要类似于 long opaque[MAX_SIZE/sizeof(long)]; 或更好,一个包含所需大小的 char 数组和用于对齐目的的所有“大”类型的联合。
      • @R 我已经发布了关于此类对齐问题的问题/答案:stackoverflow.com/questions/17619015/…
      • 严格的别名警告怎么样?
      【解决方案9】:

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

      所以这里是一个例子:

      // 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) &gt;= sizeof(privateType)。 我建议使用静态断言以确保始终检查此条件。 最后一点,如果您认为您的结构可能会在以后发展,请不要犹豫,让公共类型更大一点,以便在不破坏 ABI 的情况下为未来的扩展留出空间。

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

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

      【讨论】:

        猜你喜欢
        • 1970-01-01
        • 1970-01-01
        • 2012-01-17
        • 2013-07-12
        • 2021-12-09
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        • 1970-01-01
        相关资源
        最近更新 更多