Tipo de datos opaco en C sin puntero opaco

Me gustaría crear un tipo opaco y al mismo tiempo permitir a los usuarios del tipo crear una instancia, por ejemplo, en la stack escribiendo

struct Foo obj; /*...*/ Foo_init(&obj); Foo_do_something(&obj, param); /*...*/ 

El enfoque de puntero opaco habitual no permite la primera línea de este ejemplo. Como solución alternativa, estoy colocando una matriz de “datos” pública pero opaca dentro del archivo de encabezado público con un tamaño fijo. Esto parece funcionar para algunos ejemplos, pero estoy un poco inseguro sobre dos puntos:

  1. ¿Es seguro convertir la dirección de la matriz de datos como se hace en Foo_get_bar y Foo_set_bar? Esto funciona bien en mis pruebas pero parece cuestionable.

  2. Si FOO_DATA_SIZE permanece fijo, ¿es razonable esperar compatibilidad ABI en el código de usuario?


main.c (Ejemplo de código de usuario)

 #include  #include  #include "foo.h" int main() { struct Foo foo; Foo_init(&foo); Foo_set_bar(&foo, INT_MAX); int bar = Foo_get_bar(&foo); printf("Got bar: %d\n", bar); } 

foo.h (cabecera pública)

 #pragma once #include  #define FOO_DATA_SIZE (64) struct Foo { uint8_t data[FOO_DATA_SIZE]; }; void Foo_init(struct Foo *f); void Foo_set_bar(struct Foo *f, int barval); int Foo_get_bar(struct Foo *f); 

foo.c (Implementación)

 #include "foo.h" #include  #include  #include  typedef int64_t bar_t; struct Foo_private { bar_t bar; }; _Static_assert(sizeof(struct Foo_private) data, &foodata, sizeof(struct Foo_private)); } void Foo_set_bar(struct Foo *foo, int barval) { struct Foo_private *foodata = (void*)&(foo->data); foodata->bar = (bar_t)barval; int stored = (int)foodata->bar; if (stored != barval) { fprintf(stderr, "Foo_set_bar(%"PRId64"): warning: bar rounded to %"PRId64"\n", (int64_t)barval, (int64_t)stored); } } int Foo_get_bar(struct Foo *foo) { struct Foo_private *foodata = (void*)&(foo->data); bar_t bar = foodata->bar; return (int)bar; } 

He revisado la información en estas publicaciones:

¿Por qué uno no debería ocultar una estructura de implementación de esa manera?

Asignación estática de tipos de datos opacos

así como los comentarios. Creo que tengo una respuesta que funciona: cambié a usar un tipo de puntero opaco, pero ahora estoy exponiendo una llamada de función que le dice al usuario qué tan grande es para que pueda llamar a alloca o malloc o lo que sea para asignar el espacio. Básicamente, el requisito es que la asignación sea realizada por el usuario, no a la implementación.

Cabecera modificada:

 #pragma once #include  struct Foo; typedef struct Foo Foo; void Foo_init(Foo *f); void Foo_set_bar(Foo *f, int barval); int Foo_get_bar(Foo *f); size_t Foo_data_size(void); #define Foo_alloca() alloca(Foo_data_size()) #define Foo_new() malloc(Foo_data_size()) #define Foo_delete(PTR) do { \ free(PTR); \ PTR = NULL; \ } while(0) 

Definición de implementación modificada:

 typedef int64_t bar_t; struct Foo { volatile bar_t bar; }; void Foo_init(struct Foo *foo) { struct Foo foodata; foodata.bar = (bar_t)0; memcpy(foo, &foodata, Foo_data_size()); } size_t Foo_data_size() { return sizeof(struct Foo); } //... 

Luego, en el código de usuario, use alloca con el tamaño proporcionado por Foo_data_size () o use una macro conveniente.

Este enfoque elimina la limitación de tamaño fijo y, con suerte, aborda los problemas de alineación mencionados.

Sin relación es la palabra volátil en la statement de estructura privada. Sin declararlo de esta manera, gcc al menos en win32 intentó optimizar mis controles de restricción al almacenar ciertos valores en representaciones incompatibles.

Ejemplo de uso:

 #include "foo.h" //... { Foo *foo = Foo_alloca(); Foo_init(foo); Foo_set_bar(foo, INT_MAX); int bar = Foo_get_bar(foo); printf("Got bar: %d\n", bar); }