Статическое размещение непрозрачных типов данных - PullRequest
39 голосов
/ 14 декабря 2010

Очень часто 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, конечно, нужно иметь возможность создавать несколько экземпляров ).

Ответы [ 9 ]

15 голосов
/ 14 декабря 2010

Вы можете использовать функцию _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;
}

Мой сдвиг битов немного отключен, с тех пор, как я это сделал, прошло много времени, но я надеюсь, что вы поняли.

8 голосов
/ 14 декабря 2010

Одним из способов было бы добавить что-то вроде

#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 голосов
/ 14 декабря 2010

Одно решение, если создать статический пул из 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 голосов
/ 14 декабря 2010

К сожалению, я думаю, что типичный способ справиться с этой проблемой - просто заставить программиста рассматривать объект как непрозрачный - полная реализация структуры находится в заголовке и доступна, программист просто обязан не использовать внутренние компоненты.напрямую, только через API-интерфейсы, определенные для объекта.

Если этого недостаточно, несколько вариантов могут быть:

  • использовать C ++ как «лучший C» и объявитьвнутренние структуры как private.
  • запускают какой-то препроцессор на заголовках, так что внутренние структуры объявляются, но с непригодными именами.Оригинальный заголовок с хорошими именами будет доступен для реализации API, управляющих структурой.Я никогда не видел, чтобы этот метод использовался - это всего лишь идея, которая может быть возможной, но кажется, что это намного сложнее, чем стоит.
  • ваш код, использующий непрозрачные указатели, объявляет статическиВыделенные объекты как extern (т. е. глобальные) Затем имеется специальный модуль, который имеет доступ к полному определению объекта, фактически объявляющего эти объекты.Поскольку только «специальный» модуль имеет доступ к полному определению, обычное использование непрозрачного объекта остается непрозрачным.Однако теперь вы должны полагаться на своих программистов, чтобы они не злоупотребляли тем фактом, что эти объекты являются глобальными.Вы также увеличили изменение коллизий имен, так что этим нужно управлять (возможно, это не большая проблема, за исключением того, что это может произойти непреднамеренно - ой!).

В целом, я полагаюсь, просто полагаясь наВаши программисты, следуя правилам использования этих объектов, могут быть лучшим решением (хотя, на мой взгляд, использование подмножества C ++ также неплохо).В зависимости от ваших программистов соблюдение правил отказа от использования внутренних структур не является идеальным, но это работоспособное решение, которое широко используется.

1 голос
/ 22 мая 2013

Я столкнулся с аналогичной проблемой при реализации структуры данных, в которой заголовок структуры данных, который является непрозрачным, содержит все различные данные, которые необходимо перенести из операции в операцию.

С тех пор-инициализация может вызвать утечку памяти, я хотел убедиться, что реализация структуры данных сама никогда не перезаписывает точку для кучи выделенной памяти.

Я сделал следующее:

/** 
 * 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 голос
/ 12 марта 2013

Это просто, просто поместите структуры в заголовочный файл privateTypes.h.Он больше не будет непрозрачным, тем не менее, он будет закрыт для программиста, поскольку он находится внутри частного файла.

Пример здесь: Скрытие членов в структуре C

0 голосов
/ 25 июня 2015

Это старый вопрос, но, поскольку он также кусает меня, я хотел бы дать здесь возможный ответ (который я использую).

Итак, вот пример:

// 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, что в основном соответствует той же проблеме со строгими предупреждениями о псевдонимах.

0 голосов
/ 14 декабря 2010

Наименее мрачное решение, которое я видел в этом, состояло в том, чтобы предоставить непрозрачную структуру для использования вызывающей стороной, которая является достаточно большой, плюс, возможно, немного, наряду с упоминанием типов, используемых в реальной структуре, чтобы гарантироватьчто непрозрачная структура будет выровнена достаточно хорошо по сравнению с реальной:

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, чтобы можно было проверить ее размер.

0 голосов
/ 14 декабря 2010

Я немного запутался, почему вы говорите, что не можете использовать malloc (). Очевидно, что во встроенной системе у вас ограниченная память, и обычное решение состоит в том, чтобы иметь собственный менеджер памяти, который выделяет большой пул памяти, а затем распределяет его по мере необходимости. Я видел различные варианты реализации этой идеи в свое время.

Чтобы ответить на ваш вопрос, почему бы вам просто не статически выделить их массив фиксированного размера в module.c, добавить флаг «в использовании», а затем create_handle () просто вернуть указатель на первый свободный элемент.

В качестве дополнения к этой идее «дескриптор» может быть целочисленным индексом, а не фактическим указателем, который исключает любую вероятность того, что пользователь попытается злоупотребить им, приведя его к собственному определению объекта.

Добро пожаловать на сайт PullRequest, где вы можете задавать вопросы и получать ответы от других членов сообщества.
...