Статическое размещение непрозрачных типов данных
Очень часто 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, конечно, нужно иметь возможность создавать несколько экземпляров).).
10 ответов
Вы можете использовать функцию _alloca. Я считаю, что это не совсем Standard, но, насколько я знаю, его используют почти все распространенные компиляторы. Когда вы используете его в качестве аргумента по умолчанию, он выделяется из стека вызывающего.
// 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;
}
Я немного сдвинулся, прошло много времени с тех пор, как я это сделал, но я надеюсь, что вы поняли суть.
Одним из способов было бы добавить что-то вроде
#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
значение, так как это первый член публичной структуры.
К сожалению, я думаю, что типичный способ справиться с этой проблемой - просто заставить программиста воспринимать объект как непрозрачный - полная структура реализации находится в заголовке и доступна, программист просто обязан не использовать внутренние компоненты напрямую, только через API, определенные для объекта.
Если это не достаточно хорошо, несколько вариантов могут быть:
- использовать C++ как "лучший C" и объявить внутреннюю структуру
private
, - запустите какой-нибудь препроцессор для заголовков, чтобы объявить внутренние структуры, но с неиспользуемыми именами. Оригинальный заголовок с хорошими именами будет доступен для реализации API, управляющих структурой. Я никогда не видел, чтобы этот метод использовался - это всего лишь идея, которая может быть возможной, но кажется, что это гораздо больше проблем, чем оно того стоит.
- ваш код, который использует непрозрачные указатели, объявляет статически размещенные объекты как
extern
(т. е. глобальные) Затем есть специальный модуль, который имеет доступ к полному определению объекта, фактически объявляющего эти объекты. Поскольку только "специальный" модуль имеет доступ к полному определению, обычное использование непрозрачного объекта остается непрозрачным. Однако теперь вы должны полагаться на своих программистов, чтобы они не злоупотребляли тем, что эти объекты являются глобальными. Вы также увеличили изменение коллизий имен, так что этим нужно управлять (вероятно, это не большая проблема, за исключением того, что это может произойти непреднамеренно - ой!).
В целом, я думаю, просто полагаться на то, что ваши программисты следуют правилам использования этих объектов, может быть лучшим решением (хотя, на мой взгляд, использование подмножества C++ тоже неплохо). В зависимости от ваших программистов соблюдение правил неиспользования внутренних структур не является идеальным, но это работоспособное решение, которое широко используется.
Одно из решений, если создать статический пул 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 ;
}
Поскольку возвращаемый дескриптор больше не является указателем на внутренние данные, и любознательный или злонамеренный пользователь не может получить к нему доступ через дескриптор.
Обратите внимание, что вам может понадобиться добавить некоторые механизмы безопасности потоков, если вы получаете дескрипторы в нескольких потоках.
Чтобы расширить некоторые старые обсуждения в комментариях здесь, вы можете сделать это, предоставив функцию распределителя как часть вызова конструктора.
Учитывая некоторый непрозрачный тип
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 ему нужно (например, количество существующих аппаратных периферийных устройств UART на некоторых MCU или количество объектов сообщений шины CAN, необходимых для некоторой реализации CAN и т. д. и т. д.). С использованием
#define
Константы - неудачный дизайн, поскольку мы обычно не хотим, чтобы программисты приложений возились с предоставленными стандартизированными заголовками HAL.
Это просто, просто поместите структуры в заголовочный файл privateTypes.h. Он больше не будет непрозрачным, тем не менее, он будет закрытым для программиста, поскольку он находится внутри частного файла.
Пример здесь: Скрытие членов в структуре C
Я столкнулся с аналогичной проблемой при реализации структуры данных, в которой заголовок структуры данных, который является непрозрачным, содержит все различные данные, которые необходимо перенести из операции в операцию.
Поскольку повторная инициализация может привести к утечке памяти, я хотел убедиться, что сама реализация структуры данных никогда не перезапишет точку для кучи выделенной памяти.
Я сделал следующее:
/**
* 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 /*, ...*/);
}
Это старый вопрос, но, поскольку он также кусает меня, я хотел бы дать здесь возможный ответ (который я использую).
Итак, вот пример:
// 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 sockaddr
внутри сокета BSD, что в основном соответствует той же проблеме со строгими предупреждениями о псевдонимах.
Наименее мрачное решение, которое я видел в этом, состояло в том, чтобы предоставить непрозрачную структуру для использования вызывающей стороной, которая является достаточно большой, плюс, может быть, немного, наряду с упоминанием типов, используемых в реальной структуре, чтобы гарантировать, что непрозрачная структура будет выровнена достаточно хорошо по сравнению с реальной:
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' and
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
Тем не менее, структура как-то так, что его размер можно проверить.
Я немного запутался, почему вы говорите, что не можете использовать malloc(). Очевидно, что во встроенной системе у вас ограниченная память, и обычное решение состоит в том, чтобы иметь свой собственный менеджер памяти, который выделяет большой пул памяти, а затем распределяет его по мере необходимости. Я видел различные варианты реализации этой идеи в свое время.
Чтобы ответить на ваш вопрос, почему бы вам просто не разместить статически массив из них фиксированного размера в module.c, добавить флаг "in-use", а затем create_handle() просто вернуть указатель на первый свободный элемент.
В качестве расширения этой идеи "дескриптор" может быть целочисленным индексом, а не фактическим указателем, который исключает любую возможность того, что пользователь попытается злоупотребить им, приведя его к собственному определению объекта.