Pregunta Asignación estática de tipos de datos opacos


Muy a menudo malloc () no está permitido en absoluto cuando se programan sistemas embebidos. La mayoría de las veces soy bastante capaz de lidiar con esto, pero una cosa me irrita: me impide usar los llamados 'tipos opacos' para permitir el ocultamiento de datos. Normalmente haría algo como esto:

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

Ahí va: create_handle () realiza un malloc () para crear una 'instancia'. Una construcción utilizada a menudo para evitar tener que malloc () es cambiar el prototipo de create_handle () de esta manera:

void create_handle(handle_t *handle);

Y luego la persona que llama podría crear el mango de esta manera:

// 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!
}

¡Pero desafortunadamente este código es obviamente inválido, el tamaño de handle_t no se conoce!

Nunca encontré una solución para resolver esto de una manera adecuada. Me gustaría saber si alguien tiene una forma adecuada de hacerlo, o tal vez un enfoque completamente diferente para permitir la ocultación de datos en C (no usar variables globales estáticas en el módulo.c por supuesto, uno debe ser capaz de crear múltiples instancias) )


32
2017-12-14 14:59


origen


Respuestas:


Puede usar la función _alloca. Creo que no es exactamente Estándar, pero hasta donde yo sé, casi todos los compiladores comunes lo implementan. Cuando lo usa como un argumento predeterminado, asigna la pila de la persona que llama.

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

También podría usar algún tipo de agrupamiento de objetos semi-montón: si tiene un número máximo de objetos disponibles actualmente, entonces podría asignarles toda la memoria de forma estática, y solo desplazar los bits para cuáles están actualmente en uso.

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

Mi cambio de bit está un poco apagado, ha pasado mucho tiempo desde que lo hice, pero espero que entiendas el punto.


15
2017-12-14 15:19



Una forma sería agregar algo como

#define MODULE_HANDLE_SIZE (4711)

para el publico module.h encabezamiento. Dado que eso crea un requisito preocupante de mantener esto sincronizado con el tamaño real, la línea es, por supuesto, mejor autogenerada por el proceso de construcción.

La otra opción es, por supuesto, exponer realmente la estructura, pero documentarla como opaca y prohibir el acceso a través de cualquier otro medio que no sea a través de la API definida. Esto se puede aclarar haciendo algo como:

#include "module_private.h"

typedef struct
{
  handle_private_t private;
} handle_t;

Aquí, la declaración real del identificador del módulo se ha movido a un encabezado separado para hacerlo menos visible. Un tipo declarado en ese encabezado es simplemente envuelto en el deseado typedef nombre, asegurándose de indicar que es privado.

Funciones dentro del módulo que toman handle_t * puede acceder de forma segura private como un handle_private_t valor, ya que es el primer miembro de la estructura pública.


8
2017-12-14 15:06



Una solución si crear un grupo estático de struct handle_t objetos, y luego proporcionarlos como necesarios. Hay muchas maneras de lograr eso, pero a continuación se muestra un ejemplo ilustrativo simple:

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

Hay formas más rápidas y más rápidas de encontrar un identificador no utilizado; por ejemplo, podría mantener un índice estático que se incrementa cada vez que se asigne un identificador y 'wraps-around' cuando alcance MAX_HANDLES; esto sería más rápido para la situación típica en la que se asignan varias asas antes de liberar una. Sin embargo, para un pequeño número de identificadores, esta búsqueda de fuerza bruta probablemente sea adecuada.

Por supuesto, el identificador no necesita ser un puntero, sino que podría ser un simple índice en el grupo oculto. Esto mejoraría el ocultamiento de datos y la protección del grupo de acceso externo.

Entonces el encabezado tendría:

typedef int handle_t ;

y el código cambiaría de la siguiente manera:

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

Debido a que el identificador devuelto ya no es un puntero a los datos internos, y el usuario inquisitivo o malicioso no puede obtener acceso a él a través del identificador.

Tenga en cuenta que puede necesitar agregar algunos mecanismos de seguridad de subprocesos si obtiene identificadores en múltiples subprocesos.


4
2017-12-14 17:15



Desafortunadamente, creo que la forma típica de tratar este problema es simplemente haciendo que el programador trate el objeto como opaco: la implementación de la estructura completa está en el encabezado y disponible, es solo responsabilidad del programador no usar las partes internas directamente, solo a través de las API definidas para el objeto.

Si esto no es lo suficientemente bueno, algunas opciones podrían ser:

  • use C ++ como una 'mejor C' y declare las partes internas de la estructura como private.
  • ejecuta algún tipo de preprocesador en los encabezados para que se declaren las partes internas de la estructura, pero con nombres inutilizables. El encabezado original, con buenos nombres, estará disponible para la implementación de las API que administran la estructura. Nunca he visto usar esta técnica, es solo una idea en la cabeza que podría ser posible, pero parece mucho más problemático de lo que vale.
  • tener su código que usa punteros opacos declarar los objetos asignados estáticamente como extern (es decir, globales) Luego tiene un módulo especial que tiene acceso a la definición completa del objeto que realmente declara estos objetos. Como solo el módulo 'especial' tiene acceso a la definición completa, el uso normal del objeto opaco permanece opaco. Sin embargo, ahora debes confiar en tus programadores para no abusar del hecho de que tus objetos son globales. También ha aumentado el cambio de colisiones de nombres, por lo que debe ser administrado (probablemente no sea un gran problema, excepto que podría ocurrir involuntariamente - ¡ay!).

