Подтвердить что ты не робот

Статическое выделение непрозрачных типов данных

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

4b9b3361

Ответ 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;
}

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

Ответ 2

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

#define MODULE_HANDLE_SIZE (4711)

для общедоступного заголовка module.h. Поскольку это создает тревожное требование сохранить это в синхронизации с фактическим размером, линия, конечно, лучше всего автогенерируется процессом сборки.

Другим вариантом является, конечно, фактическое раскрытие структуры, но документируйте ее как непрозрачную и запрещающую доступ через любые другие средства, чем через определенный API. Это можно сделать более понятным, выполнив что-то вроде:

#include "module_private.h"

typedef struct handle_t
{
  handle_private_t private;
};

Здесь фактическое объявление дескриптора модуля было перемещено в отдельный заголовок, чтобы сделать его менее видимым. Тип, объявленный в этом заголовке, затем просто завернут в желаемое имя typedef, убедившись, что оно является частным.

Функции внутри модуля, которые принимают handle_t *, могут безопасно получить доступ к private как значение handle_private_t, так как это первый член публичной структуры.

Ответ 3

Одно решение, если создать статический пул объектов 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 ;
}

Поскольку возвращаемый дескриптор больше не является указателем на внутренние данные, любознательный или злонамеренный пользователь не может получить доступ к нему через дескриптор.

Обратите внимание, что вам может потребоваться добавить некоторые механизмы защиты потоков, если вы получаете дескрипторы в нескольких потоках.

Ответ 4

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

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

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

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

Ответ 5

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

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

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

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

Ответ 6

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

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

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

Ответ 7

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

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

Ответ 8

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

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

Ответ 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) >= sizeof(privateType). Я предлагаю статическое утверждение, чтобы убедиться, что это условие всегда проверяется. В заключение, если вы считаете, что ваша структура может развиваться позже, не стесняйтесь сделать публичный тип немного больше, чтобы сохранить место для будущих расширений без нарушения ABI.

Недостаток: Кастинг от открытого до частного типа может вызывать строгие предупреждения об альпинировании.

Позже я обнаружил, что этот метод имеет сходство с struct sockaddr внутри сокета BSD, который встречает в основном ту же проблему со строгими предупреждениями на псевдонимы.