Reputation: 916
I'd like to create an opaque type while still allowing users of the type to instantiate it e.g. on the stack by typing
struct Foo obj;
/*...*/
Foo_init(&obj);
Foo_do_something(&obj, param);
/*...*/
The customary opaque pointer approach doesn't allow the first line of this example. As a workaround I am placing a public but opaque "data" array inside the public header file with a fixed size. This seems to work for a few examples but I'm a bit unsure about two points:
Is it safe to cast the address of the data array as is done in Foo_get_bar and Foo_set_bar? This works okay in my tests but it looks questionable.
If FOO_DATA_SIZE remains fixed is it reasonable to expect ABI compatibility in user code?
#include <stdio.h>
#include <limits.h>
#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);
}
#pragma once
#include <inttypes.h>
#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);
#include "foo.h"
#include <stdio.h>
#include <string.h>
#include <inttypes.h>
typedef int64_t bar_t;
struct Foo_private {
bar_t bar;
};
_Static_assert(sizeof(struct Foo_private) <= FOO_DATA_SIZE,
"FOO_DATA_SIZE is insufficient for struct Foo_private");
void Foo_init(struct Foo *foo) {
struct Foo_private foodata;
foodata.bar = (bar_t)0;
memcpy(foo->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;
}
Upvotes: 1
Views: 2516
Reputation: 916
I've reviewed the information in these posts:
Why one should not hide a structure implementation that way?
Static allocation of opaque data types
as well as the comments. I think I have an answer that works: I switched to using an opaque pointer type but am now exposing a function call which tells the user how large it is so that he can call alloca or malloc or whatever in order to allocate the space. Basically the requirement is that allocation is performed by the user, not to the implementation.
Modified header:
#pragma once
#include <inttypes.h>
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)
Modified implementation definition:
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);
}
//...
Then in the user code, use alloca with the size provided by Foo_data_size() or use a convenience macro.
This approach removes the fixed-size limitation and hopefully addresses the alignment issues mentioned.
Unrelated is the word volatile in the private struct declaration. Without declaring it this way, gcc at least on win32 tried to optimize away my constraint checks on storing certain values in incompatible representations.
Example usage:
#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);
}
Upvotes: 4