Creo que, en general, confiar en que los programadores sigan las reglas para el uso de estos objetos podría ser la mejor solución (aunque el uso de un subconjunto de C ++ tampoco está mal, en mi opinión). Dependiendo de que sus programadores sigan las reglas de no usar la estructura interna, no es perfecto, pero es una solución viable que es de uso común.


3
2017-12-14 15:52



Me enfrenté a un problema similar al implementar una estructura de datos en la que el encabezado de la estructura de datos, que es opaca, contiene todos los diversos datos que deben trasladarse de una operación a otra.

Como la reinicialización podría causar una pérdida de memoria, quería asegurarme de que la implementación de la estructura de datos sí mismo nunca sobrescribir un punto para acumular la memoria asignada.

Lo que hice fue lo siguiente:

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

En el archivo de implementación:

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

lado del cliente:

int foo() 
{
    data_structure_header ds = { 0 };

    ds_init_new(&ds /*, ...*/);
}

1
2018-05-22 09:40



Estoy un poco confundido por qué dices que no puedes usar malloc (). Obviamente, en un sistema integrado tiene poca memoria y la solución habitual es tener su propio administrador de memoria que malloc un gran grupo de memoria y luego asigna trozos de esto según sea necesario. He visto varias implementaciones diferentes de esta idea en mi tiempo.

Sin embargo, para responder a su pregunta, ¿por qué no asigna de manera estática una matriz de tamaño fijo en module.c agrega una marca "en uso" y luego crea_handle () simplemente devuelve el puntero al primer elemento libre.

Como una extensión de esta idea, el "mango" podría ser un índice entero en lugar del puntero real, lo que evita cualquier posibilidad de que el usuario intente abusar de él fundiéndolo en su propia definición del objeto.


0
2017-12-14 15:24



La solución menos sombría que he visto a esto ha sido proporcionar una estructura opaca para el uso de la persona que llama, que es lo suficientemente grande, más tal vez un poco, junto con una mención de los tipos utilizados en la estructura real, para garantizar que el opaco struct se alineará bastante bien en comparación con el real:

struct Thing {
    union {
        char data[16];
        uint32_t b;
        uint8_t a;
    } opaque;
};
typedef struct Thing Thing;

Entonces las funciones toman un puntero a uno de esos:

void InitThing(Thing *thing);
void DoThingy(Thing *thing,float whatever);

Internamente, no expuesto como parte de la API, hay una estructura que tiene las verdaderas partes internas:

struct RealThing {
    uint32_t private1,private2,private3;
    uint8_t private4;
};
typedef struct RealThing RealThing;

(Este solo tiene uint32_t' anduint8_t ': esa es la razón de la aparición de estos dos tipos en la unión anterior).

Además, probablemente una afirmación en tiempo de compilación para asegurarse de que RealThingEl tamaño de este no excede el de Thing:

typedef char CheckRealThingSize[sizeof(RealThing)<=sizeof(Thing)?1:-1];

Luego, cada función de la biblioteca realiza un molde sobre su argumento cuando va a usarlo:

void InitThing(Thing *thing) {
    RealThing *t=(RealThing *)thing;

    /* stuff with *t */
}

Con esto en su lugar, la persona que llama puede crear objetos del tamaño correcto en la pila y llamar a funciones en su contra, la estructura sigue siendo opaca, y hay alguna comprobación de que la versión opaca sea lo suficientemente grande.

Un problema potencial es que los campos se pueden insertar en la estructura real, lo que significa que requiere una alineación que la estructura opaca no hace, y esto no necesariamente hará que se corte la verificación de tamaño. Muchos de estos cambios cambiarán el tamaño de la estructura, por lo que quedarán atrapados, pero no todos. No estoy seguro de ninguna solución para esto.

Alternativamente, si tiene un (os) encabezado (s) público (s) especial (es) que la biblioteca nunca incluye, entonces probablemente (sujeto a las pruebas contra los compiladores que respalda ...) simplemente escriba sus prototipos públicos con un tipo y los internos con el otro. Todavía sería una buena idea estructurar los encabezados para que la biblioteca vea el público Thing estructura de alguna manera, sin embargo, para que se pueda verificar su tamaño.


0
2017-12-14 18:25



Es simple, simplemente coloque las estructuras en un archivo de encabezado PrivateTypes.h. Ya no será opaco, aún así, será privado para el programador, ya que está dentro de un privado archivo.

Un ejemplo aquí: Ocultar miembros en una estructura C


0
2018-03-12 16:42



Esta es una vieja pregunta, pero dado que también me está mordiendo, quería brindar aquí una posible respuesta (que estoy usando).

Así que aquí hay un ejemplo:

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

Ventajas : publicType se puede asignar en la pila.

Tenga en cuenta que se debe seleccionar el tipo subyacente correcto para garantizar una alineación adecuada (es decir, no utilizar char) Tenga en cuenta también que sizeof(publicType) >= sizeof(privateType). Sugiero una afirmación estática para asegurarme de que esta condición siempre esté marcada. Como nota final, si crees que tu estructura puede evolucionar más adelante, no dudes en hacer que el público sea un poco más grande, para tener espacio para expansiones futuras sin romper ABI.

Desventaja : El casting de tipo público a privado puede desencadenar estrictas advertencias de aliasing.

Descubrí más tarde que este método tiene similitudes con struct sockaddr dentro del socket BSD, que cumple básicamente el mismo problema con advertencias de aliasing estrictas.


0
2018-06-25 05:01